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
222
gaia/shared/js/contacts/contacts_buttons.js
Executable file
222
gaia/shared/js/contacts/contacts_buttons.js
Executable file
|
|
@ -0,0 +1,222 @@
|
|||
'use strict';
|
||||
|
||||
/* exported ContactsButtons */
|
||||
|
||||
/* globals LazyLoader, MozActivity, MultiSimActionButton, Normalizer,
|
||||
SmsIntegration, TelephonyHelper, utils */
|
||||
|
||||
var ContactsButtons = {
|
||||
DEFAULT_TEL_TYPE: 'other',
|
||||
DEFAULT_EMAIL_TYPE: 'other',
|
||||
|
||||
_listContainer: null,
|
||||
_contactDetails: null,
|
||||
|
||||
_activityHandler: null,
|
||||
|
||||
init: function init(listContainer, contactDetails, activityHandler) {
|
||||
this._listContainer = listContainer;
|
||||
this._contactDetails = contactDetails;
|
||||
this._activityHandler = activityHandler;
|
||||
},
|
||||
|
||||
_enableCalls: function enableCalls() {
|
||||
this._contactDetails.classList.remove('calls-disabled');
|
||||
},
|
||||
|
||||
_disableCalls: function disableCalls() {
|
||||
this._contactDetails.classList.add('calls-disabled');
|
||||
},
|
||||
|
||||
_call: function call(phoneNumber, cardIndex) {
|
||||
this._disableCalls();
|
||||
var enableCalls = this._enableCalls.bind(this);
|
||||
LazyLoader.load('/dialer/js/telephony_helper.js', () => {
|
||||
TelephonyHelper.call(phoneNumber, cardIndex, enableCalls, enableCalls,
|
||||
enableCalls, enableCalls);
|
||||
});
|
||||
},
|
||||
|
||||
/* For security reasons we cannot directly call MmiManager.send() thus we
|
||||
* need to special-case MMI numbers. This function detects MMI numbers in
|
||||
* their most common form; other forms (such as short-string MMI codes)
|
||||
* don't make sense for the contacts app and thus aren't considered. */
|
||||
_isMMI: function _isMMI(number) {
|
||||
return number.charAt(number.length - 1) === '#';
|
||||
},
|
||||
|
||||
// Check current situation and setup different listener for the button
|
||||
_setupPhoneButtonListener: function setupPhoneButtonListener(button, number) {
|
||||
var self = this;
|
||||
|
||||
if (this._activityHandler &&
|
||||
this._activityHandler.currentActivityIsNot(['open'])) {
|
||||
button.addEventListener('click', this._onPickNumber.bind(this));
|
||||
} else if (this._isMMI(number)) {
|
||||
button.addEventListener('click', this._onMMICode.bind(this));
|
||||
} else if (navigator.mozTelephony) {
|
||||
LazyLoader.load(['/shared/js/multi_sim_action_button.js'], function() {
|
||||
/* jshint nonew: false */
|
||||
new MultiSimActionButton(button, self._call.bind(self),
|
||||
'ril.telephony.defaultServiceId',
|
||||
() => number);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// If we are currently handing an activity, send the phone
|
||||
// number as result of clicking in the phone button.
|
||||
_onPickNumber: function onPickNumber(evt) {
|
||||
var number = evt.target.dataset.tel;
|
||||
if (this._activityHandler) {
|
||||
this._activityHandler.postPickSuccess({ number: number });
|
||||
}
|
||||
},
|
||||
|
||||
// If the phone number stored in a contact is a MMI code,
|
||||
// launch the dialer with that specific code.
|
||||
_onMMICode: function onMMICode(evt) {
|
||||
var number = evt.target.dataset.tel;
|
||||
// For security reasons we cannot directly call MmiManager.send(). We
|
||||
// need to show the MMI number in the dialer instead.
|
||||
/* jshint nonew: false */
|
||||
new MozActivity({
|
||||
name: 'dial',
|
||||
data: {
|
||||
type: 'webtelephony/number',
|
||||
number: number
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onSendSmsClicked: function onSendSmsClicked(evt) {
|
||||
var tel = evt.target.dataset.tel;
|
||||
LazyLoader.load('/shared/js/contacts/sms_integration.js', () => {
|
||||
SmsIntegration.sendSms(tel);
|
||||
});
|
||||
},
|
||||
|
||||
_onEmailOrPickClick: function onEmailOrPickClick(evt) {
|
||||
evt.preventDefault();
|
||||
var email = evt.target.dataset.email;
|
||||
this.sendEmailOrPick(email);
|
||||
return false;
|
||||
},
|
||||
|
||||
renderPhones: function cd_renderPhones(contact) {
|
||||
if (!contact.tel) {
|
||||
return;
|
||||
}
|
||||
var telLength = this._getLength(contact.tel);
|
||||
for (var tel = 0; tel < telLength; tel++) {
|
||||
var currentTel = contact.tel[tel];
|
||||
var typeKey = currentTel.type;
|
||||
var escapedType = Normalizer.escapeHTML(currentTel.type, true).trim();
|
||||
var carrier = Normalizer.escapeHTML(currentTel.carrier || '', true) || '';
|
||||
|
||||
if (!escapedType) {
|
||||
typeKey = this.DEFAULT_TEL_TYPE;
|
||||
}
|
||||
|
||||
var telField = {
|
||||
value: Normalizer.escapeHTML(currentTel.value, true) || '',
|
||||
type: escapedType,
|
||||
'type_l10n_id': typeKey,
|
||||
carrier: carrier,
|
||||
i: tel
|
||||
};
|
||||
|
||||
var phonesTemplate =
|
||||
document.querySelector('#phone-details-template-\\#i\\#');
|
||||
var template = utils.templates.render(phonesTemplate, telField);
|
||||
|
||||
// Add event listeners to the phone template components
|
||||
var sendSmsButton = template.querySelector('#send-sms-button-' + tel);
|
||||
sendSmsButton.dataset.tel = telField.value;
|
||||
sendSmsButton.addEventListener('click',
|
||||
this._onSendSmsClicked.bind(this));
|
||||
|
||||
var callOrPickButton = template.querySelector('#call-or-pick-' + tel);
|
||||
callOrPickButton.dataset.tel = telField.value;
|
||||
this._setupPhoneButtonListener(callOrPickButton, telField.value);
|
||||
|
||||
if (carrier) {
|
||||
var carrierWrapperElt = template.querySelector('.carrier-wrapper');
|
||||
carrierWrapperElt.hidden = false;
|
||||
}
|
||||
|
||||
this._listContainer.appendChild(template);
|
||||
}
|
||||
},
|
||||
|
||||
renderEmails: function cd_renderEmails(contact) {
|
||||
if (!contact.email) {
|
||||
return;
|
||||
}
|
||||
var emailLength = this._getLength(contact.email);
|
||||
for (var email = 0; email < emailLength; email++) {
|
||||
var currentEmail = contact.email[email];
|
||||
var emailField = {
|
||||
value: Normalizer.escapeHTML(currentEmail.value, true) || '',
|
||||
'type_l10n_id': currentEmail.type,
|
||||
i: email
|
||||
};
|
||||
var emailsTemplate =
|
||||
document.querySelector('#email-details-template-\\#i\\#');
|
||||
var template = utils.templates.render(emailsTemplate, emailField);
|
||||
|
||||
// Send MMS to the email address
|
||||
var sendSmsButton =
|
||||
template.querySelector('#send-sms-to-email-button-' + email);
|
||||
sendSmsButton.dataset.tel = emailField.value;
|
||||
sendSmsButton.addEventListener('click',
|
||||
this._onSendSmsClicked.bind(this));
|
||||
|
||||
// Add event listeners to the phone template components
|
||||
var emailButton = template.querySelector('#email-or-pick-' + email);
|
||||
emailButton.dataset.email = emailField.value;
|
||||
emailButton.addEventListener('click',
|
||||
this._onEmailOrPickClick.bind(this));
|
||||
|
||||
this._listContainer.appendChild(template);
|
||||
}
|
||||
},
|
||||
|
||||
reMark: function(field, value, remarkClass) {
|
||||
var selector = '[data-' + field + '="' + value + '"]';
|
||||
var elements = this._listContainer.querySelectorAll(selector);
|
||||
|
||||
if (typeof remarkClass === 'undefined') {
|
||||
remarkClass = 'remark';
|
||||
}
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
elements[i].classList.add(remarkClass);
|
||||
}
|
||||
},
|
||||
|
||||
_getLength: function(prop) {
|
||||
if (!prop || !prop.length) {
|
||||
return 0;
|
||||
}
|
||||
return prop.length;
|
||||
},
|
||||
|
||||
sendEmailOrPick: function(address) {
|
||||
try {
|
||||
// We don't check the email format, lets the email
|
||||
// app do that
|
||||
/* jshint nonew: false */
|
||||
new MozActivity({
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'mail',
|
||||
URI: 'mailto:' + address
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('WebActivities unavailable? : ' + e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
113
gaia/shared/js/contacts/contacts_cleaner.js
Executable file
113
gaia/shared/js/contacts/contacts_cleaner.js
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
'use strict';
|
||||
/* global utils */
|
||||
|
||||
window.ContactsCleaner = function(contacts) {
|
||||
this.lcontacts = contacts;
|
||||
var total = contacts.length;
|
||||
var next = 0;
|
||||
var self = this;
|
||||
var CHUNK_SIZE = 5;
|
||||
var numResponses = 0;
|
||||
var notifyClean = false;
|
||||
|
||||
var mustHold = false;
|
||||
var holded = false;
|
||||
var mustFinish = false;
|
||||
|
||||
this.start = function() {
|
||||
mustHold = holded = mustFinish = false;
|
||||
|
||||
if (total > 0) {
|
||||
cleanContacts(0);
|
||||
}
|
||||
else if (typeof self.onsuccess === 'function') {
|
||||
window.setTimeout(self.onsuccess, 0);
|
||||
}
|
||||
};
|
||||
|
||||
this.hold = function() {
|
||||
mustHold = true;
|
||||
};
|
||||
|
||||
this.finish = function() {
|
||||
mustFinish = true;
|
||||
|
||||
if (holded) {
|
||||
notifySuccess();
|
||||
}
|
||||
};
|
||||
|
||||
this.resume = function() {
|
||||
mustHold = holded = mustFinish = false;
|
||||
|
||||
window.setTimeout(function resume_clean() {
|
||||
cleanContacts(next);
|
||||
});
|
||||
};
|
||||
|
||||
this.performClean = function(contact, number, cbs) {
|
||||
var req = navigator.mozContacts.remove(utils.misc.toMozContact(contact));
|
||||
req.number = number;
|
||||
req.onsuccess = cbs.success;
|
||||
req.onerror = function(e) {
|
||||
errorHandler(contact.id, e.target.error);
|
||||
};
|
||||
};
|
||||
|
||||
function successHandler(e) {
|
||||
if (notifyClean || typeof self.oncleaned === 'function') {
|
||||
notifyClean = true;
|
||||
// Avoiding race condition so the cleaned element is cached
|
||||
var cleaned = e.target.number;
|
||||
window.setTimeout(function() {
|
||||
self.oncleaned(cleaned);
|
||||
},0);
|
||||
}
|
||||
continueCb();
|
||||
}
|
||||
|
||||
function errorHandler(contactid, error) {
|
||||
if (typeof self.onerror === 'function') {
|
||||
self.onerror(contactid, error);
|
||||
}
|
||||
|
||||
continueCb();
|
||||
}
|
||||
|
||||
function cleanContacts(from) {
|
||||
for (var idx = from; idx < (from + CHUNK_SIZE) && idx < total; idx++) {
|
||||
var contact = contacts[idx];
|
||||
self.performClean(contact, idx, {
|
||||
success: successHandler
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function notifySuccess() {
|
||||
if (typeof self.onsuccess === 'function') {
|
||||
window.setTimeout(self.onsuccess);
|
||||
}
|
||||
}
|
||||
|
||||
function continueCb() {
|
||||
next++;
|
||||
numResponses++;
|
||||
if (next < total && numResponses === CHUNK_SIZE) {
|
||||
numResponses = 0;
|
||||
if (!mustHold && !mustFinish) {
|
||||
cleanContacts(next);
|
||||
}
|
||||
else if (mustFinish && !holded) {
|
||||
notifySuccess();
|
||||
}
|
||||
|
||||
if (mustHold) {
|
||||
holded = true;
|
||||
}
|
||||
}
|
||||
else if (next >= total) {
|
||||
// End has been reached
|
||||
notifySuccess();
|
||||
}
|
||||
} // function
|
||||
}; // ContactsCleaner
|
||||
83
gaia/shared/js/contacts/contacts_datastore.js
Executable file
83
gaia/shared/js/contacts/contacts_datastore.js
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
'use strict';
|
||||
|
||||
/* exported ContactsDatastore */
|
||||
|
||||
/**
|
||||
* This object will be useful for contact providers. It provides convenient
|
||||
* access to a contacts datastore
|
||||
*
|
||||
*/
|
||||
|
||||
var ContactsDatastore = (function() {
|
||||
function ContactsDatastore(provider) {
|
||||
this.provider = provider;
|
||||
|
||||
this._datastoreLoaded = false;
|
||||
this._datastoreLoading = false;
|
||||
}
|
||||
|
||||
function promiseResolution (resolve, reject) {
|
||||
/*jshint validthis:true */
|
||||
this._datastoreLoading = false;
|
||||
this._datastoreLoaded = (this.datastore !== null);
|
||||
|
||||
if (!this.datastore) {
|
||||
reject({
|
||||
name: 'DatastoreNotFound'
|
||||
});
|
||||
}
|
||||
else {
|
||||
resolve(this.datastore);
|
||||
}
|
||||
}
|
||||
|
||||
ContactsDatastore.prototype = {
|
||||
_DS_LOADED_EVENT: 'datastore_loaded',
|
||||
_DS_NAME: 'contacts',
|
||||
|
||||
getStore: function() {
|
||||
var self = this;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!navigator.getDataStores) {
|
||||
reject({
|
||||
name: 'DatastoreNotEnabled'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._datastoreLoaded) {
|
||||
resolve(self.datastore);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._datastoreLoading) {
|
||||
document.addEventListener(self._DS_LOADED_EVENT, function loaded() {
|
||||
document.removeEventListener(self._DS_LOADED_EVENT, loaded);
|
||||
promiseResolution.bind(self)(resolve, reject);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self._datastoreLoading = true;
|
||||
navigator.getDataStores(self._DS_NAME).then(function(stores) {
|
||||
var targetOwner = self.provider;
|
||||
|
||||
for (var j = 0; j < stores.length; j++) {
|
||||
if (stores[j].owner === targetOwner) {
|
||||
self.datastore = stores[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent(self._DS_LOADED_EVENT));
|
||||
promiseResolution.bind(self)(resolve, reject);
|
||||
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return ContactsDatastore;
|
||||
|
||||
}());
|
||||
717
gaia/shared/js/contacts/contacts_matcher.js
Executable file
717
gaia/shared/js/contacts/contacts_matcher.js
Executable file
|
|
@ -0,0 +1,717 @@
|
|||
/* global SimplePhoneMatcher */
|
||||
/* global Normalizer */
|
||||
/* exported Matcher */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
function MatcherObj(pdataProvider) {
|
||||
var blankRegExp = /\s+/g;
|
||||
|
||||
var FB_CATEGORY = 'facebook';
|
||||
var FB_LINKED = 'fb_linked';
|
||||
|
||||
var dataProvider = pdataProvider;
|
||||
|
||||
// Multiple matcher Object. It tries to find a set of Contacts that match at
|
||||
// least one of the targets passed as parameters
|
||||
// ptargets: They are the targets (telephone numbers, emails) we want to
|
||||
// find in the Contacts DB
|
||||
// mmatchingOptions: are the matching options to be passed to the
|
||||
// mozContacts API. Particularly filterBy and filterOp
|
||||
// MultipleMatcher.onmatch is called when there is at least one matching
|
||||
// MultipleMatcher.onmismatch is called when there are no matches
|
||||
|
||||
function MultipleMatcher(ptargets, pmatchingOptions) {
|
||||
var next = 0;
|
||||
var self = this;
|
||||
var targets = ptargets;
|
||||
var matchingOptions = pmatchingOptions;
|
||||
var finalMatchings = {};
|
||||
|
||||
function doMatchBy(target, callbacks) {
|
||||
var options = {
|
||||
filterValue: target,
|
||||
filterBy: matchingOptions.filterBy,
|
||||
filterOp: matchingOptions.filterOp
|
||||
};
|
||||
|
||||
var req = dataProvider.find(options);
|
||||
|
||||
req.onsuccess = function() {
|
||||
var matchings = req.result.filter(function(aResult) {
|
||||
return filterFacebook(aResult, matchingOptions.linkParams);
|
||||
});
|
||||
|
||||
var filterBy = options.filterBy;
|
||||
|
||||
var sanitizedTarget = SimplePhoneMatcher.sanitizedNumber(target);
|
||||
var targetVariants = SimplePhoneMatcher.generateVariants(target);
|
||||
|
||||
matchings.forEach(function(aMatching) {
|
||||
if (matchingOptions.selfContactId === aMatching.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var field = options.filterBy[0];
|
||||
var values = aMatching[field];
|
||||
|
||||
values.forEach(function(aValue) {
|
||||
var value = aValue.value;
|
||||
var sanitizedValue = SimplePhoneMatcher.sanitizedNumber(value);
|
||||
var valueMatched = false;
|
||||
|
||||
if (targetVariants.length > 1) {
|
||||
if (targetVariants.indexOf(sanitizedValue) !== -1) {
|
||||
valueMatched = true;
|
||||
}
|
||||
} else if (SimplePhoneMatcher.generateVariants(sanitizedValue).
|
||||
indexOf(sanitizedTarget) !== -1) {
|
||||
valueMatched = true;
|
||||
}
|
||||
|
||||
if (valueMatched) {
|
||||
var matchings, matchingObj;
|
||||
if (!finalMatchings[aMatching.id]) {
|
||||
matchingObj = {
|
||||
matchings: {},
|
||||
matchingContact: aMatching
|
||||
};
|
||||
finalMatchings[aMatching.id] = matchingObj;
|
||||
matchingObj.matchings[filterBy[0]] = [];
|
||||
}
|
||||
else {
|
||||
matchingObj = finalMatchings[aMatching.id];
|
||||
}
|
||||
matchings = matchingObj.matchings[filterBy[0]];
|
||||
|
||||
// Avoinding to report multiple matchings due to variants
|
||||
var sameValueMatchings = matchings.filter(function(matching) {
|
||||
return (matching.matchedValue === value);
|
||||
});
|
||||
if (sameValueMatchings.length === 0) {
|
||||
matchings.push({
|
||||
'target': target,
|
||||
'matchedValue': value
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}); // matchings.forEach
|
||||
|
||||
carryOn();
|
||||
}; // onsuccess
|
||||
|
||||
req.onerror = function(e) {
|
||||
window.console.error('Error while trying to do the matching',
|
||||
e.target.error.name);
|
||||
notifyMismatch(self);
|
||||
};
|
||||
}
|
||||
|
||||
function carryOn() {
|
||||
next++;
|
||||
if (next < targets.length) {
|
||||
doMatchBy(targets[next], callbacks);
|
||||
}
|
||||
else if (Object.keys(finalMatchings).length > 0) {
|
||||
notifyMatch(self, finalMatchings);
|
||||
}
|
||||
else {
|
||||
notifyMismatch(self);
|
||||
}
|
||||
}
|
||||
|
||||
function matched(contacts) {
|
||||
carryOn();
|
||||
}
|
||||
|
||||
var callbacks = {
|
||||
onmatch: matched,
|
||||
onmismatch: carryOn
|
||||
};
|
||||
|
||||
this.start = function() {
|
||||
doMatchBy(targets[0], callbacks);
|
||||
};
|
||||
}
|
||||
|
||||
function notifyMatch(obj, matchings) {
|
||||
typeof obj.onmatch === 'function' && obj.onmatch(matchings);
|
||||
}
|
||||
|
||||
function notifyMismatch(obj) {
|
||||
typeof obj.onmismatch === 'function' && obj.onmismatch();
|
||||
}
|
||||
|
||||
// Match a Contact by the field and filter specified as parameters
|
||||
// Callbacks is an object that declares onmatch and onmismatch callbacks
|
||||
function matchBy(aContact, field, filterOper, callbacks, poptions) {
|
||||
var options = poptions || {};
|
||||
var values = [];
|
||||
|
||||
if (Array.isArray(aContact[field])) {
|
||||
aContact[field].forEach(function(aField) {
|
||||
if (field === 'tel') {
|
||||
// Bug 924378 Contacts API does not properly match phone numbers
|
||||
// and their variants
|
||||
var variants = SimplePhoneMatcher.generateVariants(aField.value);
|
||||
variants.forEach(function(aVariant) {
|
||||
values.push(aVariant);
|
||||
});
|
||||
}
|
||||
else if (typeof aField.value === 'string') {
|
||||
values.push(aField.value.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (values.length > 0) {
|
||||
var matcher = new MultipleMatcher(values, {
|
||||
filterBy: [field],
|
||||
filterOp: filterOper,
|
||||
selfContactId: aContact.id,
|
||||
linkParams: options
|
||||
});
|
||||
matcher.onmatch = callbacks.onmatch;
|
||||
|
||||
matcher.onmismatch = callbacks.onmismatch;
|
||||
|
||||
matcher.start();
|
||||
}
|
||||
else {
|
||||
notifyMismatch(callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
function matchByTel(aContact, callbacks, options) {
|
||||
matchBy(aContact, 'tel', 'match', callbacks, options);
|
||||
}
|
||||
|
||||
function matchByEmail(aContact, callbacks, options) {
|
||||
matchBy(aContact, 'email', 'startsWith', callbacks, options);
|
||||
}
|
||||
|
||||
// Performs a matching for an incoming contact 'aContact' and the mode
|
||||
// ['active', passive'] specified as parameter. Callbacks is an object
|
||||
// with the onmatch and onmismatch functions that will be called after
|
||||
// the matching process has finished
|
||||
function doMatch(aContact, mode, callbacks) {
|
||||
if (mode === 'passive') {
|
||||
doMatchPassive(aContact, callbacks);
|
||||
}
|
||||
else if (mode === 'active') {
|
||||
doMatchActive(aContact, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
// Implements the active mode 'matching'
|
||||
function doMatchTelAndEmail(aContact, callbacks, options) {
|
||||
var localCbs = {
|
||||
onmatch: function(telMatches) {
|
||||
var matchCbs = {
|
||||
onmatch: function(mailMatches) {
|
||||
// Have a unique set of matches
|
||||
var allMatches = telMatches;
|
||||
Object.keys(mailMatches).forEach(function(aMatch) {
|
||||
if (!allMatches[aMatch]) {
|
||||
allMatches[aMatch] = mailMatches[aMatch];
|
||||
}
|
||||
else {
|
||||
allMatches[aMatch].matchings.email =
|
||||
mailMatches[aMatch].matchings.email;
|
||||
}
|
||||
});
|
||||
notifyMatch(callbacks, allMatches);
|
||||
},
|
||||
onmismatch: function() {
|
||||
notifyMatch(callbacks, telMatches);
|
||||
}
|
||||
};
|
||||
matchByEmail(aContact, matchCbs, options);
|
||||
},
|
||||
onmismatch: function() {
|
||||
matchByEmail(aContact, callbacks, options);
|
||||
}
|
||||
};
|
||||
matchByTel(aContact, localCbs, options);
|
||||
}
|
||||
|
||||
function getSourceSimUrl(aContact) {
|
||||
var out;
|
||||
|
||||
if (Array.isArray(aContact.category) &&
|
||||
aContact.category.indexOf('sim') !== -1 &&
|
||||
Array.isArray(aContact.url)) {
|
||||
for (var j = 0; j < aContact.url.length; j++) {
|
||||
var aUrl = aContact.url[j];
|
||||
if (aUrl.type.indexOf('source') !== -1 &&
|
||||
aUrl.type.indexOf('sim') !== -1) {
|
||||
out = aUrl.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Implements the silent mode matching
|
||||
function doMatchPassive(aContact, callbacks) {
|
||||
if (!hasName(aContact)) {
|
||||
notifyMismatch(callbacks);
|
||||
return;
|
||||
}
|
||||
|
||||
// SIM Contacts are detected and matched accordingly
|
||||
// We try to match by name and then try to see if they correspond to
|
||||
// the same SIM contact. In that case matching is reported
|
||||
var simUrl = getSourceSimUrl(aContact);
|
||||
if (simUrl) {
|
||||
performPassiveMatchForSimContact(simUrl, aContact, callbacks);
|
||||
}
|
||||
else {
|
||||
performPassiveMatch(aContact, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
function performPassiveMatchForSimContact(simUrl, aContact, callbacks) {
|
||||
var localCbs = {
|
||||
onmatch: function(results) {
|
||||
var matchingFound = false;
|
||||
|
||||
Object.keys(results).forEach(function(aResultId) {
|
||||
var matchingContact = results[aResultId].matchingContact;
|
||||
var matchingUrl = getSourceSimUrl(matchingContact);
|
||||
if (matchingUrl && matchingUrl === simUrl) {
|
||||
matchingFound = true;
|
||||
var matchings = {};
|
||||
matchings[aResultId] = {
|
||||
matchingContact: matchingContact
|
||||
};
|
||||
callbacks.onmatch(matchings);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (!matchingFound) {
|
||||
performPassiveMatch(aContact, callbacks);
|
||||
}
|
||||
},
|
||||
onmismatch: function() {
|
||||
performPassiveMatch(aContact, callbacks);
|
||||
}
|
||||
};
|
||||
// Matching by name and then see if they have the same SIM contact source
|
||||
matchByName(aContact, localCbs);
|
||||
}
|
||||
|
||||
// Implements the silent mode matching
|
||||
function performPassiveMatch(aContact, callbacks) {
|
||||
var matchingsFound = {};
|
||||
|
||||
var localCbs = {
|
||||
onmatch: function(results) {
|
||||
// Results will contain contacts that match by tel or email
|
||||
// Normalizing the strings
|
||||
var names = [];
|
||||
Object.keys(results).forEach(function(aResultId) {
|
||||
var mContact = results[aResultId].matchingContact;
|
||||
|
||||
if (!hasName(mContact)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// As the number of candidates here will be short a normal search
|
||||
// will be conducted
|
||||
var targetFN = null;
|
||||
if (!isEmptyStr(aContact.familyName)) {
|
||||
targetFN = Normalizer.toAscii(
|
||||
aContact.familyName[0].trim().toLowerCase()).
|
||||
replace(blankRegExp, '');
|
||||
}
|
||||
var targetGN = null;
|
||||
if (!isEmptyStr(aContact.givenName)) {
|
||||
targetGN = Normalizer.toAscii(
|
||||
aContact.givenName[0].trim().toLowerCase()).
|
||||
replace(blankRegExp, '');
|
||||
}
|
||||
// To support seamless matching of SIM contacts
|
||||
var targetName = (targetGN || '') + (targetFN || '');
|
||||
|
||||
var mFamilyName = null;
|
||||
var mGivenName = null;
|
||||
|
||||
if (!isEmptyStr(mContact.familyName)) {
|
||||
mFamilyName = Normalizer.toAscii(
|
||||
mContact.familyName[0].trim().toLowerCase()
|
||||
).replace(blankRegExp, '');
|
||||
}
|
||||
|
||||
if (!isEmptyStr(mContact.givenName)) {
|
||||
mGivenName = Normalizer.toAscii(
|
||||
mContact.givenName[0].trim().toLowerCase()
|
||||
).replace(blankRegExp, '');
|
||||
}
|
||||
// To support seamless matching of SIM contacts
|
||||
var mName = (mGivenName || '') + (mFamilyName || '');
|
||||
|
||||
names.push({
|
||||
contact: mContact,
|
||||
familyName: mFamilyName,
|
||||
givenName: mGivenName,
|
||||
name: mName
|
||||
});
|
||||
|
||||
var matchingList = names.filter(function(obj) {
|
||||
return ((obj.familyName === targetFN &&
|
||||
obj.givenName === targetGN) ||
|
||||
(obj.name && obj.name === targetName) && (
|
||||
!Array.isArray(obj.contact.category) ||
|
||||
obj.contact.category.indexOf(FB_CATEGORY) === -1));
|
||||
});
|
||||
|
||||
matchingList.forEach(function(aMatching) {
|
||||
var contact = aMatching.contact;
|
||||
matchingsFound[contact.id] = {
|
||||
matchingContact: contact
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
reconcileResults(aContact, matchingsFound, results, callbacks);
|
||||
},
|
||||
|
||||
onmismatch: function() {
|
||||
notifyMismatch(callbacks);
|
||||
}
|
||||
};
|
||||
|
||||
// Matching by email and phone number, then match by names
|
||||
doMatchTelAndEmail(aContact, localCbs);
|
||||
}
|
||||
|
||||
function doMatchActive(aContact, callbacks) {
|
||||
var options = {
|
||||
linkedMatched: {},
|
||||
linkedTo: getLinkedTo(aContact)
|
||||
};
|
||||
|
||||
var localCbs = {
|
||||
onmatch: function(results) {
|
||||
var cbsName = {
|
||||
onmatch: function(nameResults) {
|
||||
Object.keys(nameResults).forEach(function(aId) {
|
||||
if (!results[aId]) {
|
||||
results[aId] = nameResults[aId];
|
||||
}
|
||||
else {
|
||||
results[aId].matchings.name =
|
||||
nameResults[aId].matchings.name;
|
||||
}
|
||||
});
|
||||
|
||||
notifyMatch(callbacks, results);
|
||||
},
|
||||
onmismatch: function() {
|
||||
notifyMatch(callbacks, results);
|
||||
}
|
||||
};
|
||||
|
||||
matchByName(aContact, cbsName, options);
|
||||
},
|
||||
onmismatch: function() {
|
||||
matchByName(aContact, callbacks, options);
|
||||
}
|
||||
};
|
||||
|
||||
// Matching by email and phone number, then match by names
|
||||
doMatchTelAndEmail(aContact, localCbs, options);
|
||||
}
|
||||
|
||||
function notifyFindNameReady() {
|
||||
document.dispatchEvent(new CustomEvent('by_name_ready'));
|
||||
}
|
||||
|
||||
// Used for the active mode. Performs matching by familyName and givenName
|
||||
function matchByName(aContact, callbacks, options) {
|
||||
// First we try to find by familyName
|
||||
// Afterwards we search by givenName
|
||||
var isSimContact = (Array.isArray(aContact.category) &&
|
||||
aContact.category.indexOf('sim') !== -1);
|
||||
|
||||
if ((isEmptyStr(aContact.familyName) || isEmptyStr(aContact.givenName)) &&
|
||||
!isSimContact) {
|
||||
notifyMismatch(callbacks);
|
||||
return;
|
||||
}
|
||||
|
||||
var finalResult = {};
|
||||
|
||||
var resultsByName = [];
|
||||
if (!isEmptyStr(aContact.name)) {
|
||||
var targetName = aContact.name[0].trim();
|
||||
// Filter by familyName using startsWith. Gecko 'startsWith' operation
|
||||
// acts as 'equal' but does not match case.
|
||||
var reqName = dataProvider.find({
|
||||
filterValue: targetName,
|
||||
filterBy: ['name'],
|
||||
filterOp: 'startsWith'
|
||||
});
|
||||
|
||||
reqName.onsuccess = function() {
|
||||
resultsByName = reqName.result.filter(function(aResult) {
|
||||
return filterFacebook(aResult, options);
|
||||
});
|
||||
notifyFindNameReady();
|
||||
};
|
||||
|
||||
reqName.onerror = function(e) {
|
||||
window.console.warn('Error while trying to find by name: ',
|
||||
e.target.error.name);
|
||||
notifyFindNameReady();
|
||||
};
|
||||
}
|
||||
else {
|
||||
notifyFindNameReady();
|
||||
}
|
||||
|
||||
if (isEmptyStr(aContact.familyName)) {
|
||||
endOfMatchByName(finalResult, aContact, resultsByName, callbacks);
|
||||
}
|
||||
else {
|
||||
var targetFamilyName = aContact.familyName[0].trim();
|
||||
// Filter by familyName using startsWith. Gecko 'startsWith' operation
|
||||
// acts as 'equal' but does not match case.
|
||||
var reqFamilyName = dataProvider.find({
|
||||
filterValue: targetFamilyName,
|
||||
filterBy: ['familyName'],
|
||||
filterOp: 'startsWith'
|
||||
});
|
||||
|
||||
reqFamilyName.onsuccess = function() {
|
||||
var results = reqFamilyName.result;
|
||||
|
||||
var givenNames = [];
|
||||
var targetGN = null;
|
||||
|
||||
if (!isEmptyStr(aContact.givenName)) {
|
||||
targetGN = Normalizer.toAscii(
|
||||
aContact.givenName[0].trim().toLowerCase()).
|
||||
replace(blankRegExp, '');
|
||||
}
|
||||
|
||||
results.forEach(function(mContact) {
|
||||
if (mContact.id === aContact.id || isEmptyStr(mContact.givenName)) {
|
||||
return;
|
||||
}
|
||||
givenNames.push({
|
||||
contact: mContact,
|
||||
givenName: Normalizer.toAscii(
|
||||
mContact.givenName[0].trim().toLowerCase()).
|
||||
replace(blankRegExp, '')
|
||||
});
|
||||
});
|
||||
|
||||
var finalMatchings = givenNames.filter(function(obj) {
|
||||
var gn = obj.givenName;
|
||||
return ((gn === targetGN || targetGN.startsWith(gn) ||
|
||||
gn.startsWith(targetGN)) &&
|
||||
filterFacebook(obj.contact, options));
|
||||
});
|
||||
|
||||
finalMatchings.forEach(function(aMatching) {
|
||||
finalResult[aMatching.contact.id] = {
|
||||
matchings: {
|
||||
'name': [{
|
||||
target: targetFamilyName,
|
||||
matchedValue: Array.isArray(aMatching.contact.name) ?
|
||||
aMatching.contact.name[0] :
|
||||
getCompleteName(aMatching.contact)
|
||||
}]
|
||||
},
|
||||
matchingContact: aMatching.contact
|
||||
};
|
||||
});
|
||||
|
||||
if (resultsByName) {
|
||||
endOfMatchByName(finalResult, aContact, resultsByName, callbacks);
|
||||
}
|
||||
else {
|
||||
document.addEventListener('by_name_ready', function nameReady() {
|
||||
document.removeEventListener('by_name_ready', nameReady);
|
||||
endOfMatchByName(finalResult, aContact, resultsByName, callbacks);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reqFamilyName.onerror = function(e) {
|
||||
window.console.error('Error while trying to find by familyName: ',
|
||||
e.target.error.name);
|
||||
notifyMismatch(callbacks);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getLinkedTo(contact) {
|
||||
var out = null;
|
||||
|
||||
if (Array.isArray(contact.category)) {
|
||||
var idx = contact.category.indexOf(FB_LINKED);
|
||||
if (idx !== -1) {
|
||||
out = contact.category[idx + 1];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isFbLinked(contact) {
|
||||
return (Array.isArray(contact.category) &&
|
||||
contact.category.indexOf(FB_LINKED) !== -1);
|
||||
}
|
||||
|
||||
function isFbContact(contact) {
|
||||
return (Array.isArray(contact.category) &&
|
||||
contact.category.indexOf(FB_CATEGORY) !== -1);
|
||||
}
|
||||
|
||||
function filterFacebook(contact, linkParams) {
|
||||
var out = false;
|
||||
|
||||
if (!isFbContact(contact)) {
|
||||
out = true;
|
||||
}
|
||||
else if (isFbLinked(contact)) {
|
||||
var linkedTo = getLinkedTo(contact);
|
||||
var targetUid = linkParams.linkedTo || '';
|
||||
var linkedMatched = linkParams.linkedMatched || {};
|
||||
|
||||
if (targetUid === linkedTo) {
|
||||
out = true;
|
||||
}
|
||||
// It is only allowed to match one linked contact or various but all of
|
||||
// them linked to the same FB friend
|
||||
else if ((Object.keys(linkedMatched).length === 0 ||
|
||||
linkedMatched[linkedTo]) && !targetUid) {
|
||||
linkedMatched[linkedTo] = linkedTo;
|
||||
out = true;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function endOfMatchByName(finalResult, aContact, resultsByName, callbacks) {
|
||||
resultsByName.forEach(function(aResult) {
|
||||
if (aResult.id === aContact.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var matchingObj = {
|
||||
matchings: {
|
||||
'name': [{
|
||||
target: (Array.isArray(aContact.name) &&
|
||||
(typeof aContact.name[0] === 'string')) ?
|
||||
aContact.name[0].trim() : getCompleteName(aContact),
|
||||
matchedValue: Array.isArray(aResult.name) ?
|
||||
aResult.name[0] :
|
||||
getCompleteName(aResult)
|
||||
}]
|
||||
},
|
||||
matchingContact: aResult
|
||||
};
|
||||
|
||||
if (!finalResult[aResult.id]) {
|
||||
finalResult[aResult.id] = matchingObj;
|
||||
}
|
||||
else {
|
||||
finalResult[aResult.id].matchings.name =
|
||||
matchingObj.matchings.name;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(finalResult).length > 0) {
|
||||
notifyMatch(callbacks, finalResult);
|
||||
}
|
||||
else {
|
||||
notifyMismatch(callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
function getCompleteName(contact) {
|
||||
var givenName = Array.isArray(contact.givenName) ?
|
||||
contact.givenName[0] : '';
|
||||
|
||||
var familyName = Array.isArray(contact.familyName) ?
|
||||
contact.familyName[0] : '';
|
||||
|
||||
var completeName = givenName && familyName ?
|
||||
givenName + ' ' + familyName :
|
||||
givenName || familyName;
|
||||
|
||||
return completeName;
|
||||
}
|
||||
|
||||
function isEmpty(collection) {
|
||||
return ((!Array.isArray(collection) ||
|
||||
!collection[0]) ||
|
||||
(collection[0] &&
|
||||
collection[0].value &&
|
||||
!collection[0].value.trim()));
|
||||
}
|
||||
|
||||
function isEmptyStr(collection) {
|
||||
return (!Array.isArray(collection) ||
|
||||
(typeof collection[0] !== 'string') || !(collection[0].trim()));
|
||||
}
|
||||
|
||||
function hasName(aContact) {
|
||||
return (!isEmptyStr(aContact.givenName) ||
|
||||
!isEmptyStr(aContact.familyName));
|
||||
}
|
||||
|
||||
function reconcileResults(incomingContact, nameMatches, phoneMailMatches,
|
||||
callbacks) {
|
||||
var finalMatchings = {};
|
||||
|
||||
// Name matches drive all the process
|
||||
Object.keys(nameMatches).forEach(function(aNameMatching) {
|
||||
var matchingContact = nameMatches[aNameMatching].matchingContact;
|
||||
|
||||
var isPhoneMatching = Array.isArray(phoneMailMatches[aNameMatching].
|
||||
matchings.tel);
|
||||
var isMailMatching = Array.isArray(phoneMailMatches[aNameMatching].
|
||||
matchings.email);
|
||||
|
||||
// Three cases under which a matching is considered
|
||||
if (isPhoneMatching && isMailMatching) {
|
||||
finalMatchings[aNameMatching] = phoneMailMatches[aNameMatching];
|
||||
}
|
||||
else if (isPhoneMatching && (isEmpty(incomingContact.email) ||
|
||||
isEmpty(matchingContact.email))) {
|
||||
finalMatchings[aNameMatching] = phoneMailMatches[aNameMatching];
|
||||
}
|
||||
else if (isMailMatching && (isEmpty(incomingContact.tel) ||
|
||||
isEmpty(matchingContact.tel))) {
|
||||
finalMatchings[aNameMatching] = phoneMailMatches[aNameMatching];
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(finalMatchings).length > 0) {
|
||||
notifyMatch(callbacks, finalMatchings);
|
||||
}
|
||||
else {
|
||||
notifyMismatch(callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
// Exported method
|
||||
this.match = doMatch;
|
||||
|
||||
Object.defineProperty(this, 'dataProvider', {
|
||||
set: function(theProvider) {
|
||||
dataProvider = theProvider;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.Matcher = new MatcherObj(navigator.mozContacts);
|
||||
}(window));
|
||||
378
gaia/shared/js/contacts/contacts_merger.js
Executable file
378
gaia/shared/js/contacts/contacts_merger.js
Executable file
|
|
@ -0,0 +1,378 @@
|
|||
/* globals Promise, SimplePhoneMatcher, utils, ContactPhotoHelper */
|
||||
|
||||
'use strict';
|
||||
|
||||
var contacts = window.contacts || {};
|
||||
|
||||
contacts.Merger = (function() {
|
||||
var DEFAULT_ADR_TYPE = 'home';
|
||||
var DEFAULT_TEL_TYPE = 'other';
|
||||
var DEFAULT_EMAIL_TYPE = 'other';
|
||||
|
||||
// Performs the merge passing the master contact and matching contacts
|
||||
// The master contact is the one that contains the info with more priority
|
||||
// The matchingContacts are the contacts which information will merged with
|
||||
// the master. It is an Array in which the elements should be ordered
|
||||
// by priority i.e. the data from the first elements might take precedence
|
||||
// over the data of the last elements according to the merging rules.
|
||||
//
|
||||
// Each element in the array is an object with the following keys:
|
||||
// * matchingContact: the Contact object matching one or more targets.
|
||||
// * matchings: optional, an object which entries are arrays of
|
||||
// field-matching objects indexed by the field name.
|
||||
//
|
||||
// Each field-matching object has two fields:
|
||||
// * target: the value that field matched with.
|
||||
// * matchedValue: the value from the matchingContact field matching the
|
||||
// target.
|
||||
//
|
||||
// Here is an example:
|
||||
// {
|
||||
// matchingContact: { ... },
|
||||
// matchings: {
|
||||
// 'tel': [{
|
||||
// target: '600000000',
|
||||
// matchedValue: '+34600000000'
|
||||
// }]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The `matchings` field can be not present if the matching contact comes
|
||||
// from an external source not related with the matching algorithm.
|
||||
function doMerge(pmasterContact, pmatchingContacts, callbacks) {
|
||||
window.setTimeout(function contactsMerge() {
|
||||
mergeAll(pmasterContact, pmatchingContacts, filled, callbacks);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function doInMemoryMerge(pmasterContact, pmatchingContacts) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
mergeAll(pmasterContact, pmatchingContacts, inMemoryMergeDone, {
|
||||
success: function(masterContact) {
|
||||
resolve(masterContact);
|
||||
},
|
||||
error: function(err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function inMemoryMergeDone(callbacks, matchingContacts, masterContact) {
|
||||
callbacks.success(masterContact);
|
||||
}
|
||||
|
||||
function isSimContact(contact) {
|
||||
return Array.isArray(contact.category) &&
|
||||
contact.category.indexOf('sim') !== -1;
|
||||
}
|
||||
|
||||
function mergeAll(masterContact, matchingContacts, onFilled, callbacks) {
|
||||
var emailsHash;
|
||||
var categoriesHash;
|
||||
var telsHash;
|
||||
var mergedContact = {};
|
||||
var mergedPhoto;
|
||||
|
||||
mergedContact.givenName = [];
|
||||
copyStringArray(masterContact.givenName, mergedContact.givenName);
|
||||
|
||||
mergedContact.familyName = [];
|
||||
copyStringArray(masterContact.familyName, mergedContact.familyName);
|
||||
|
||||
mergedContact.photo = masterContact.photo || [];
|
||||
mergedPhoto = mergedContact.photo;
|
||||
|
||||
mergedContact.bday = masterContact.bday;
|
||||
mergedContact.anniversary = masterContact.anniversary;
|
||||
|
||||
mergedContact.adr = masterContact.adr || [];
|
||||
|
||||
telsHash = {};
|
||||
mergedContact.tel = [];
|
||||
if (Array.isArray(masterContact.tel)) {
|
||||
masterContact.tel.forEach(function(aTel) {
|
||||
if (!telsHash[aTel.value]) {
|
||||
aTel.type = (Array.isArray(aTel.type) ? aTel.type : [aTel.type]);
|
||||
aTel.type[0] = aTel.type[0] || DEFAULT_TEL_TYPE;
|
||||
mergedContact.tel.push(aTel);
|
||||
}
|
||||
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
|
||||
variants.forEach(function(aVariant) {
|
||||
telsHash[aVariant] = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
mergedContact.email = [];
|
||||
emailsHash = {};
|
||||
populateEmails(masterContact.email, emailsHash, mergedContact.email);
|
||||
|
||||
mergedContact.org = masterContact.org || [];
|
||||
|
||||
mergedContact.category = [];
|
||||
categoriesHash = {};
|
||||
populateNoDuplicates(masterContact.category, categoriesHash,
|
||||
mergedContact.category);
|
||||
|
||||
mergedContact.url = masterContact.url || [];
|
||||
mergedContact.note = masterContact.note || [];
|
||||
|
||||
matchingContacts.forEach(function(aResult) {
|
||||
var theMatchingContact = aResult.matchingContact;
|
||||
|
||||
var givenName = theMatchingContact.givenName;
|
||||
if (Array.isArray(givenName) && givenName[0]) {
|
||||
if (mergedContact.givenName.indexOf(givenName[0]) === -1) {
|
||||
if (mergedContact.givenName[0] &&
|
||||
mergedContact.givenName[0].trim()) {
|
||||
mergedContact.givenName.push(givenName[0]);
|
||||
}
|
||||
else {
|
||||
mergedContact.givenName[0] = givenName[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var familyName = theMatchingContact.familyName;
|
||||
if (Array.isArray(familyName) && familyName[0]) {
|
||||
if (mergedContact.familyName.indexOf(familyName[0]) === -1) {
|
||||
if (mergedContact.familyName[0] &&
|
||||
mergedContact.familyName[0].trim()) {
|
||||
mergedContact.familyName.push(familyName[0]);
|
||||
}
|
||||
else {
|
||||
mergedContact.familyName[0] = familyName[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avoiding duplicating givenName - familyName for SIM Contacts
|
||||
if ((!isDefined(masterContact.givenName) ||
|
||||
!isDefined(masterContact.familyName)) &&
|
||||
isSimContact(theMatchingContact)) {
|
||||
|
||||
if (isDefined(mergedContact.givenName) &&
|
||||
isDefined(mergedContact.familyName) &&
|
||||
mergedContact.familyName[0] === mergedContact.givenName[0]) {
|
||||
|
||||
if (!isDefined(masterContact.givenName)) {
|
||||
mergedContact.givenName = [];
|
||||
mergedContact.name = mergedContact.familyName;
|
||||
}
|
||||
if (!isDefined(masterContact.familyName)) {
|
||||
mergedContact.familyName = [];
|
||||
mergedContact.name = mergedContact.givenName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mergedContact.bday && theMatchingContact.bday) {
|
||||
mergedContact.bday = theMatchingContact.bday;
|
||||
}
|
||||
if (!mergedContact.anniversary && theMatchingContact.anniversary) {
|
||||
mergedContact.anniversary = theMatchingContact.anniversary;
|
||||
}
|
||||
|
||||
if (isDefined(theMatchingContact.org) && mergedContact.org.length === 0) {
|
||||
mergedContact.org = theMatchingContact.org;
|
||||
}
|
||||
if (isDefined(theMatchingContact.category)) {
|
||||
populateNoDuplicates(theMatchingContact.category, categoriesHash,
|
||||
mergedContact.category);
|
||||
}
|
||||
|
||||
populateEmails(theMatchingContact.email, emailsHash, mergedContact.email);
|
||||
|
||||
if (Array.isArray(theMatchingContact.tel)) {
|
||||
var theMatchings = aResult.matchings || {};
|
||||
var telMatchings = theMatchings.tel;
|
||||
theMatchingContact.tel.forEach(function(aTel) {
|
||||
var theValue = aTel.value;
|
||||
var target = theValue, matchedValue = '';
|
||||
if (telMatchings) {
|
||||
var matchedFrom = telMatchings.filter(function(x) {
|
||||
return (x.target === theValue || x.matchedValue === theValue);
|
||||
});
|
||||
if (matchedFrom[0]) {
|
||||
target = matchedFrom[0].target;
|
||||
matchedValue = matchedFrom[0].matchedValue;
|
||||
}
|
||||
}
|
||||
var variants = SimplePhoneMatcher.generateVariants(theValue);
|
||||
var alreadyPresent = false;
|
||||
for (var j = 0; j < variants.length; j++) {
|
||||
if (telsHash[variants[j]]) {
|
||||
alreadyPresent = true;
|
||||
}
|
||||
}
|
||||
if (!alreadyPresent) {
|
||||
theValue = target.length > matchedValue.length ?
|
||||
target : matchedValue;
|
||||
mergedContact.tel.push({
|
||||
type: aTel.type || [DEFAULT_TEL_TYPE],
|
||||
value: theValue,
|
||||
carrier: aTel.carrier,
|
||||
pref: aTel.pref
|
||||
});
|
||||
variants.forEach(function(aVariant) {
|
||||
telsHash[aVariant] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(mergedPhoto) || mergedPhoto.length === 0 &&
|
||||
isDefined(theMatchingContact.photo)) {
|
||||
var photo = ContactPhotoHelper.getFullResolution(theMatchingContact);
|
||||
mergedPhoto = [photo];
|
||||
}
|
||||
|
||||
populateField(
|
||||
theMatchingContact.adr,
|
||||
mergedContact.adr,
|
||||
DEFAULT_ADR_TYPE
|
||||
);
|
||||
|
||||
populateField(theMatchingContact.url, mergedContact.url);
|
||||
populateField(theMatchingContact.note, mergedContact.note);
|
||||
|
||||
}); // matchingResults
|
||||
|
||||
mergedContact.name = [((mergedContact.givenName[0] ?
|
||||
mergedContact.givenName[0] : '') + ' ' +
|
||||
(mergedContact.familyName[0] ?
|
||||
mergedContact.familyName[0] : '')).trim()];
|
||||
|
||||
fillMasterContact(masterContact, mergedContact, mergedPhoto,
|
||||
onFilled.bind(null, callbacks, matchingContacts));
|
||||
}
|
||||
|
||||
function filled(callbacks, matchingContacts, masterContact) {
|
||||
// Updating the master contact
|
||||
var req = navigator.mozContacts.save(
|
||||
utils.misc.toMozContact(masterContact));
|
||||
|
||||
req.onsuccess = function() {
|
||||
// Now for all the matchingContacts they have to be removed
|
||||
matchingContacts.forEach(function(aMatchingContact) {
|
||||
// Only remove those contacts which are already in the DB
|
||||
if (aMatchingContact.matchingContact.id) {
|
||||
var contact = aMatchingContact.matchingContact;
|
||||
navigator.mozContacts.remove(utils.misc.toMozContact(contact));
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof callbacks.success === 'function') {
|
||||
callbacks.success(masterContact);
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('Error while saving merged Contact: ',
|
||||
req.error.name);
|
||||
typeof callbacks.error === 'function' && callbacks.error(req.error);
|
||||
};
|
||||
}
|
||||
|
||||
function isDefined(field) {
|
||||
return (Array.isArray(field) && field[0] &&
|
||||
((typeof field[0] === 'string' && field[0].trim().length > 0) ||
|
||||
typeof field[0] === 'object'));
|
||||
}
|
||||
|
||||
function copyStringArray(source, dest) {
|
||||
if (Array.isArray(source)) {
|
||||
source.forEach(function(aVal) {
|
||||
if (aVal && aVal.trim()) {
|
||||
dest.push(aVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function populateEmails(sourceEmails, hash, out) {
|
||||
if (Array.isArray(sourceEmails)) {
|
||||
sourceEmails.forEach(function(aEmail) {
|
||||
aEmail.type = Array.isArray(aEmail.type) ? aEmail.type : [aEmail.type];
|
||||
aEmail.type[0] = aEmail.type[0] || DEFAULT_EMAIL_TYPE;
|
||||
var value = aEmail.value;
|
||||
if (value && value.trim()) {
|
||||
value = value.toLowerCase();
|
||||
if (!hash[value]) {
|
||||
aEmail.value = value;
|
||||
out.push(aEmail);
|
||||
hash[value] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function populateNoDuplicates(source, hash, out) {
|
||||
if (Array.isArray(source)) {
|
||||
source.forEach(function(aCat) {
|
||||
if (!hash[aCat]) {
|
||||
out.push(aCat);
|
||||
hash[aCat] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function populateField(source, destination, defaultType) {
|
||||
if (Array.isArray(source)) {
|
||||
// The easiest way to compare two objects is to compare their's
|
||||
// stringified JSON representations as strings. So we create
|
||||
// temporary Array with JSONs of the objects to compare.
|
||||
var stringifiedDestination = destination.map(function(element){
|
||||
return JSON.stringify(element);
|
||||
});
|
||||
|
||||
source.forEach(function(as, index) {
|
||||
// If the source value and destination value is the same we
|
||||
// don't want to merge and will leave contact as it is. This
|
||||
// prevents duplication of the data like in Bug 935636
|
||||
if (stringifiedDestination.indexOf(JSON.stringify(as)) === -1) {
|
||||
if (defaultType && (!as.type || !as.type[0])) {
|
||||
as.type = [defaultType];
|
||||
}
|
||||
destination.push(as);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fillMasterContact(masterContact, mergedContact, mergedPhotos, done) {
|
||||
var fields = ['familyName', 'givenName', 'name', 'org', 'email', 'tel',
|
||||
'bday', 'anniversary', 'adr', 'category',
|
||||
'url', 'note', 'photo'];
|
||||
|
||||
fields.forEach(function(aField) {
|
||||
masterContact[aField] = mergedContact[aField];
|
||||
});
|
||||
|
||||
if (!Array.isArray(mergedPhotos) || !mergedPhotos[0] ||
|
||||
mergedPhotos.length >= 2) {
|
||||
done(masterContact);
|
||||
return;
|
||||
}
|
||||
|
||||
var mergedPhoto = mergedPhotos[0];
|
||||
|
||||
utils.thumbnailImage(mergedPhoto, function gotTumbnail(thumbnail) {
|
||||
if (mergedPhoto !== thumbnail) {
|
||||
masterContact.photo = [mergedPhoto, thumbnail];
|
||||
} else {
|
||||
masterContact.photo = [mergedPhoto];
|
||||
}
|
||||
done(masterContact);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
merge: doMerge,
|
||||
inMemoryMerge: doInMemoryMerge
|
||||
};
|
||||
|
||||
})();
|
||||
231
gaia/shared/js/contacts/contacts_shortcuts.js
Executable file
231
gaia/shared/js/contacts/contacts_shortcuts.js
Executable file
|
|
@ -0,0 +1,231 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.alphaScroll) {
|
||||
(function initScrollbar(doc) {
|
||||
var alphaScroll = utils.alphaScroll = {};
|
||||
|
||||
var scrollToCallback, jumper, overlay,
|
||||
overlayStyle, groupSelector, liSearch, liFavorites;
|
||||
|
||||
var isScrolling = false;
|
||||
var alreadyRendered = false;
|
||||
var isDesktop = false;
|
||||
|
||||
// Callback invoked when scrolling is neded
|
||||
var P_SCROLLTO_CB = 'scrollToCb';
|
||||
// Element that represents the alpha scroll bar
|
||||
var P_JUMPER = 'jumper';
|
||||
// Element that shows the current letter
|
||||
var P_OVERLAY = 'overlay';
|
||||
// Selector that will allow to get the group that should be scrolled to
|
||||
// Group will be identified by this selector plus the corresponding letter
|
||||
var P_GROUP_SELECTOR = 'groupSelector';
|
||||
|
||||
var TRANSITION_DELAY = '0.3s';
|
||||
var TRANSITION_DURATION = '0.2s';
|
||||
|
||||
var RESET_TRANSITION = '0s';
|
||||
|
||||
var offset = 0, lastY = 0;
|
||||
|
||||
var isTouch = 'ontouchstart' in window;
|
||||
var touchstart = isTouch ? 'touchstart' : 'mousedown';
|
||||
var touchmove = isTouch ? 'touchmove' : 'mousemove';
|
||||
var touchend = isTouch ? 'touchend' : 'mouseup';
|
||||
|
||||
var getY = (function getYWrapper() {
|
||||
return isTouch ? function(e) { return e.touches[0].pageY; } :
|
||||
function(e) { return e.pageY; };
|
||||
})();
|
||||
|
||||
var getTarget = (function getTargetWrapper() {
|
||||
if (isTouch) {
|
||||
return function(e) {
|
||||
var touch = e.touches[0];
|
||||
return document.elementFromPoint(touch.pageX, touch.pageY);
|
||||
};
|
||||
} else {
|
||||
return function(e) {
|
||||
return e.target;
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
alphaScroll.init = function(params, desktop) {
|
||||
isDesktop = params && params.desktop;
|
||||
if (alreadyRendered && !isDesktop) {
|
||||
return;
|
||||
}
|
||||
scrollToCallback = params[P_SCROLLTO_CB];
|
||||
jumper = params[P_JUMPER];
|
||||
overlay = params[P_OVERLAY];
|
||||
groupSelector = params[P_GROUP_SELECTOR];
|
||||
|
||||
overlay.textContent = '';
|
||||
overlayStyle = overlay.style;
|
||||
|
||||
jumper.addEventListener(touchstart, scrollStart);
|
||||
jumper.addEventListener(touchmove, scrollTo);
|
||||
jumper.addEventListener(touchend, scrollEnd);
|
||||
|
||||
// Determine if the content for this scrollbar is statically defined
|
||||
// in the index.html. If there are no templates, then do not attempt
|
||||
// to render it.
|
||||
if (typeof jumper === 'string') {
|
||||
jumper = document.querySelector(jumper);
|
||||
}
|
||||
var template = jumper.querySelector('*[data-template]');
|
||||
if (!template) {
|
||||
alreadyRendered = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise render the content via templating.
|
||||
var frag = document.createDocumentFragment();
|
||||
var alphabet = [];
|
||||
for (var i = 65; i <= 90; i++) {
|
||||
alphabet.push({ anchor: String.fromCharCode(i) });
|
||||
}
|
||||
alphabet.push({
|
||||
anchor: '#'
|
||||
});
|
||||
utils.templates.append(jumper, alphabet, frag);
|
||||
jumper.appendChild(frag);
|
||||
|
||||
alreadyRendered = true;
|
||||
};
|
||||
|
||||
// Provide a mechanism to hide/show groups at will
|
||||
alphaScroll.showGroup = function(name) {
|
||||
var selector = '[data-anchor="group-'+ name + '"]';
|
||||
var group = jumper.querySelector(selector);
|
||||
if (group) {
|
||||
group.classList.remove('hide');
|
||||
}
|
||||
};
|
||||
|
||||
alphaScroll.hideGroup = function(name) {
|
||||
var selector = '[data-anchor="group-'+ name + '"]';
|
||||
var group = jumper.querySelector(selector);
|
||||
if (group) {
|
||||
group.classList.add('hide');
|
||||
}
|
||||
};
|
||||
|
||||
function hideExtraItems(value) {
|
||||
if (!liSearch) {
|
||||
liSearch = jumper.querySelector('li[data-anchor="search-container"]');
|
||||
}
|
||||
|
||||
if (!liFavorites) {
|
||||
liFavorites = jumper.querySelector('li[data-anchor="group-favorites"]');
|
||||
}
|
||||
|
||||
liSearch.hidden = value;
|
||||
liFavorites.hidden = value;
|
||||
}
|
||||
|
||||
alphaScroll.toggleFormat = function(type) {
|
||||
hideExtraItems(type === 'short');
|
||||
};
|
||||
|
||||
function scrollTo(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (!isScrolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentY = getY(evt);
|
||||
// We set the threshold of updating the shortcut to half of the offset
|
||||
// to avoid when touch already moved to center of the certain letter but
|
||||
// shows another letter.
|
||||
if (Math.abs(lastY - currentY) < offset / 2 && !isDesktop) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastY = currentY;
|
||||
|
||||
var elem = getTarget(evt);
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dataset = elem.dataset;
|
||||
// Render
|
||||
if (dataset.letter) {
|
||||
overlay.textContent = dataset.letter;
|
||||
} else if (dataset.img) {
|
||||
overlay.textContent = '';
|
||||
var img = new Image();
|
||||
img.src = 'style/images/' + dataset.img;
|
||||
overlay.appendChild(img);
|
||||
}
|
||||
|
||||
// The headers are sticky, and can have wrong offsetTop,
|
||||
// so scroll to the top of each section on jump click.
|
||||
var anch = dataset.anchor;
|
||||
var selector = anch === 'group-#' ? 'group-und' : anch;
|
||||
var domTarget = doc.querySelector('#section-' + selector +
|
||||
', #' + selector);
|
||||
if (!domTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToCallback(domTarget, selector.replace('group-', ''));
|
||||
}
|
||||
|
||||
function scrollStart(evt) {
|
||||
var dataset = getTarget(evt).dataset;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
// There is no need to show overlay if the target doesn't contain
|
||||
// any valid data for overlay block.
|
||||
if (!dataset.letter && !dataset.img) {
|
||||
return;
|
||||
}
|
||||
|
||||
offset = offset || jumper.querySelector('[data-anchor]').offsetHeight;
|
||||
overlayStyle.MozTransitionDelay = RESET_TRANSITION;
|
||||
overlayStyle.MozTransitionDuration = RESET_TRANSITION;
|
||||
overlayStyle.opacity = '1';
|
||||
isScrolling = true;
|
||||
|
||||
scrollTo(evt);
|
||||
}
|
||||
|
||||
function scrollEnd(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
var transitionDelay = TRANSITION_DELAY;
|
||||
var transitionDuration = TRANSITION_DURATION;
|
||||
// In the case of the scroll ending when we are at the
|
||||
// bottom or the top, remove the transition inmediately
|
||||
if (overlay.textContent === '#' || overlay.textContent === '') {
|
||||
transitionDelay = RESET_TRANSITION;
|
||||
transitionDuration = RESET_TRANSITION;
|
||||
}
|
||||
overlayStyle.MozTransitionDelay = transitionDelay;
|
||||
overlayStyle.MozTransitionDuration = transitionDuration;
|
||||
overlayStyle.opacity = '0';
|
||||
overlay.textContent = null;
|
||||
isScrolling = false;
|
||||
lastY = 0;
|
||||
}
|
||||
|
||||
// Cache images refered in 'data-img'es
|
||||
(function(doc) {
|
||||
var images = doc.querySelectorAll('li[data-img]');
|
||||
Object.keys(images).forEach(function(value) {
|
||||
var img = new Image();
|
||||
img.src = 'contacts/style/images/' + images[value].dataset.img;
|
||||
});
|
||||
}(doc));
|
||||
|
||||
})(document);
|
||||
}
|
||||
214
gaia/shared/js/contacts/import/contacts_importer.js
Executable file
214
gaia/shared/js/contacts/import/contacts_importer.js
Executable file
|
|
@ -0,0 +1,214 @@
|
|||
'use strict';
|
||||
/* global contacts */
|
||||
/* global utils */
|
||||
/* global Matcher */
|
||||
|
||||
(function() {
|
||||
var CHUNK_SIZE = 5;
|
||||
|
||||
window.ContactsImporter = function(pContacts, pAccessToken, pConnector) {
|
||||
/* jshint validthis:true */
|
||||
|
||||
this.contacts = Object.keys(pContacts);
|
||||
var contactsHash = pContacts;
|
||||
var access_token = pAccessToken;
|
||||
var total = this.contacts.length;
|
||||
|
||||
var self = this;
|
||||
var serviceConnector = pConnector;
|
||||
var numImported = 0;
|
||||
|
||||
var mustHold = false;
|
||||
var holded = false;
|
||||
var mustFinish = false;
|
||||
|
||||
var isOnLine = navigator.onLine;
|
||||
|
||||
// To count the number of merged duplicate contacts
|
||||
var numMergedDuplicated = 0;
|
||||
|
||||
window.addEventListener('online', onLineChanged);
|
||||
window.addEventListener('offline', onLineChanged);
|
||||
|
||||
function onLineChanged() {
|
||||
isOnLine = navigator.onLine;
|
||||
}
|
||||
|
||||
function contactSaved(e) {
|
||||
var cfdata = this;
|
||||
if (typeof self.oncontactimported === 'function') {
|
||||
window.setTimeout(function() {
|
||||
self.oncontactimported(cfdata);
|
||||
}, 0);
|
||||
}
|
||||
continueCb();
|
||||
}
|
||||
|
||||
function contactSaveError(err) {
|
||||
window.console.error('Error while importing contact: ', err.name);
|
||||
|
||||
if (typeof self.onerror === 'function') {
|
||||
window.setTimeout(self.onerror.bind(null, err), 0);
|
||||
}
|
||||
continueCb();
|
||||
}
|
||||
|
||||
function saveMozContact(deviceContact, successCb, errorCb) {
|
||||
var req = navigator.mozContacts.save(
|
||||
utils.misc.toMozContact(deviceContact));
|
||||
|
||||
req.onsuccess = successCb;
|
||||
req.onerror = errorCb;
|
||||
}
|
||||
|
||||
function pictureReady(blobPicture) {
|
||||
var serviceContact = this;
|
||||
|
||||
var done = function() {
|
||||
var deviceContact = self.adapt(serviceContact);
|
||||
self.persist(deviceContact, contactSaved.bind(serviceContact),
|
||||
contactSaveError);
|
||||
};
|
||||
|
||||
// Photo is assigned to the service contact as it is needed by the
|
||||
// Fb Connector
|
||||
if (!blobPicture) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
utils.thumbnailImage(blobPicture, function gotThumbnail(thumbnail) {
|
||||
if (blobPicture !== thumbnail) {
|
||||
serviceContact.photo = [blobPicture, thumbnail];
|
||||
} else {
|
||||
serviceContact.photo = [blobPicture];
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function pictureError() {
|
||||
window.console.error('Error while getting picture for contact: ',
|
||||
this.user_id);
|
||||
self.persist(self.adapt(this), contactSaved.bind(this), contactSaveError);
|
||||
}
|
||||
|
||||
function pictureTimeout() {
|
||||
window.console.warn('Timeout while getting picture for contact: ',
|
||||
this.user_id);
|
||||
self.persist(self.adapt(this), contactSaved.bind(this),
|
||||
contactSaveError);
|
||||
}
|
||||
|
||||
this.start = function() {
|
||||
numMergedDuplicated = 0;
|
||||
|
||||
mustHold = false;
|
||||
holded = false;
|
||||
mustFinish = false;
|
||||
importContacts(numImported);
|
||||
};
|
||||
|
||||
this.hold = function() {
|
||||
mustHold = true;
|
||||
};
|
||||
|
||||
this.finish = function() {
|
||||
mustFinish = true;
|
||||
|
||||
if (holded) {
|
||||
notifySuccess();
|
||||
}
|
||||
};
|
||||
|
||||
this.resume = function() {
|
||||
mustHold = false;
|
||||
holded = false;
|
||||
mustFinish = false;
|
||||
|
||||
window.setTimeout(function resume_import() {
|
||||
importContacts(numImported);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// This method might be overritten
|
||||
this.persist = function(contactData, successCb, errorCb) {
|
||||
var cbs = {
|
||||
onmatch: function(matches) {
|
||||
numMergedDuplicated++;
|
||||
|
||||
contacts.adaptAndMerge(this, matches, {
|
||||
success: successCb,
|
||||
error: errorCb
|
||||
});
|
||||
}.bind(utils.misc.toMozContact(contactData)),
|
||||
onmismatch: function() {
|
||||
saveMozContact(this, successCb, function onMismatchError(evt) {
|
||||
errorCb(evt.target.error);
|
||||
});
|
||||
}.bind(contactData)
|
||||
};
|
||||
|
||||
// Try to match and if so merge is performed
|
||||
Matcher.match(contactData, 'passive', cbs);
|
||||
};
|
||||
|
||||
// This method might be overwritten
|
||||
this.adapt = function(serviceContact) {
|
||||
return serviceConnector.adaptDataForSaving(serviceContact);
|
||||
};
|
||||
|
||||
function importContacts(from) {
|
||||
for (var i = from; i < from + CHUNK_SIZE && i < total; i++) {
|
||||
importContact(i);
|
||||
}
|
||||
}
|
||||
|
||||
function importContact(index) {
|
||||
var serviceContact = contactsHash[self.contacts[index]];
|
||||
// We need to get the picture
|
||||
var callbacks = {
|
||||
success: pictureReady.bind(serviceContact),
|
||||
error: pictureError.bind(serviceContact),
|
||||
timeout: pictureTimeout.bind(serviceContact)
|
||||
};
|
||||
|
||||
if (isOnLine === true) {
|
||||
serviceConnector.downloadContactPicture(serviceContact,
|
||||
access_token, callbacks);
|
||||
}
|
||||
else {
|
||||
callbacks.success(null);
|
||||
}
|
||||
}
|
||||
|
||||
function notifySuccess() {
|
||||
if (typeof self.onsuccess === 'function') {
|
||||
window.setTimeout(function do_success() {
|
||||
self.onsuccess(numImported, numMergedDuplicated);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function continueCb() {
|
||||
numImported++;
|
||||
var next = numImported + CHUNK_SIZE - 1;
|
||||
if (next < total) {
|
||||
if (!mustHold && !mustFinish) {
|
||||
importContact(next);
|
||||
}
|
||||
else if (mustFinish && !holded) {
|
||||
notifySuccess();
|
||||
}
|
||||
|
||||
if (mustHold) {
|
||||
holded = true;
|
||||
}
|
||||
}
|
||||
else if (numImported >= total) {
|
||||
// End has been reached
|
||||
notifySuccess();
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
383
gaia/shared/js/contacts/import/facebook/facebook_connector.js
Executable file
383
gaia/shared/js/contacts/import/facebook/facebook_connector.js
Executable file
|
|
@ -0,0 +1,383 @@
|
|||
'use strict';
|
||||
|
||||
/* global fb, utils */
|
||||
/* global Matcher */
|
||||
|
||||
|
||||
if (!window.FacebookConnector) {
|
||||
window.FacebookConnector = (function() {
|
||||
|
||||
var nextUpdateTime;
|
||||
window.fb = window.fb || {};
|
||||
fb.operationsTimeout = parent.config ?
|
||||
parent.config.operationsTimeout : null;
|
||||
|
||||
// Only one instance in order to reuse it for saving friends in the
|
||||
// database. If the instance is not released between transactions, new
|
||||
// objects will be created.
|
||||
var reusedFbContact = new fb.Contact();
|
||||
reusedFbContact.ready = true;
|
||||
|
||||
function createFbImporter(clist, access_token) {
|
||||
/* jshint validthis:true */
|
||||
var out = new window.ContactsImporter(clist, access_token, this);
|
||||
|
||||
out.persist = persistFbData;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function persistFbData(data, successCb, errorCb, options) {
|
||||
var saveCbs = {
|
||||
success: function(e) {
|
||||
if (options === 'not_match') {
|
||||
successCb();
|
||||
return;
|
||||
}
|
||||
|
||||
var cbs = {
|
||||
onmatch: function(matches) {
|
||||
// For each of the matching contacts link with the just imported
|
||||
Object.keys(matches).forEach(function(aMatchId) {
|
||||
var fbContact = new
|
||||
fb.Contact(matches[aMatchId].matchingContact);
|
||||
|
||||
var mozContReq = fb.utils.getMozContact(data.uid);
|
||||
|
||||
mozContReq.onsuccess = function() {
|
||||
var linkParams = {
|
||||
uid: data.uid,
|
||||
mozContact: mozContReq.result
|
||||
};
|
||||
if (Array.isArray(data.photo) && data.photo[0]) {
|
||||
linkParams.photoUrl = data.pic_big;
|
||||
}
|
||||
var req = fbContact.linkTo(linkParams);
|
||||
|
||||
req.onsuccess = successCb;
|
||||
|
||||
req.onerror = function error(e) {
|
||||
window.console.error('Error while automatically linking : ',
|
||||
aMatchId, data.uid, e.target.name);
|
||||
successCb();
|
||||
};
|
||||
};
|
||||
mozContReq.onerror = function(e) {
|
||||
window.console.error(
|
||||
'Error getting mozContact data in automatic linking: ',
|
||||
e.target.error.name);
|
||||
successCb();
|
||||
};
|
||||
});
|
||||
},
|
||||
onmismatch: successCb
|
||||
};
|
||||
// Try to match and if so merge is performed
|
||||
Matcher.match(data, 'passive', cbs);
|
||||
},
|
||||
error: errorCb
|
||||
};
|
||||
saveFbContact(data, saveCbs.success, saveCbs.error);
|
||||
}
|
||||
|
||||
|
||||
function saveFbContact(data, successCb, errorCb) {
|
||||
var fbContact, successWrapperCb;
|
||||
|
||||
if (reusedFbContact.ready) {
|
||||
reusedFbContact.ready = null;
|
||||
fbContact = reusedFbContact;
|
||||
successWrapperCb = function onsuccess(e) {
|
||||
reusedFbContact.ready = true;
|
||||
successCb(e);
|
||||
};
|
||||
} else {
|
||||
fbContact = new fb.Contact();
|
||||
successWrapperCb = successCb;
|
||||
}
|
||||
|
||||
fbContact.setData(data);
|
||||
|
||||
var req = fbContact.save();
|
||||
req.onsuccess = successWrapperCb;
|
||||
req.onerror = errorCb;
|
||||
}
|
||||
|
||||
function setInfraForSync(existingContacts, friendsImported, callback) {
|
||||
// From the FTU the infra for sync will not be loaded
|
||||
if (!fb.sync) {
|
||||
(typeof callback === 'function') && callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check wether we need to set the update alarm
|
||||
window.asyncStorage.getItem(fb.utils.ALARM_ID_KEY, function(data) {
|
||||
if (!data || (existingContacts && existingContacts.length === 0) &&
|
||||
!friendsImported) {
|
||||
// This is the first contact imported
|
||||
fb.utils.setLastUpdate(nextUpdateTime, function() {
|
||||
var req = fb.sync.scheduleNextSync();
|
||||
if (typeof callback === 'function') {
|
||||
req.onsuccess = callback;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var UID_FILTER_IDX = 5;
|
||||
var acc_tk;
|
||||
|
||||
function buildFriendQuery(uid) {
|
||||
var aquery1 = [].concat(FRIENDS_QUERY);
|
||||
aquery1[UID_FILTER_IDX] = '= ' + uid;
|
||||
|
||||
return aquery1.join('');
|
||||
}
|
||||
|
||||
// Query that retrieves the information about friends
|
||||
var FRIENDS_QUERY = [
|
||||
'SELECT uid, name, first_name, last_name, pic_big, current_location, ' ,
|
||||
'middle_name, birthday_date, email, profile_update_time, ' ,
|
||||
' work, phones, hometown_location' ,
|
||||
' FROM user' ,
|
||||
' WHERE uid ',
|
||||
'IN (SELECT uid1 FROM friend WHERE uid2=me())' ,
|
||||
' ORDER BY ',
|
||||
'first_name'
|
||||
];
|
||||
|
||||
var friendsQueryStr = FRIENDS_QUERY.join('');
|
||||
|
||||
function contactDataLoaded(options, response) {
|
||||
if (!response.error) {
|
||||
// Just in case this is the first contact imported
|
||||
nextUpdateTime = Date.now();
|
||||
/* jshint validthis:true */
|
||||
var successCb = this.success;
|
||||
|
||||
var friend = response.data[0];
|
||||
if (friend) {
|
||||
var out1 = self.adaptDataForShowing(friend);
|
||||
var photoCbs = {};
|
||||
|
||||
photoCbs.success = function(blobPicture) {
|
||||
if (!blobPicture) {
|
||||
pictureLoaded(out1, options, successCb);
|
||||
return;
|
||||
}
|
||||
utils.thumbnailImage(blobPicture,
|
||||
function gotThumbnail(thumbnail) {
|
||||
if (blobPicture !== thumbnail) {
|
||||
out1.photo = [blobPicture, thumbnail];
|
||||
} else {
|
||||
out1.photo = [blobPicture];
|
||||
}
|
||||
pictureLoaded(out1, options, successCb);
|
||||
});
|
||||
}; // successCb
|
||||
|
||||
self.downloadContactPicture(friend, acc_tk, photoCbs);
|
||||
} // if friend
|
||||
else {
|
||||
window.console.error('FB: No Friend data found');
|
||||
/* jshint validthis:true */
|
||||
this.error('No friend data found');
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* jshint validthis:true */
|
||||
this.error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
function pictureLoaded(friend, options, success) {
|
||||
var data = self.adaptDataForSaving(friend);
|
||||
/* jshint validthis:true */
|
||||
persistFbData(data, function() {
|
||||
success({
|
||||
uid: friend.uid,
|
||||
url: friend.pic_big
|
||||
});
|
||||
}, function error(err) {
|
||||
console.error('Error while persisting: ', err);
|
||||
}, options);
|
||||
|
||||
// If there is no an alarm set it has to be set
|
||||
window.asyncStorage.getItem(fb.utils.ALARM_ID_KEY, function(data) {
|
||||
if (!data) {
|
||||
fb.utils.setLastUpdate(nextUpdateTime,
|
||||
fb.sync.scheduleNextSync);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function FacebookConnector() { }
|
||||
|
||||
FacebookConnector.prototype = {
|
||||
listAllContacts: function(access_token, callbacks, options) {
|
||||
if (options && options.orderBy === 'lastName') {
|
||||
FRIENDS_QUERY[FRIENDS_QUERY.length] = 'last_name';
|
||||
friendsQueryStr = FRIENDS_QUERY.join('');
|
||||
}
|
||||
return fb.utils.runQuery(friendsQueryStr, {
|
||||
success: callbacks.success,
|
||||
error: callbacks.error,
|
||||
timeout: callbacks.timeout
|
||||
},access_token);
|
||||
},
|
||||
|
||||
listDeviceContacts: function(callbacks) {
|
||||
// Dummy implementation for the time being
|
||||
var req = fb.utils.getAllFbContacts();
|
||||
|
||||
req.onsuccess = function successHandler() {
|
||||
callbacks.success(req.result);
|
||||
};
|
||||
req.onerror = callbacks.error;
|
||||
},
|
||||
|
||||
getImporter: function(contactsList, access_token) {
|
||||
return createFbImporter.bind(this)(contactsList, access_token);
|
||||
},
|
||||
|
||||
// Imports a Contact to FB indexedDB private database
|
||||
importContact: function(uid, access_token, callbacks, options) {
|
||||
acc_tk = access_token;
|
||||
var oneFriendQuery = buildFriendQuery(uid);
|
||||
var auxCallbacks = {
|
||||
success: contactDataLoaded.bind(callbacks, options),
|
||||
error: callbacks.error,
|
||||
timeout: callbacks.timeout
|
||||
};
|
||||
fb.utils.runQuery(oneFriendQuery, auxCallbacks, access_token);
|
||||
},
|
||||
|
||||
cleanContacts: function(contactsList, mode, cb) {
|
||||
if (mode === 'update') {
|
||||
var cleaner = new fb.utils.FbContactsCleaner(contactsList, mode);
|
||||
window.setTimeout(cleaner.start, 0);
|
||||
cb(cleaner);
|
||||
}
|
||||
else {
|
||||
// Mode === clear
|
||||
var req = fb.utils.clearFbData();
|
||||
req.onsuccess = function() {
|
||||
cb(req.result);
|
||||
};
|
||||
|
||||
req.onerror = function(e) {
|
||||
window.console.error('Error while starting cleaning: ',
|
||||
e.target.error.name);
|
||||
// Notifying the UI importer to show an error to the user
|
||||
// As we cannot get a cleaner the param is set to null
|
||||
cb(null);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
adaptDataForShowing: function(source) {
|
||||
|
||||
var box = utils.misc.getPreferredPictureBox();
|
||||
var picWidth = box.width;
|
||||
var picHeight = box.height;
|
||||
|
||||
var out = fb.friend2mozContact(source);
|
||||
out.contactPictureUri = 'https://graph.facebook.com/' +
|
||||
source.uid + '/picture?type=square' +
|
||||
'&width=' + picWidth +
|
||||
'&height=' + picHeight +
|
||||
'&t=' + Date.now();
|
||||
return out;
|
||||
},
|
||||
|
||||
adaptDataForSaving: function fbAdapt(cfdata) {
|
||||
var worksAt = fb.getWorksAt(cfdata);
|
||||
var address = fb.getAddresses(cfdata);
|
||||
|
||||
var birthDate = null;
|
||||
if (cfdata.birthday_date && cfdata.birthday_date.length > 0) {
|
||||
birthDate = fb.getBirthDate(cfdata.birthday_date);
|
||||
}
|
||||
|
||||
var fbInfo = {
|
||||
bday: birthDate,
|
||||
org: [worksAt]
|
||||
};
|
||||
|
||||
if (address) {
|
||||
fbInfo.adr = address;
|
||||
}
|
||||
|
||||
// This is the short telephone number to enable indexing
|
||||
if (cfdata.shortTelephone) {
|
||||
fbInfo.shortTelephone = cfdata.shortTelephone;
|
||||
delete cfdata.shortTelephone;
|
||||
}
|
||||
|
||||
// Check whether we were able to get the photo or not
|
||||
fbInfo.url = [];
|
||||
|
||||
if (cfdata.photo) {
|
||||
fbInfo.photo = cfdata.photo;
|
||||
if (cfdata.pic_big) {
|
||||
// The URL is stored for synchronization purposes
|
||||
fb.setFriendPictureUrl(fbInfo, cfdata.pic_big);
|
||||
}
|
||||
delete cfdata.photo;
|
||||
}
|
||||
// Facebook info is set and then contact is saved
|
||||
cfdata.fbInfo = fbInfo;
|
||||
|
||||
return cfdata;
|
||||
},
|
||||
|
||||
// It would allow to know the UID of a service contact already imported
|
||||
// on the device. That is needed by the generic importer, for live
|
||||
// a dummy implementation as we are currently not supporting updates
|
||||
getContactUid: function(deviceContact) {
|
||||
return fb.getFriendUid(deviceContact);
|
||||
},
|
||||
|
||||
get name() {
|
||||
return 'facebook';
|
||||
},
|
||||
|
||||
get automaticLogout() {
|
||||
return false;
|
||||
},
|
||||
|
||||
downloadContactPicture: function(contact, access_token, callbacks) {
|
||||
return fb.utils.getFriendPicture(contact.uid, callbacks.success,
|
||||
access_token, utils.misc.getPreferredPictureDetail());
|
||||
},
|
||||
|
||||
oncontactsloaded: function(lfriends) {
|
||||
// This is the timestamp for later syncing as it set at the time
|
||||
// when data was ready
|
||||
nextUpdateTime = Date.now();
|
||||
// Now caching the number
|
||||
fb.utils.setCachedNumFriends(lfriends.length);
|
||||
},
|
||||
|
||||
oncontactsimported: function(existingContacts, friendsImported, cb) {
|
||||
setInfraForSync(existingContacts, friendsImported, cb);
|
||||
fb.contacts.flush();
|
||||
},
|
||||
|
||||
startSync: function(existingContacts, myFriendsByUid, syncFinished) {
|
||||
var callbacks = {
|
||||
success: syncFinished
|
||||
};
|
||||
fb.sync.startWithData(existingContacts, myFriendsByUid, callbacks);
|
||||
},
|
||||
|
||||
scheduleNextSync: function() {
|
||||
fb.sync.scheduleNextSync();
|
||||
}
|
||||
};
|
||||
var self = new FacebookConnector();
|
||||
|
||||
return self;
|
||||
})();
|
||||
}
|
||||
659
gaia/shared/js/contacts/import/facebook/fb_contact.js
Executable file
659
gaia/shared/js/contacts/import/facebook/fb_contact.js
Executable file
|
|
@ -0,0 +1,659 @@
|
|||
'use strict';
|
||||
|
||||
/* global utils */
|
||||
|
||||
var fb = window.fb || {};
|
||||
|
||||
// Encapsulates all the logic to obtain the data for a FB contact
|
||||
fb.Contact = function(deviceContact, cid) {
|
||||
var contactData;
|
||||
var devContact = deviceContact;
|
||||
var contactid = cid;
|
||||
|
||||
function doGetFacebookUid(data) {
|
||||
return fb.getFriendUid(data);
|
||||
}
|
||||
|
||||
function getFacebookUid() {
|
||||
return doGetFacebookUid(deviceContact);
|
||||
}
|
||||
|
||||
function setFacebookUid(value) {
|
||||
doSetFacebookUid(deviceContact, value);
|
||||
}
|
||||
|
||||
function doSetFacebookUid(dcontact, value) {
|
||||
if (!dcontact.category) {
|
||||
dcontact.category = [];
|
||||
}
|
||||
|
||||
if (dcontact.category.indexOf(fb.CATEGORY) === -1) {
|
||||
markAsFb(dcontact);
|
||||
}
|
||||
|
||||
var idx = dcontact.category.indexOf(fb.CATEGORY);
|
||||
|
||||
dcontact.category[idx + 2] = value;
|
||||
}
|
||||
|
||||
function markAsFb(dcontact) {
|
||||
if (!dcontact.category) {
|
||||
dcontact.category = [];
|
||||
}
|
||||
|
||||
if (dcontact.category.indexOf(fb.CATEGORY) === -1) {
|
||||
dcontact.category.push(fb.CATEGORY);
|
||||
dcontact.category.push(fb.NOT_LINKED);
|
||||
}
|
||||
|
||||
return dcontact;
|
||||
}
|
||||
|
||||
// Mark a mozContact (deviceContact) as linked to a FB contact (uid)
|
||||
function markAsLinked(dcontact, uid) {
|
||||
if (!dcontact.category) {
|
||||
dcontact.category = [];
|
||||
}
|
||||
|
||||
if (dcontact.category.indexOf(fb.LINKED) === -1) {
|
||||
dcontact.category.push(fb.CATEGORY);
|
||||
dcontact.category.push(fb.LINKED);
|
||||
dcontact.category.push(uid);
|
||||
}
|
||||
|
||||
return dcontact;
|
||||
}
|
||||
|
||||
function promoteToLinked(dcontact) {
|
||||
var idx = dcontact.category.indexOf(fb.NOT_LINKED);
|
||||
|
||||
if (idx != -1) {
|
||||
dcontact.category[idx] = fb.LINKED;
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the data for an imported FB Contact
|
||||
this.setData = function(data) {
|
||||
contactData = data;
|
||||
};
|
||||
|
||||
Object.defineProperty(this, 'uid', {
|
||||
get: getFacebookUid,
|
||||
set: setFacebookUid,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'mozContact', {
|
||||
get: getDevContact
|
||||
});
|
||||
|
||||
function getDevContact() {
|
||||
return devContact;
|
||||
}
|
||||
|
||||
// For saving an imported FB contact
|
||||
this.save = function() {
|
||||
var outReq = new fb.utils.Request();
|
||||
|
||||
if (contactData && navigator.mozContacts) {
|
||||
window.setTimeout(function save_do() {
|
||||
var photoList = contactData.fbInfo.photo;
|
||||
if (photoList && photoList.length > 0 && photoList[0]) {
|
||||
utils.squareImage(photoList[0], function squared(squared_image) {
|
||||
photoList[0] = squared_image;
|
||||
doSave(outReq);
|
||||
});
|
||||
}
|
||||
else {
|
||||
doSave(outReq);
|
||||
}
|
||||
},0);
|
||||
}
|
||||
else {
|
||||
throw 'Data or mozContacts not available';
|
||||
}
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
function doSave(outReq) {
|
||||
// Info to be saved on mozContacts
|
||||
var contactInfo = {};
|
||||
|
||||
// Copying names to the mozContact
|
||||
copyNames(contactData, contactInfo);
|
||||
// URL (photo, etc) is stored also with the Device contact
|
||||
if (contactData.fbInfo.url) {
|
||||
contactInfo.url = contactData.fbInfo.url;
|
||||
}
|
||||
|
||||
doSetFacebookUid(contactInfo, contactData.uid);
|
||||
|
||||
var fbReq = persistToFbCache(contactData);
|
||||
|
||||
fbReq.onsuccess = function() {
|
||||
var mozContactsReq = navigator.mozContacts.save(
|
||||
utils.misc.toMozContact(contactInfo));
|
||||
mozContactsReq.onsuccess = function(e) {
|
||||
outReq.done(fbReq.result);
|
||||
};
|
||||
mozContactsReq.onerror = function(e) {
|
||||
window.console.error('FB: Error while saving on mozContacts',
|
||||
e.target.error);
|
||||
outReq.failed(e.target.error);
|
||||
}; // fbReq.onsuccess
|
||||
};
|
||||
fbReq.onerror = function() {
|
||||
window.console.error('FB: Error while saving on datastore',
|
||||
fbReq.error && fbReq.error.name);
|
||||
outReq.failed(fbReq.error);
|
||||
};
|
||||
}
|
||||
|
||||
// Persists FB Friend Data to the FB cache
|
||||
function persistToFbCache(contactData, mUpdate) {
|
||||
var isUpdate = (mUpdate === true ? mUpdate : false);
|
||||
|
||||
var outReq = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function persist_fb_do() {
|
||||
// now saving the FB-originated data to the "private cache area"
|
||||
var data = Object.create(contactData.fbInfo);
|
||||
|
||||
data.tel = contactData.tel || [];
|
||||
data.email = contactData.email || [];
|
||||
data.uid = contactData.uid;
|
||||
|
||||
Object.keys(contactData.fbInfo).forEach(function(prop) {
|
||||
data[prop] = contactData.fbInfo[prop];
|
||||
});
|
||||
|
||||
// Names are also stored on datastore
|
||||
// thus restoring the contact (if unlinked) will be trivial
|
||||
copyNames(contactData, data);
|
||||
|
||||
var updaterFn = (isUpdate === true ?
|
||||
fb.contacts.update : fb.contacts.save);
|
||||
var fbReq = updaterFn(data);
|
||||
|
||||
fbReq.onsuccess = function() {
|
||||
outReq.done(fbReq.result);
|
||||
};
|
||||
fbReq.onerror = function() {
|
||||
window.console.error('FB: Error while saving Fb data on the Datastore',
|
||||
fbReq.error && fbReq.error.name);
|
||||
outReq.failed(fbReq.error);
|
||||
};
|
||||
},0);
|
||||
|
||||
return outReq;
|
||||
}
|
||||
|
||||
// Updates a FB Contact
|
||||
this.update = function(contactData) {
|
||||
|
||||
// Auxiliary function to do all the work for saving to the FB cache
|
||||
function auxDoUpdate(contactData, outReq) {
|
||||
// If the photo was not updated it is needed to save it
|
||||
if (!contactData.fbInfo.photo) {
|
||||
var dataReq = fb.contacts.get(contactData.uid);
|
||||
dataReq.onsuccess = function() {
|
||||
contactData.fbInfo.photo = dataReq.result.photo;
|
||||
auxCachePersist(contactData, outReq, true);
|
||||
};
|
||||
dataReq.onerror = function() {
|
||||
window.console.error('Error while retrieving existing photo for ',
|
||||
contactData.uid, dataReq.error);
|
||||
outReq.failed(dataReq.error);
|
||||
};
|
||||
}
|
||||
else {
|
||||
utils.squareImage(contactData.fbInfo.photo[0],
|
||||
function sq_img(squaredImg) {
|
||||
contactData.fbInfo.photo[0] = squaredImg;
|
||||
auxCachePersist(contactData, outReq, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the data to the FB Cache
|
||||
function auxCachePersist(contactData, outReq, isUpdate) {
|
||||
var fbReq = persistToFbCache(contactData, isUpdate);
|
||||
|
||||
fbReq.onsuccess = function() {
|
||||
outReq.done(fbReq.result);
|
||||
};
|
||||
fbReq.onerror = function() {
|
||||
window.console.error('FB: Error while saving to FB cache: ',
|
||||
contactData.uid, fbReq.error);
|
||||
outReq.failed(fbReq.error);
|
||||
};
|
||||
}
|
||||
|
||||
// Code starts here
|
||||
var outReq = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function update_do() {
|
||||
if (!fb.isFbLinked(devContact)) {
|
||||
copyNames(contactData, devContact);
|
||||
} else {
|
||||
// We are going to update names if they are propagated
|
||||
revisitPropagatedNames(contactData, devContact);
|
||||
}
|
||||
|
||||
// Check whether the photo has changed
|
||||
if (contactData.fbInfo.photo) {
|
||||
devContact.url = contactData.fbInfo.url;
|
||||
}
|
||||
|
||||
var auxReq = new fb.utils.Request();
|
||||
auxReq.onsuccess = function() {
|
||||
var mozContactsReq = navigator.mozContacts.save(
|
||||
utils.misc.toMozContact(devContact));
|
||||
mozContactsReq.onsuccess = function(e) {
|
||||
outReq.done();
|
||||
};
|
||||
|
||||
mozContactsReq.onerror = function(e) {
|
||||
window.console.error('FB: Error while saving mozContact: ',
|
||||
devContact.id, e.target.error);
|
||||
outReq.failed(e.target.error);
|
||||
};
|
||||
}; // auxReq.onsuccess
|
||||
|
||||
auxReq.onerror = function(e) {
|
||||
outReq.failed(e.target.error);
|
||||
}; // auxReq.onerror
|
||||
|
||||
// And now doing the update
|
||||
auxDoUpdate(contactData, auxReq);
|
||||
},0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
function asArrayOfValues(value) {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
function copyNames(source, destination) {
|
||||
destination.name = asArrayOfValues(source.name);
|
||||
destination.givenName = asArrayOfValues(source.givenName);
|
||||
destination.familyName = asArrayOfValues(source.familyName);
|
||||
destination.additionalName = asArrayOfValues(source.additionalName);
|
||||
}
|
||||
|
||||
// Merges mozContact data with Facebook data
|
||||
this.merge = function(fbdata) {
|
||||
return fb.mergeContact(devContact, fbdata);
|
||||
};
|
||||
|
||||
|
||||
// Gets the data
|
||||
this.getData = function() {
|
||||
return fb.getData(devContact);
|
||||
};
|
||||
|
||||
this.getDataAndValues = function() {
|
||||
var outReq = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function do_getData() {
|
||||
var uid = doGetFacebookUid(devContact);
|
||||
|
||||
if (uid) {
|
||||
var fbreq = fb.contacts.get(uid);
|
||||
|
||||
fbreq.onsuccess = (function() {
|
||||
var fbdata = fbreq.result;
|
||||
|
||||
var out1 = this.merge(fbdata);
|
||||
|
||||
var out2 = {};
|
||||
|
||||
var duplicates = {};
|
||||
Object.keys(fbdata).forEach(function(key) {
|
||||
var dataElement = fbdata[key];
|
||||
|
||||
if (dataElement && Array.isArray(dataElement) && key !== 'photo') {
|
||||
dataElement.forEach(function(item) {
|
||||
if (item.value && item.value.length > 0) {
|
||||
// Check for duplicates. Those duplicates are annotated to
|
||||
// be later removed from the out2 array
|
||||
var dupList = fb.checkDuplicates(key, item, devContact[key],
|
||||
fbdata.shortTelephone);
|
||||
dupList.forEach(function(aDup) {
|
||||
duplicates[aDup] = true;
|
||||
});
|
||||
|
||||
out2[item.value] = true;
|
||||
}
|
||||
else if (typeof item === 'string' && item.length > 0) {
|
||||
out2[item] = true;
|
||||
}
|
||||
else if (key === 'adr') {
|
||||
if (item.locality) {
|
||||
out2[item.locality] = true;
|
||||
}
|
||||
if (item.countryName) {
|
||||
out2[item.countryName] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (key === 'photo') {
|
||||
out2.hasPhoto = true;
|
||||
}
|
||||
else if (dataElement) {
|
||||
out2[dataElement] = true;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(duplicates).forEach(function(aDup) {
|
||||
delete out2[aDup];
|
||||
});
|
||||
|
||||
outReq.done([out1, out2, fbdata]);
|
||||
|
||||
}).bind(this);
|
||||
|
||||
fbreq.onerror = function() {
|
||||
outReq.failed(fbreq.error);
|
||||
};
|
||||
}
|
||||
else {
|
||||
outReq.done([devContact, {}]);
|
||||
}
|
||||
}.bind(this), 0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
this.promoteToLinked = function() {
|
||||
promoteToLinked(devContact);
|
||||
};
|
||||
|
||||
this.linkTo = function(fbFriend) {
|
||||
var out = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function do_linkTo() {
|
||||
if (!devContact) {
|
||||
// We need to get the Contact data
|
||||
var req = fb.utils.getContactData(contactid);
|
||||
|
||||
req.onsuccess = function() {
|
||||
devContact = req.result;
|
||||
doLink(devContact, fbFriend, out);
|
||||
}; // req.onsuccess
|
||||
|
||||
req.onerror = function() {
|
||||
throw 'FB: Error while retrieving contact data';
|
||||
};
|
||||
} // devContact
|
||||
else {
|
||||
doLink(devContact, fbFriend, out);
|
||||
}
|
||||
},0);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
function propagateField(field, from, to) {
|
||||
var copied = false;
|
||||
|
||||
// The field is copied when it is undefined in the target object
|
||||
if (!Array.isArray(to[field]) || !to[field][0] || !to[field][0].trim()) {
|
||||
to[field] = from[field];
|
||||
copied = true;
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
function createName(contact) {
|
||||
contact.name = [];
|
||||
|
||||
if (Array.isArray(contact.givenName)) {
|
||||
contact.name[0] = contact.givenName[0] + ' ';
|
||||
}
|
||||
|
||||
if (Array.isArray(contact.familyName)) {
|
||||
contact.name[0] += contact.familyName[0];
|
||||
}
|
||||
}
|
||||
|
||||
function propagateNames(from, to) {
|
||||
var isGivenNamePropagated = propagateField('givenName', from, to);
|
||||
var isFamilyNamePropagated = propagateField('familyName', from, to);
|
||||
|
||||
if (isGivenNamePropagated || isFamilyNamePropagated) {
|
||||
// We are going to mark the propagation in the category field
|
||||
if (isGivenNamePropagated) {
|
||||
fb.setPropagatedFlag('givenName', to);
|
||||
}
|
||||
|
||||
if (isFamilyNamePropagated) {
|
||||
fb.setPropagatedFlag('familyName', to);
|
||||
}
|
||||
|
||||
createName(to);
|
||||
}
|
||||
}
|
||||
|
||||
// This method copies names to a contact when they are propagated from fb
|
||||
function revisitPropagatedNames(from, to) {
|
||||
if (fb.isPropagated('givenName', to)) {
|
||||
to.givenName = from.givenName;
|
||||
}
|
||||
|
||||
if (fb.isPropagated('familyName', to)) {
|
||||
to.familyName = from.familyName;
|
||||
}
|
||||
|
||||
createName(to);
|
||||
}
|
||||
|
||||
function doLink(contactdata, fbFriend, out) {
|
||||
if (contactdata) {
|
||||
if (fbFriend.uid) {
|
||||
// When marking as linked is needed to store a reference to the profile
|
||||
// picture URL
|
||||
markAsLinked(contactdata, fbFriend.uid);
|
||||
if (fbFriend.photoUrl) {
|
||||
fb.setFriendPictureUrl(contactdata, fbFriend.photoUrl);
|
||||
}
|
||||
else if (fbFriend.mozContact) {
|
||||
contactdata.url = fbFriend.mozContact.url;
|
||||
}
|
||||
}
|
||||
else if (fbFriend.mozContact) {
|
||||
markAsLinked(contactdata, doGetFacebookUid(fbFriend.mozContact));
|
||||
contactdata.url = fbFriend.mozContact.url;
|
||||
}
|
||||
|
||||
propagateNames(fbFriend.mozContact, contactdata);
|
||||
var mozContactsReq = navigator.mozContacts.save(
|
||||
utils.misc.toMozContact(contactdata));
|
||||
|
||||
mozContactsReq.onsuccess = function(e) {
|
||||
// The FB contact on mozContacts needs to be removed
|
||||
if (fbFriend.mozContact && !fb.isFbLinked(fbFriend.mozContact)) {
|
||||
var deleteReq = navigator.mozContacts.remove(
|
||||
utils.misc.toMozContact(fbFriend.mozContact));
|
||||
|
||||
deleteReq.onsuccess = function(e) {
|
||||
out.done(e.target.result);
|
||||
};
|
||||
|
||||
deleteReq.onerror = function(e) {
|
||||
window.console.error('FB: Error while linking');
|
||||
out.failed(e.target.error);
|
||||
};
|
||||
}
|
||||
else {
|
||||
out.done(e.target.result);
|
||||
}
|
||||
|
||||
}; // mozContactsReq.onsuccess
|
||||
|
||||
mozContactsReq.onerror = function(e) {
|
||||
out.failed(e.target.error);
|
||||
}; // mozContactsReq.onerror
|
||||
} // if(dev.contact)
|
||||
else {
|
||||
throw 'FB: Contact data not defined';
|
||||
}
|
||||
}
|
||||
|
||||
// if type === 'hard' the FB Friend is removed from the cache
|
||||
this.unlink = function(type) {
|
||||
var out = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function do_unlink() {
|
||||
if (!devContact) {
|
||||
// We need to get the Contact data
|
||||
var req = fb.utils.getContactData(contactid);
|
||||
|
||||
req.onsuccess = function() {
|
||||
devContact = req.result;
|
||||
doUnlink(devContact, out, type);
|
||||
}; // req.onsuccess
|
||||
|
||||
req.onerror = function() {
|
||||
throw 'FB: Error while retrieving contact data';
|
||||
};
|
||||
} // devContact
|
||||
else {
|
||||
doUnlink(devContact, out, type);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
// Reset givenName and familyName if it is needed after unlinking
|
||||
function resetNames(dContact) {
|
||||
fb.resetNames(dContact);
|
||||
}
|
||||
|
||||
function doUnlink(dContact, out, type) {
|
||||
var theType = type || 'soft';
|
||||
var uid = doGetFacebookUid(dContact);
|
||||
|
||||
resetNames(dContact);
|
||||
fb.markAsUnlinked(dContact);
|
||||
var req = navigator.mozContacts.save(utils.misc.toMozContact(dContact));
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
if (theType !== 'hard') {
|
||||
|
||||
// Then the original FB imported contact is restored
|
||||
var fbNumReq = fb.utils.getNumberMozContacts(uid);
|
||||
|
||||
fbNumReq.onsuccess = function() {
|
||||
// Because we have already unlinked one
|
||||
if (fbNumReq.result >= 1) {
|
||||
out.done();
|
||||
} else {
|
||||
var fbDataReq = fb.contacts.get(uid);
|
||||
|
||||
fbDataReq.onsuccess = function() {
|
||||
var imported = fbDataReq.result;
|
||||
|
||||
var data = {};
|
||||
copyNames(imported, data);
|
||||
data.url = imported.url;
|
||||
doSetFacebookUid(data, uid);
|
||||
|
||||
// The FB contact is restored
|
||||
var aMozContact = utils.misc.toMozContact(data);
|
||||
var reqRestore = navigator.mozContacts.save(aMozContact);
|
||||
|
||||
reqRestore.onsuccess = function(e) {
|
||||
out.done(aMozContact.id);
|
||||
};
|
||||
|
||||
reqRestore.onerror = function(e) {
|
||||
out.failed(e.target.error);
|
||||
};
|
||||
};
|
||||
|
||||
fbDataReq.onerror = function() {
|
||||
window.console.error('FB: Error while unlinking contact data');
|
||||
out.failed(fbDataReq.error);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fbNumReq.onerror = function() {
|
||||
window.console.error('FB: Error while unlinking contact data');
|
||||
out.failed(fbNumReq.error);
|
||||
};
|
||||
}
|
||||
else {
|
||||
// FB Data is removed from the cache
|
||||
var removeReq = fb.contacts.remove(uid, true);
|
||||
|
||||
removeReq.onsuccess = function() {
|
||||
out.done(removeReq.result);
|
||||
};
|
||||
|
||||
removeReq.onerror = function() {
|
||||
out.failed(removeReq.error);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function(e) {
|
||||
out.failed(e.target.error);
|
||||
};
|
||||
}
|
||||
|
||||
this.remove = function(pforceFlush) {
|
||||
var forceFlush = (pforceFlush === true) ? pforceFlush : false;
|
||||
|
||||
var out = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function do_remove() {
|
||||
var uid = doGetFacebookUid(devContact);
|
||||
|
||||
var fbNumReq = fb.utils.getNumberMozContacts(uid);
|
||||
fbNumReq.onsuccess = function num_onsuccess() {
|
||||
var removeReq;
|
||||
// If there is only one Device Contact associated
|
||||
// Then corresponding FB Data is removed otherwise only
|
||||
// the device contact is removed
|
||||
if (fbNumReq.result === 1) {
|
||||
removeReq = navigator.mozContacts.remove(
|
||||
utils.misc.toMozContact(devContact));
|
||||
removeReq.onsuccess = function(e) {
|
||||
var fbReq = fb.contacts.remove(uid, forceFlush);
|
||||
fbReq.onsuccess = function() {
|
||||
out.done(fbReq.result);
|
||||
};
|
||||
|
||||
fbReq.onerror = function() {
|
||||
out.failed(fbReq.error);
|
||||
};
|
||||
};
|
||||
removeReq.onerror = function(e) {
|
||||
out.failed(e.target.error);
|
||||
};
|
||||
}
|
||||
else {
|
||||
removeReq = navigator.mozContacts.remove(
|
||||
utils.misc.toMozContact(devContact));
|
||||
removeReq.onsuccess = function(e) {
|
||||
out.done();
|
||||
};
|
||||
removeReq.onerror = function(e) {
|
||||
out.failed(e.target.error);
|
||||
};
|
||||
}
|
||||
};
|
||||
}, 0);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
}; // fb.Contact
|
||||
260
gaia/shared/js/contacts/import/facebook/fb_contact_utils.js
Executable file
260
gaia/shared/js/contacts/import/facebook/fb_contact_utils.js
Executable file
|
|
@ -0,0 +1,260 @@
|
|||
'use strict';
|
||||
|
||||
/* global fb, utils */
|
||||
|
||||
/* jshint ignore:start */
|
||||
var fb = this.fb || {};
|
||||
/* jshint ignore:end */
|
||||
|
||||
fb.PROPAGATED_PREFIX = 'fb_propagated_';
|
||||
|
||||
// Types of URLs for FB Information
|
||||
fb.PROFILE_PHOTO_URI = 'fb_profile_photo';
|
||||
fb.FRIEND_URI = 'fb_friend';
|
||||
fb.DEFAULT_PHONE_TYPE = 'other';
|
||||
fb.DEFAULT_EMAIL_TYPE = 'other';
|
||||
// This year indicates that the year can be ignored (yearless date)
|
||||
fb.FLAG_YEAR_IGNORED = 9996;
|
||||
|
||||
fb.CONTACTS_APP_ORIGIN = location.origin;
|
||||
|
||||
fb.isPropagated = function fcu_isPropagated(field, devContact) {
|
||||
return (devContact.category && devContact.category.
|
||||
indexOf(fb.PROPAGATED_PREFIX + field) !== -1);
|
||||
};
|
||||
|
||||
fb.removePropagatedFlag = function fcu_removePropagatedFlag(field, devContact) {
|
||||
var idx = devContact.category.indexOf(fb.PROPAGATED_PREFIX + field);
|
||||
if (idx !== -1) {
|
||||
devContact.category.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
fb.setPropagatedFlag = function fcu_setPropagatedFlag(field, devContact) {
|
||||
var idx = devContact.category.indexOf(fb.PROPAGATED_PREFIX + field);
|
||||
if (idx === -1) {
|
||||
devContact.category.push(fb.PROPAGATED_PREFIX + field);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fb.getFriendPictureUrl = function(devContact) {
|
||||
var out;
|
||||
|
||||
var urls = devContact.url;
|
||||
|
||||
if (urls) {
|
||||
for (var c = 0; c < urls.length; c++) {
|
||||
var aurl = urls[c];
|
||||
if (aurl.type.indexOf(fb.PROFILE_PHOTO_URI) !== -1) {
|
||||
out = aurl.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
fb.setFriendPictureUrl = function(devContact, url) {
|
||||
var urls = devContact.url || [];
|
||||
|
||||
urls.push({
|
||||
type: [fb.PROFILE_PHOTO_URI],
|
||||
value: url
|
||||
});
|
||||
|
||||
devContact.url = urls;
|
||||
};
|
||||
|
||||
// Adapts data to the mozContact format names
|
||||
fb.friend2mozContact = function(f) {
|
||||
|
||||
function normalizeFbPhoneNumber(phone) {
|
||||
var out = phone.number;
|
||||
if (phone.country_code && out.indexOf('+') !== 0) {
|
||||
out = '+' + phone.country_code + out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Check whether this has been already normalized to mozContact
|
||||
if (Array.isArray(f.familyName)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
// givenName is put as name but it should be f.first_name
|
||||
f.familyName = [f.last_name ? f.last_name.trim() : (f.last_name || '')];
|
||||
var middleName = f.middle_name ? f.middle_name.trim() : (f.middle_name || '');
|
||||
f.additionalName = middleName;
|
||||
var firstName = f.first_name ? f.first_name.trim() : (f.first_name || '');
|
||||
f.givenName = [(firstName + ' ' + middleName).trim()];
|
||||
|
||||
delete f.last_name;
|
||||
delete f.middle_name;
|
||||
delete f.first_name;
|
||||
|
||||
if (f.email) {
|
||||
f.email1 = f.email;
|
||||
f.email = [{
|
||||
type: [fb.DEFAULT_EMAIL_TYPE],
|
||||
value: f.email
|
||||
}];
|
||||
}
|
||||
else {
|
||||
f.email1 = '';
|
||||
}
|
||||
|
||||
if (Array.isArray(f.phones) && f.phones.length > 0) {
|
||||
f.tel = [];
|
||||
f.shortTelephone = [];
|
||||
f.phones.forEach(function(aphone) {
|
||||
f.tel.push({
|
||||
type: [fb.DEFAULT_PHONE_TYPE],
|
||||
value: normalizeFbPhoneNumber(aphone)
|
||||
});
|
||||
// Enabling to find FB phones by short number
|
||||
f.shortTelephone.push(aphone.number);
|
||||
});
|
||||
}
|
||||
|
||||
delete f.phones;
|
||||
|
||||
f.uid = f.uid.toString();
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Auxiliary function to know where a contact works
|
||||
*
|
||||
*/
|
||||
fb.getWorksAt = function(fbdata) {
|
||||
var out = '';
|
||||
if (fbdata.work && fbdata.work.length > 0) {
|
||||
// It is assumed that first is the latest
|
||||
out = fbdata.work[0].employer.name;
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Facebook dates are MM/DD/YYYY
|
||||
*
|
||||
* Returns the birth date
|
||||
*
|
||||
*/
|
||||
fb.getBirthDate = function getBirthDate(sbday) {
|
||||
var out = new Date(0);
|
||||
|
||||
var imonth = sbday.indexOf('/');
|
||||
var smonth = sbday.substring(0, imonth);
|
||||
|
||||
var iyear = sbday.lastIndexOf('/');
|
||||
if (iyear === imonth) {
|
||||
iyear = sbday.length;
|
||||
}
|
||||
var sday = sbday.substring(imonth + 1, iyear);
|
||||
|
||||
var syear = sbday.substring(iyear + 1, sbday.length);
|
||||
|
||||
out.setUTCDate(parseInt(sday, 10));
|
||||
out.setUTCMonth(parseInt(smonth, 10) - 1, parseInt(sday, 10));
|
||||
|
||||
if (syear && syear.length > 0) {
|
||||
out.setUTCFullYear(parseInt(syear, 10));
|
||||
}
|
||||
else {
|
||||
// 9996 is the year that flags a not known year
|
||||
out.setUTCFullYear(fb.FLAG_YEAR_IGNORED);
|
||||
}
|
||||
|
||||
out.setUTCHours(0);
|
||||
out.setUTCMinutes(0);
|
||||
out.setUTCSeconds(0);
|
||||
out.setUTCMilliseconds(0);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
fb.getAddresses = function(fbdata) {
|
||||
|
||||
function fillAddress(fbAddress, type) {
|
||||
var outAddr = {};
|
||||
|
||||
outAddr.type = [type];
|
||||
outAddr.locality = fbAddress.city || '';
|
||||
outAddr.region = fbAddress.state || '';
|
||||
outAddr.countryName = fbAddress.country || '';
|
||||
|
||||
return outAddr;
|
||||
}
|
||||
|
||||
var out = [];
|
||||
var addrTypes = {
|
||||
'home': 'hometown_location',
|
||||
'current': 'current_location'
|
||||
};
|
||||
|
||||
Object.keys(addrTypes).forEach(function onAddressType(type) {
|
||||
var addrObj = fbdata[addrTypes[type]];
|
||||
if (addrObj) {
|
||||
out.push(fillAddress(addrObj, type));
|
||||
}
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
// The contact is now totally unlinked
|
||||
// [...,facebook, fb_not_linked, 123456,....]
|
||||
fb.markAsUnlinked = function(devContact) {
|
||||
var category = devContact.category;
|
||||
var updatedCategory = [];
|
||||
|
||||
if (category) {
|
||||
var idx = category.indexOf(fb.CATEGORY);
|
||||
if (idx !== -1) {
|
||||
for (var c = 0; c < idx; c++) {
|
||||
updatedCategory.push(category[c]);
|
||||
}
|
||||
// The facebook category, the linked mark and the UID are skipped
|
||||
for (c = idx + 3; c < category.length; c++) {
|
||||
updatedCategory.push(category[c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
devContact.category = updatedCategory;
|
||||
|
||||
return devContact;
|
||||
};
|
||||
|
||||
// Unlinks a FB Contact. This must only be called from the clean all process
|
||||
fb.unlinkClearAll = function(devContact) {
|
||||
fb.resetNames(devContact);
|
||||
fb.markAsUnlinked(devContact);
|
||||
};
|
||||
|
||||
// Reset givenName and familyName if it is needed after unlinking
|
||||
fb.resetNames = function resetNames(dContact) {
|
||||
if (fb.isPropagated('givenName', dContact)) {
|
||||
dContact.givenName = [''];
|
||||
fb.removePropagatedFlag('givenName', dContact);
|
||||
}
|
||||
|
||||
if (fb.isPropagated('familyName', dContact)) {
|
||||
dContact.familyName = [''];
|
||||
fb.removePropagatedFlag('familyName', dContact);
|
||||
}
|
||||
|
||||
dContact.name = [dContact.givenName[0] + ' ' + dContact.familyName[0]];
|
||||
};
|
||||
|
||||
// Marks that FB Cleaning is in progress or not
|
||||
fb.markFbCleaningInProgress = function(value) {
|
||||
utils.cookie.update({
|
||||
fbCleaningInProgress: value
|
||||
});
|
||||
};
|
||||
380
gaia/shared/js/contacts/import/facebook/fb_data.js
Executable file
380
gaia/shared/js/contacts/import/facebook/fb_data.js
Executable file
|
|
@ -0,0 +1,380 @@
|
|||
'use strict';
|
||||
|
||||
/* global LazyLoader, TelIndexer, SimplePhoneMatcher */
|
||||
|
||||
// WARNING: This file lazy loads:
|
||||
// 'shared/js/fb/fb_data_reader.js
|
||||
// 'shared/js/fb/fb_tel_index.js'
|
||||
// 'shared/js/binary_search.js'
|
||||
// 'shared/js/simple_phone_matcher.js' (will be removed once bug 938265 lands)
|
||||
|
||||
var fb = window.fb || {};
|
||||
|
||||
(function() {
|
||||
var contacts = fb.contacts || {};
|
||||
fb.contacts = contacts;
|
||||
|
||||
var Reader;
|
||||
var readerLoaded = false;
|
||||
|
||||
// Record Id for the index
|
||||
var INDEX_ID = 1;
|
||||
var isIndexDirty = false;
|
||||
var READER_LOADED_EV = 'reader_loaded';
|
||||
|
||||
var TEL_INDEXER_JS = '/shared/js/fb/fb_tel_index.js';
|
||||
var PHONE_MATCHER_JS = '/shared/js/simple_phone_matcher.js';
|
||||
var FB_READER_JS = '/shared/js/fb/fb_data_reader.js';
|
||||
var BINARY_SEARCH_JS = '/shared/js/binary_search.js';
|
||||
|
||||
contacts.UID_NOT_FOUND = 'UIDNotFound';
|
||||
contacts.ALREADY_EXISTS = 'AlreadyExists';
|
||||
|
||||
// This is needed for having proxy methods setted and ready before
|
||||
// the real reader methods (in fb_data_reader) are loaded
|
||||
if (!contacts.init) {
|
||||
var proxyMethods = ['get', 'getLength', 'getByPhone', 'search',
|
||||
'refresh', 'init', 'restart'];
|
||||
proxyMethods.forEach(function(aMethod) {
|
||||
contacts[aMethod] = defaultFunction.bind(null, aMethod);
|
||||
});
|
||||
LazyLoader.load(FB_READER_JS, onreaderLoaded);
|
||||
}
|
||||
else {
|
||||
onreaderLoaded();
|
||||
}
|
||||
|
||||
function onreaderLoaded() {
|
||||
readerLoaded = true;
|
||||
Reader = fb.contacts;
|
||||
document.dispatchEvent(new CustomEvent(READER_LOADED_EV));
|
||||
}
|
||||
|
||||
function setIndex(index) {
|
||||
Reader.dsIndex = index;
|
||||
isIndexDirty = false;
|
||||
}
|
||||
|
||||
function datastore() {
|
||||
return Reader.datastore;
|
||||
}
|
||||
|
||||
function index() {
|
||||
return Reader.dsIndex;
|
||||
}
|
||||
|
||||
function defaultFunction(target) {
|
||||
var args = [];
|
||||
for (var j = 1; j < arguments.length; j++) {
|
||||
args.push(arguments[j]);
|
||||
}
|
||||
if (!readerLoaded) {
|
||||
document.addEventListener(READER_LOADED_EV, function rd_loaded() {
|
||||
document.removeEventListener(READER_LOADED_EV, rd_loaded);
|
||||
Reader[target].apply(this, args);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// As the reader load will overwrite those functions probably this
|
||||
// will never be called
|
||||
/*jshint validthis:true */
|
||||
Reader[target].apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
function initError(outRequest, error) {
|
||||
outRequest.failed(error);
|
||||
}
|
||||
|
||||
// Ensures a proper error object is returned
|
||||
function safeError(err) {
|
||||
if (err && err.name) {
|
||||
return err;
|
||||
}
|
||||
return {
|
||||
name: 'UnknownError'
|
||||
};
|
||||
}
|
||||
|
||||
// Creates a default handler for errors
|
||||
function defaultError(request) {
|
||||
return defaultErrorCb.bind(null, request);
|
||||
}
|
||||
|
||||
// Creates a default handler for success
|
||||
function defaultSuccess(request) {
|
||||
return defaultSuccessCb.bind(null, request);
|
||||
}
|
||||
|
||||
function defaultErrorCb(request, error) {
|
||||
request.failed(safeError(error));
|
||||
}
|
||||
|
||||
function defaultSuccessCb(request, result) {
|
||||
request.done(result);
|
||||
}
|
||||
|
||||
function doSave(obj, outRequest) {
|
||||
LazyLoader.load([TEL_INDEXER_JS, PHONE_MATCHER_JS,
|
||||
BINARY_SEARCH_JS] , function() {
|
||||
var uid = obj.uid;
|
||||
datastore().add(obj, uid).then(function success() {
|
||||
indexByPhone(obj, uid);
|
||||
isIndexDirty = true;
|
||||
outRequest.done();
|
||||
}, function error(err) {
|
||||
if (err.name === 'ConstraintError') {
|
||||
err = {
|
||||
name: contacts.ALREADY_EXISTS
|
||||
};
|
||||
}
|
||||
outRequest.failed(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function indexByPhone(obj, newId) {
|
||||
// Update index by tel
|
||||
// As this is populated by FB importer we don't need to have
|
||||
// extra checks
|
||||
if (Array.isArray(obj.tel)) {
|
||||
obj.tel.forEach(function(aTel) {
|
||||
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
|
||||
|
||||
variants.forEach(function(aVariant) {
|
||||
index().byTel[aVariant] = newId;
|
||||
});
|
||||
// To avoid the '+' char
|
||||
TelIndexer.index(index().treeTel, aTel.value.substring(1), newId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function reIndexByPhone(oldObj, newObj, dsId) {
|
||||
removePhoneIndex(oldObj);
|
||||
indexByPhone(newObj, dsId);
|
||||
TelIndexer.orderTree(index().treeTel);
|
||||
}
|
||||
|
||||
function removePhoneIndex(deletedFriend) {
|
||||
// Need to update the tel indexes
|
||||
if (Array.isArray(deletedFriend.tel)) {
|
||||
deletedFriend.tel.forEach(function(aTel) {
|
||||
TelIndexer.remove(index().treeTel, aTel.value.substring(1));
|
||||
|
||||
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
|
||||
variants.forEach(function(aVariant) {
|
||||
delete index().byTel[aVariant];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to save FB Friend Information
|
||||
*
|
||||
*/
|
||||
contacts.save = function(obj) {
|
||||
var retRequest = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function save() {
|
||||
contacts.init(function() {
|
||||
doSave(obj, retRequest);
|
||||
},
|
||||
function(err) {
|
||||
initError(retRequest, err);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return retRequest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows to update FB Friend Information
|
||||
*
|
||||
*
|
||||
*/
|
||||
contacts.update = function(obj) {
|
||||
var retRequest = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function save() {
|
||||
contacts.init(function() {
|
||||
doUpdate(obj, retRequest);
|
||||
},
|
||||
function(err) {
|
||||
initError(retRequest, err);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return retRequest;
|
||||
};
|
||||
|
||||
function doUpdate(obj, outRequest) {
|
||||
LazyLoader.load([TEL_INDEXER_JS, PHONE_MATCHER_JS,
|
||||
BINARY_SEARCH_JS], function() {
|
||||
var uid = obj.uid;
|
||||
|
||||
var successCb = successUpdate.bind(null, outRequest);
|
||||
var errorCb = errorUpdate.bind(null, outRequest, uid);
|
||||
|
||||
// It is necessary to get the old object and delete old indexes
|
||||
datastore().get(uid).then(function success(oldObj) {
|
||||
if (!oldObj) {
|
||||
errorCb({
|
||||
name: contacts.UID_NOT_FOUND
|
||||
});
|
||||
return;
|
||||
}
|
||||
reIndexByPhone(oldObj, obj, uid);
|
||||
datastore().put(obj, uid).then(function success() {
|
||||
return datastore().put(index(), INDEX_ID);
|
||||
}, errorCb).then(successCb, errorCb);
|
||||
}, errorCb); // datastore.get
|
||||
});
|
||||
}
|
||||
|
||||
function successUpdate(outRequest) {
|
||||
outRequest.done();
|
||||
}
|
||||
|
||||
function errorUpdate(outRequest, uid, error) {
|
||||
window.console.error('Error while updating datastore for: ', uid);
|
||||
outRequest.failed(error);
|
||||
}
|
||||
|
||||
function doRemove(uid, outRequest, forceFlush) {
|
||||
LazyLoader.load([TEL_INDEXER_JS, PHONE_MATCHER_JS,
|
||||
BINARY_SEARCH_JS], function() {
|
||||
var errorCb = errorRemove.bind(null, outRequest, uid);
|
||||
var objToDelete;
|
||||
|
||||
datastore().get(uid).then(function success_get_remove(object) {
|
||||
objToDelete = object;
|
||||
if (!objToDelete) {
|
||||
errorRemove(outRequest, uid, {
|
||||
name: contacts.UID_NOT_FOUND
|
||||
});
|
||||
return;
|
||||
}
|
||||
datastore().remove(uid).then(function success_rm(removed) {
|
||||
successRemove(outRequest, objToDelete, forceFlush, removed);
|
||||
}, errorCb);
|
||||
}, errorCb);
|
||||
}); // LazyLoader.load
|
||||
}
|
||||
|
||||
// Needs to update the index data conveniently
|
||||
function successRemove(outRequest, deletedFriend, forceFlush, removed) {
|
||||
if (removed === true) {
|
||||
isIndexDirty = true;
|
||||
|
||||
removePhoneIndex(deletedFriend);
|
||||
if (forceFlush) {
|
||||
var flushReq = fb.contacts.flush();
|
||||
|
||||
flushReq.onsuccess = function() {
|
||||
isIndexDirty = false;
|
||||
outRequest.done(true);
|
||||
};
|
||||
flushReq.onerror = function() {
|
||||
outRequest.failed(flushReq.error);
|
||||
};
|
||||
}
|
||||
else {
|
||||
outRequest.done(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
outRequest.done(false);
|
||||
}
|
||||
}
|
||||
|
||||
function errorRemove(outRequest, uid, error) {
|
||||
error = safeError(error);
|
||||
window.console.error('FB Data: Error while removing ', uid, ': ',
|
||||
error.name);
|
||||
outRequest.failed(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to remove FB contact from the DB
|
||||
*
|
||||
*/
|
||||
contacts.remove = function(uid, flush) {
|
||||
var hasToFlush = (flush === true ? flush : false);
|
||||
var retRequest = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function remove() {
|
||||
contacts.init(function() {
|
||||
doRemove(uid, retRequest, hasToFlush);
|
||||
},
|
||||
function(err) {
|
||||
initError(retRequest, err);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return retRequest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all the FB Friends and the index
|
||||
*
|
||||
* The index is restored as empty
|
||||
*
|
||||
*/
|
||||
contacts.clear = function() {
|
||||
var outRequest = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function clear() {
|
||||
contacts.init(function() {
|
||||
doClear(outRequest);
|
||||
},
|
||||
function(err) {
|
||||
initError(outRequest, err);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return outRequest;
|
||||
};
|
||||
|
||||
function doClear(outRequest) {
|
||||
datastore().clear().then(function success() {
|
||||
setIndex(null);
|
||||
// TODO:
|
||||
// This is working but there are open questions on the mailing list
|
||||
datastore().put(index(), INDEX_ID).then(defaultSuccess(outRequest),
|
||||
function error(err) {
|
||||
err = safeError(err);
|
||||
window.console.error('Error while re-creating the index: ',
|
||||
err.name);
|
||||
outRequest.failed(err);
|
||||
}
|
||||
);
|
||||
}, defaultError(outRequest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the index on the datastore
|
||||
*
|
||||
*/
|
||||
contacts.flush = function() {
|
||||
var outRequest = new fb.utils.Request();
|
||||
|
||||
window.setTimeout(function do_Flush() {
|
||||
if (!(datastore()) || !isIndexDirty) {
|
||||
window.console.warn(
|
||||
'The datastore has not been initialized or is not dirty');
|
||||
outRequest.done();
|
||||
return;
|
||||
}
|
||||
|
||||
TelIndexer.orderTree(index().treeTel);
|
||||
datastore().put(index(), INDEX_ID).then(
|
||||
defaultSuccess(outRequest),
|
||||
defaultError(outRequest));
|
||||
}, 0);
|
||||
|
||||
return outRequest;
|
||||
};
|
||||
|
||||
})();
|
||||
163
gaia/shared/js/contacts/import/facebook/fb_query.js
Executable file
163
gaia/shared/js/contacts/import/facebook/fb_query.js
Executable file
|
|
@ -0,0 +1,163 @@
|
|||
'use strict';
|
||||
|
||||
/* global fb */
|
||||
|
||||
/* jshint ignore:start */
|
||||
var fb = this.fb || {};
|
||||
|
||||
// In an only worker execution context this would not be necessary
|
||||
var self = this;
|
||||
fb.utils = this.fb.utils || {};
|
||||
/* jshint ignore:end */
|
||||
|
||||
fb.DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
// Runs a query against Facebook FQL. Callback is a string!!
|
||||
fb.utils.runQuery = function(query, callback, access_token) {
|
||||
// Auxiliary function for canceling a request
|
||||
function QueryRequest() {
|
||||
this.cancel = function() {
|
||||
if (typeof this.oncancel === 'function') {
|
||||
window.setTimeout(function() {
|
||||
this.oncancel();
|
||||
}.bind(this), 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var outReq = new QueryRequest();
|
||||
|
||||
var queryService = 'https://graph.facebook.com/fql?q=';
|
||||
queryService += encodeURIComponent(query);
|
||||
|
||||
var params = ['access_token' + '=' + access_token,
|
||||
'format=json'];
|
||||
|
||||
var queryParams = params.join('&');
|
||||
|
||||
var remote = queryService + '&' + queryParams;
|
||||
|
||||
var xhr = new XMLHttpRequest({
|
||||
mozSystem: true
|
||||
});
|
||||
|
||||
// To enable xhr.abort if user cancels
|
||||
outReq.xhr = xhr;
|
||||
outReq.oncancel = function() {
|
||||
this.xhr.abort();
|
||||
};
|
||||
|
||||
xhr.open('GET', remote, true);
|
||||
xhr.responseType = 'json';
|
||||
|
||||
xhr.timeout = fb.operationsTimeout || fb.DEFAULT_TIMEOUT;
|
||||
|
||||
xhr.onload = function(e) {
|
||||
if (xhr.status === 200 || xhr.status === 400 || xhr.status === 0) {
|
||||
if (callback && typeof callback.success === 'function') {
|
||||
self.setTimeout(function() {
|
||||
callback.success(xhr.response);
|
||||
},0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.console.error('FB: HTTP error executing query. ',
|
||||
query, ' Status: ', xhr.status);
|
||||
if (callback && typeof callback.error === 'function') {
|
||||
self.setTimeout(callback.error, 0);
|
||||
}
|
||||
}
|
||||
}; // onload
|
||||
|
||||
xhr.ontimeout = function(e) {
|
||||
self.console.error('FB: Timeout!!! while executing query', query);
|
||||
if (callback && typeof callback.timeout === 'function') {
|
||||
self.setTimeout(callback.timeout, 0);
|
||||
}
|
||||
}; // ontimeout
|
||||
|
||||
xhr.onerror = function(e) {
|
||||
self.console.error('FB: Error while executing query: ', query,
|
||||
': ', e);
|
||||
if (callback && typeof callback.error === 'function') {
|
||||
self.setTimeout(function() {
|
||||
callback.error(e);
|
||||
},0);
|
||||
}
|
||||
}; // onerror
|
||||
|
||||
xhr.send();
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtains a img DOM Element with the Contact's img
|
||||
*
|
||||
*/
|
||||
fb.utils.getFriendPicture = function(uid, callback, access_token,
|
||||
targetPictureSize) {
|
||||
// Access token is necessary just in case the image is not public
|
||||
// When passing an access token to FB https must be used
|
||||
|
||||
var imgSrc = 'https://graph.facebook.com/' + uid + '/picture?';
|
||||
|
||||
var params = [
|
||||
'width=' + targetPictureSize,
|
||||
'access_token=' + access_token,
|
||||
't=' + Date.now()
|
||||
];
|
||||
|
||||
var imgService = imgSrc + params.join('&');
|
||||
|
||||
var xhr = new XMLHttpRequest({
|
||||
mozSystem: true
|
||||
});
|
||||
xhr.open('GET', imgService, true);
|
||||
xhr.responseType = 'blob';
|
||||
|
||||
xhr.timeout = fb.operationsTimeout || fb.DEFAULT_TIMEOUT;
|
||||
|
||||
xhr.onload = function(e) {
|
||||
if (xhr.status === 200 || xhr.status === 0) {
|
||||
var mblob = e.target.response;
|
||||
if (typeof callback === 'function') {
|
||||
self.setTimeout(function() {
|
||||
callback(mblob);
|
||||
},0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.console.error('FB: HTTP error retrieving img for uid: ',
|
||||
uid, ' Status: ', xhr.status);
|
||||
if (typeof callback === 'function') {
|
||||
self.setTimeout(function() {
|
||||
callback(null);
|
||||
},0);
|
||||
}
|
||||
}
|
||||
}; // onload
|
||||
|
||||
xhr.ontimeout = function(e) {
|
||||
self.console.error('FB: Timeout!!! while retrieving img for uid: ', uid);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
self.setTimeout(function() {
|
||||
callback(null);
|
||||
},0);
|
||||
}
|
||||
}; // ontimeout
|
||||
|
||||
xhr.onerror = function(e) {
|
||||
self.console.error('FB: Error while retrieving img for uid: ', uid,
|
||||
'Error: ', e);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
self.setTimeout(function() {
|
||||
callback(null);
|
||||
},0);
|
||||
}
|
||||
}; // onerror
|
||||
|
||||
xhr.send();
|
||||
};
|
||||
526
gaia/shared/js/contacts/import/facebook/fb_utils.js
Executable file
526
gaia/shared/js/contacts/import/facebook/fb_utils.js
Executable file
|
|
@ -0,0 +1,526 @@
|
|||
'use strict';
|
||||
|
||||
/* global ImportStatusData, oauthflow, utils */
|
||||
/* Ignoring function declaration in loops */
|
||||
/* jshint -W083 */
|
||||
|
||||
var fb = window.fb || {};
|
||||
window.fb = fb;
|
||||
|
||||
(function(document) {
|
||||
var Utils = fb.utils || {};
|
||||
fb.utils = Utils;
|
||||
|
||||
var FRIEND_COUNT_QUERY = 'select friend_count from user where uid=me()';
|
||||
|
||||
var CACHE_FRIENDS_KEY = Utils.CACHE_FRIENDS_KEY = 'numFacebookFriends';
|
||||
var LAST_UPDATED_KEY = Utils.LAST_UPDATED_KEY = 'lastUpdatedTime';
|
||||
Utils.ALARM_ID_KEY = 'nextAlarmId';
|
||||
Utils.SCHEDULE_SYNC_KEY = 'facebookShouldHaveScheduledAt';
|
||||
|
||||
var REDIRECT_LOGOUT_URI = window.oauthflow ?
|
||||
oauthflow.params.facebook.redirectLogout : '';
|
||||
var STORAGE_KEY = Utils.TOKEN_DATA_KEY = 'tokenData';
|
||||
|
||||
// For controlling data synchronization
|
||||
Utils.setLastUpdate = function(value, cb) {
|
||||
window.asyncStorage.setItem(LAST_UPDATED_KEY, {
|
||||
data: value
|
||||
}, cb);
|
||||
};
|
||||
|
||||
Utils.getLastUpdate = function(callback) {
|
||||
window.asyncStorage.getItem(LAST_UPDATED_KEY, function(obj) {
|
||||
var out = 0;
|
||||
if (obj) {
|
||||
out = obj.data || out;
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback(out);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Utils.getNonCacheableUrl = function(url) {
|
||||
if (url.indexOf('?') === -1) {
|
||||
url += '?';
|
||||
}
|
||||
|
||||
return url + '&burst_cache=' + Date.now();
|
||||
};
|
||||
|
||||
|
||||
Utils.getContactData = function(cid) {
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
var req = navigator.mozContacts.find({
|
||||
filterBy: ['id'],
|
||||
filterValue: cid,
|
||||
filterOp: 'equals'
|
||||
});
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
if (e.target.result && e.target.result.length > 0) {
|
||||
outReq.done(e.target.result[0]);
|
||||
}
|
||||
else {
|
||||
outReq.done(null);
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function(e) {
|
||||
outReq.failed(e.target.error);
|
||||
};
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
// Returns the mozContact associated to a UID in FB
|
||||
Utils.getMozContact = function(uid) {
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
window.setTimeout(function get_mozContact_ByUid() {
|
||||
fb.getMozContactByUid(uid,
|
||||
function onsuccess(result) {
|
||||
if (Array.isArray(result) && result.length > 0) {
|
||||
outReq.done(result[0]);
|
||||
} else {
|
||||
outReq.done(null);
|
||||
}
|
||||
},
|
||||
function onerror(e) {
|
||||
outReq.failed(e.target.error);
|
||||
}
|
||||
);
|
||||
}, 0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
// Returns the number of mozContacts associated to a UID in FB
|
||||
Utils.getNumberMozContacts = function(uid) {
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
window.setTimeout(function get_mozContact_ByUid() {
|
||||
fb.getMozContactByUid(uid,
|
||||
function onsuccess(result) {
|
||||
if (Array.isArray(result)) {
|
||||
outReq.done(result.length);
|
||||
} else {
|
||||
outReq.done(0);
|
||||
}
|
||||
},
|
||||
function onerror(error) {
|
||||
outReq.failed(error);
|
||||
}
|
||||
);
|
||||
},0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
Utils.getAllFbContacts = function() {
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
window.setTimeout(function get_all_fb_contacts() {
|
||||
var filter = {
|
||||
filterValue: fb.CATEGORY,
|
||||
filterOp: 'contains',
|
||||
filterBy: ['category']
|
||||
};
|
||||
|
||||
var req = navigator.mozContacts.find(filter);
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
outReq.done(e.target.result);
|
||||
};
|
||||
|
||||
req.onerror = function(e) {
|
||||
outReq.failed(e.target.error);
|
||||
};
|
||||
}, 0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
// On the device
|
||||
Utils.getNumFbContacts = function() {
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
window.setTimeout(function get_num_fb_contacts() {
|
||||
var req = fb.contacts.getLength();
|
||||
|
||||
req.onsuccess = function() {
|
||||
outReq.done(req.result);
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
outReq.failed(req.error);
|
||||
};
|
||||
}, 0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
// Requests the number remotely
|
||||
Utils.getNumFbFriends = function(callback, access_token) {
|
||||
fb.utils.runQuery(FRIEND_COUNT_QUERY, callback, access_token);
|
||||
};
|
||||
|
||||
Utils.getCachedAccessToken = function(callback) {
|
||||
ImportStatusData.get(STORAGE_KEY).then(function(data) {
|
||||
var out = null;
|
||||
|
||||
if (data) {
|
||||
out = data.access_token || null;
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(out);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
Utils.setCachedAccessToken = function(data, callback) {
|
||||
ImportStatusData.put(STORAGE_KEY, data).then(function done() {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}, function error() {
|
||||
console.error('Error while saving access token');
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Obtains the number locally
|
||||
Utils.getCachedNumFbFriends = function(callback) {
|
||||
ImportStatusData.get(CACHE_FRIENDS_KEY).then(function(data) {
|
||||
if (typeof callback === 'function' && typeof data === 'number') {
|
||||
callback(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Utils.setCachedNumFriends = function(value, cb) {
|
||||
ImportStatusData.put(CACHE_FRIENDS_KEY, value).then(cb);
|
||||
};
|
||||
|
||||
// Removes cached number of friends from datastore to keep state of sync.
|
||||
Utils.removeCachedNumFriends = function(callback) {
|
||||
ImportStatusData.remove(CACHE_FRIENDS_KEY).then(function() {
|
||||
typeof callback === 'function' && callback();
|
||||
});
|
||||
};
|
||||
|
||||
Utils.getImportChecked = function(callback) {
|
||||
// If we have an access token Import should be checked
|
||||
Utils.getCachedAccessToken(function(access_token) {
|
||||
var out = 'logged-out';
|
||||
|
||||
if (access_token) {
|
||||
out = 'logged-in';
|
||||
}
|
||||
else {
|
||||
// In this case is needed to know whether the access_token has
|
||||
// been invalidated
|
||||
Utils.getCachedNumFbFriends(function(value) {
|
||||
if (value) {
|
||||
out = 'renew-pwd';
|
||||
if (typeof callback === 'function') {
|
||||
callback(out);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(out);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Obtains the number locally (cached) and tries to get them remotely
|
||||
Utils.numFbFriendsData = function(callback) {
|
||||
var localCb = callback.local;
|
||||
var remoteCb = callback.remote;
|
||||
|
||||
Utils.getCachedNumFbFriends(localCb);
|
||||
|
||||
function auxCallback(response) {
|
||||
if (response.data && response.data[0] &&
|
||||
response.data[0].friend_count) {
|
||||
remoteCb(response.data[0].friend_count);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof remoteCb === 'function' && navigator.onLine === true) {
|
||||
var remoteCallbacks = {
|
||||
success: auxCallback,
|
||||
error: null,
|
||||
timeout: null
|
||||
};
|
||||
Utils.getCachedAccessToken(function(access_token) {
|
||||
if (access_token) {
|
||||
Utils.getNumFbFriends(remoteCallbacks, access_token);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Clears all Fb data (use with caution!!)
|
||||
Utils.clearFbData = function() {
|
||||
// First step all Contacts which are FB Friends are obtained
|
||||
// then those not linked are directly removed
|
||||
// Those linked are unlinked
|
||||
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
window.setTimeout(function do_clearFbData() {
|
||||
// First a clear request is issued
|
||||
var ireq = fb.contacts.clear();
|
||||
|
||||
ireq.onsuccess = function() {
|
||||
var req = Utils.getAllFbContacts();
|
||||
|
||||
req.onsuccess = function() {
|
||||
var cleaner = new Utils.FbContactsCleaner(req.result, 'clear');
|
||||
// And now success notification is sent
|
||||
outReq.done(cleaner);
|
||||
// The cleaning activity should be starting immediately
|
||||
window.setTimeout(cleaner.start, 0);
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('FB Clean. Error retrieving FB Contacts');
|
||||
outReq.failed(req.error);
|
||||
};
|
||||
};
|
||||
|
||||
ireq.onerror = function(e) {
|
||||
window.console.error('Error while clearing the FB Cache');
|
||||
outReq.failed(ireq.error);
|
||||
};
|
||||
|
||||
},0);
|
||||
|
||||
return outReq;
|
||||
};
|
||||
|
||||
Utils.logout = function() {
|
||||
var outReq = new Utils.Request();
|
||||
|
||||
// Duration of the timer that checks window close by the user (1/2 sec)
|
||||
var WINDOW_TIMER_INTERVAL = 500;
|
||||
// Maximum time we are going to wait for a logout operation (5 secs)
|
||||
var LOGOUT_TIMEOUT = 5000;
|
||||
var MAX_TIMER_TICKS = LOGOUT_TIMEOUT / WINDOW_TIMER_INTERVAL;
|
||||
|
||||
window.setTimeout(function do_logout() {
|
||||
Utils.getCachedAccessToken(function getAccessToken(access_token) {
|
||||
if (access_token) {
|
||||
var logoutService = 'https://www.facebook.com/logout.php?';
|
||||
var params = [
|
||||
'next' + '=' + encodeURIComponent(REDIRECT_LOGOUT_URI),
|
||||
'access_token' + '=' + access_token
|
||||
];
|
||||
|
||||
var logoutParams = params.join('&');
|
||||
var logoutUrl = logoutService + logoutParams;
|
||||
var loggedOut = false;
|
||||
|
||||
var m_listen = function(e) {
|
||||
if (e.origin !== fb.CONTACTS_APP_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
if (e.data === 'closed') {
|
||||
loggedOut = true;
|
||||
ImportStatusData.remove(STORAGE_KEY);
|
||||
outReq.done();
|
||||
}
|
||||
e.stopImmediatePropagation();
|
||||
window.removeEventListener('message', m_listen);
|
||||
};
|
||||
|
||||
window.addEventListener('message', m_listen);
|
||||
|
||||
var logoutWindow = window.open(logoutUrl, '', 'dialog');
|
||||
// This timer is run in order to monitor whether the window is
|
||||
// closed by the user during the logout process (bug 1084224).
|
||||
// This will have to be removed once bug 966216 is fixed
|
||||
var timerTicks = 0;
|
||||
var timerId = window.setInterval(function() {
|
||||
timerTicks++;
|
||||
var closed = (logoutWindow.closed === true);
|
||||
var timedOut = (timerTicks >= MAX_TIMER_TICKS);
|
||||
|
||||
if (loggedOut || closed || timedOut) {
|
||||
window.clearInterval(timerId);
|
||||
window.removeEventListener('message', m_listen);
|
||||
}
|
||||
|
||||
if (closed && !loggedOut) {
|
||||
outReq.failed('UserCancelled');
|
||||
}
|
||||
else if (timedOut && !loggedOut) {
|
||||
outReq.failed('Timeout');
|
||||
}
|
||||
}, WINDOW_TIMER_INTERVAL);
|
||||
} // if
|
||||
else {
|
||||
outReq.done();
|
||||
}
|
||||
}); // cachedToken
|
||||
}, 0); // setTimeout
|
||||
|
||||
return outReq;
|
||||
|
||||
}; // logout
|
||||
|
||||
|
||||
// FbContactsCleaner Object
|
||||
// Mode can be 'update' or 'cleanAll'
|
||||
Utils.FbContactsCleaner = function(contacts, pmode) {
|
||||
this.lcontacts = contacts;
|
||||
var total = contacts.length;
|
||||
var next = 0;
|
||||
var self = this;
|
||||
var CHUNK_SIZE = 5;
|
||||
var numResponses = 0;
|
||||
var mustUpdate = (pmode === 'update');
|
||||
var notifyClean = false;
|
||||
|
||||
var mustHold = false;
|
||||
var holded = false;
|
||||
var mustFinish = false;
|
||||
|
||||
this.start = function() {
|
||||
mustHold = holded = mustFinish = false;
|
||||
|
||||
if (total > 0) {
|
||||
cleanContacts(0);
|
||||
}
|
||||
else if (typeof self.onsuccess === 'function') {
|
||||
window.setTimeout(self.onsuccess);
|
||||
}
|
||||
};
|
||||
|
||||
this.hold = function() {
|
||||
mustHold = true;
|
||||
};
|
||||
|
||||
this.finish = function() {
|
||||
mustFinish = true;
|
||||
|
||||
if (holded) {
|
||||
notifySuccess();
|
||||
}
|
||||
};
|
||||
|
||||
this.resume = function() {
|
||||
mustHold = holded = mustFinish = false;
|
||||
|
||||
window.setTimeout(function resume_clean() {
|
||||
cleanContacts(next);
|
||||
});
|
||||
};
|
||||
|
||||
function successHandler(e) {
|
||||
if (notifyClean || typeof self.oncleaned === 'function') {
|
||||
notifyClean = true;
|
||||
// Avoiding race condition so the cleaned element is cached
|
||||
var cleaned = e.target.number;
|
||||
window.setTimeout(function() {
|
||||
self.oncleaned(cleaned);
|
||||
},0);
|
||||
}
|
||||
continueCb();
|
||||
}
|
||||
|
||||
function errorHandler(contactid, error) {
|
||||
if (typeof self.onerror === 'function') {
|
||||
self.onerror(contactid, error);
|
||||
}
|
||||
|
||||
continueCb();
|
||||
}
|
||||
|
||||
function cleanContacts(from) {
|
||||
for (var idx = from; idx < (from + CHUNK_SIZE) && idx < total; idx++) {
|
||||
var contact = contacts[idx];
|
||||
var number = idx;
|
||||
var req, fbContact;
|
||||
if (fb.isFbLinked(contact)) {
|
||||
if (mustUpdate) {
|
||||
fbContact = new fb.Contact(contact);
|
||||
req = fbContact.unlink('hard');
|
||||
}
|
||||
else {
|
||||
// Here we only need to mark this contact as unlinked
|
||||
// as the FB Data would have been already removed
|
||||
fb.unlinkClearAll(contact);
|
||||
req = navigator.mozContacts.save(
|
||||
utils.misc.toMozContact(contact));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (mustUpdate) {
|
||||
fbContact = new fb.Contact(contact);
|
||||
req = fbContact.remove();
|
||||
}
|
||||
else {
|
||||
req = navigator.mozContacts.remove(
|
||||
utils.misc.toMozContact(contact));
|
||||
}
|
||||
}
|
||||
req.number = number;
|
||||
req.onsuccess = successHandler;
|
||||
req.onerror = function(e) {
|
||||
errorHandler(contact.id, e.target.error);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function notifySuccess() {
|
||||
if (typeof self.onsuccess === 'function') {
|
||||
window.setTimeout(self.onsuccess);
|
||||
}
|
||||
}
|
||||
|
||||
function finishHandler() {
|
||||
// It is needed to flush in order to properly update the index
|
||||
var req = fb.contacts.flush();
|
||||
req.onsuccess = notifySuccess;
|
||||
req.onerror = function cleaner_flushError() {
|
||||
errorHandler(null, req.error);
|
||||
};
|
||||
}
|
||||
|
||||
function continueCb() {
|
||||
next++;
|
||||
numResponses++;
|
||||
if (next < total && numResponses === CHUNK_SIZE) {
|
||||
numResponses = 0;
|
||||
if (!mustHold && !mustFinish) {
|
||||
cleanContacts(next);
|
||||
}
|
||||
else if (mustFinish && !holded) {
|
||||
finishHandler();
|
||||
}
|
||||
|
||||
if (mustHold) {
|
||||
holded = true;
|
||||
}
|
||||
}
|
||||
else if (next >= total) {
|
||||
// End has been reached
|
||||
finishHandler();
|
||||
}
|
||||
} // function
|
||||
}; // FbContactsCleaner
|
||||
|
||||
})(document);
|
||||
223
gaia/shared/js/contacts/import/friends_list.js
Executable file
223
gaia/shared/js/contacts/import/friends_list.js
Executable file
|
|
@ -0,0 +1,223 @@
|
|||
'use strict';
|
||||
|
||||
/* global Normalizer, utils */
|
||||
/* exported FriendListRenderer */
|
||||
|
||||
var FriendListRenderer = (function() {
|
||||
|
||||
// Order criteria
|
||||
var orderCriteria = {
|
||||
firstName: ['givenName', 'familyName', 'email1'],
|
||||
lastName: ['familyName', 'givenName', 'email1']
|
||||
};
|
||||
|
||||
var HEADER_LETTERS = [
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ', // Roman
|
||||
'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ' , // Greek
|
||||
'АБВГДЂЕЁЖЗИЙЈКЛЉМНЊОПРСТЋУФХЦЧЏШЩЭЮЯ' // Cyrillic (Russian + Serbian)
|
||||
].join('');
|
||||
|
||||
// Options by default
|
||||
var defaults = {
|
||||
container: '#groups-list',
|
||||
orderBy: 'lastName'
|
||||
};
|
||||
|
||||
// Callback to notify when finish
|
||||
var finishCb;
|
||||
var totalContacts;
|
||||
var lock;
|
||||
var CHUNK_SIZE = 10;
|
||||
|
||||
var render = function render(contacts, cb, options) {
|
||||
// Populate defaults
|
||||
for (var key in defaults) {
|
||||
if (typeof options[key] === 'undefined') {
|
||||
options[key] = defaults[key];
|
||||
}
|
||||
}
|
||||
|
||||
finishCb = cb;
|
||||
totalContacts = contacts.length;
|
||||
|
||||
lock = navigator.requestWakeLock('cpu');
|
||||
|
||||
doRender(contacts, cb, options);
|
||||
};
|
||||
|
||||
var doRender = function doRender(contacts, cb, options) {
|
||||
var groupsList = document.querySelector(options.container);
|
||||
var orderBy = options.orderBy;
|
||||
var order = orderCriteria[orderBy];
|
||||
|
||||
// Sorting friend list
|
||||
contacts.sort(function(a, b) {
|
||||
return getStringToBeOrdered(a, order).localeCompare(
|
||||
getStringToBeOrdered(b, order));
|
||||
});
|
||||
|
||||
// Hash containing each group
|
||||
var groups = {};
|
||||
|
||||
contacts.forEach(function(contact) {
|
||||
// Contacts are ordered so it is pretty easy to group them
|
||||
var groupName = getGroupName(contact, order);
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = [];
|
||||
}
|
||||
// An array per group
|
||||
groups[groupName].push(contact);
|
||||
});
|
||||
|
||||
// We are going to delete the paragraph that paints the other order for the
|
||||
// contact's name from template...
|
||||
var notRenderedParagraph = groupsList.querySelector('[data-order-by="' +
|
||||
(orderBy === 'firstName' ? 'lastName' : 'firstName') + '"]');
|
||||
if (notRenderedParagraph) {
|
||||
notRenderedParagraph.parentNode.removeChild(notRenderedParagraph);
|
||||
}
|
||||
// Start first element of HEADER_LETTERS (A)
|
||||
var letterStart = 0;
|
||||
doRenderGroupChunk(letterStart, HEADER_LETTERS[letterStart],
|
||||
groupsList, groups);
|
||||
};
|
||||
|
||||
// Controls group rendering one by one
|
||||
function doRenderGroupChunk(index, group, groupsList, groups) {
|
||||
renderGroup(groupsList, group, groups[group], function(fragment) {
|
||||
if (fragment) {
|
||||
groupsList.appendChild(fragment);
|
||||
}
|
||||
fragment = null;
|
||||
|
||||
var headerLettersLength = HEADER_LETTERS.length;
|
||||
if (index + 1 <= headerLettersLength) {
|
||||
window.setTimeout(function renderNextGroup() {
|
||||
doRenderGroupChunk(index + 1, HEADER_LETTERS[index + 1],
|
||||
groupsList, groups);
|
||||
});
|
||||
}
|
||||
else if (group != '#') {
|
||||
window.setTimeout(function renderNextGroup() {
|
||||
doRenderGroupChunk(index + 1, '#', groupsList, groups);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Deleting template
|
||||
groupsList.removeChild(groupsList.firstElementChild);
|
||||
|
||||
if (typeof finishCb === 'function') {
|
||||
// We wait a delay depending on number of nodes
|
||||
// Afterwards the curtain will be displayed
|
||||
window.setTimeout(finishCb, totalContacts * 2);
|
||||
}
|
||||
if (lock) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Renders the items in a group in chunks
|
||||
function doRenderGroupItems(from, groupsList, group, friends, element, cb) {
|
||||
var end = from + CHUNK_SIZE;
|
||||
|
||||
// This is the <ol> and <header> is children[0]
|
||||
var list = element.children[1];
|
||||
|
||||
for (var i = from; i < end && i < friends.length; i++) {
|
||||
var friend = friends[i];
|
||||
|
||||
if (friend.search && friend.search.length > 0) {
|
||||
// Set the picture size
|
||||
var box = utils.misc.getPreferredPictureBox();
|
||||
friend.picwidth = box.width;
|
||||
friend.picheight = box.height;
|
||||
|
||||
friend.search = Normalizer.toAscii(friend.search);
|
||||
|
||||
// New friend appended
|
||||
utils.templates.append(list, friend);
|
||||
}
|
||||
}
|
||||
|
||||
if (i < friends.length) {
|
||||
window.setTimeout(function renderNextChunk() {
|
||||
doRenderGroupItems(end, groupsList, group, friends, element, cb);
|
||||
});
|
||||
}
|
||||
else {
|
||||
list.removeChild(list.firstElementChild);
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
// Renders a group
|
||||
function renderGroup(groupsList, group, friends, cb) {
|
||||
if (!friends || friends.length === 0) {
|
||||
window.setTimeout(cb);
|
||||
return;
|
||||
}
|
||||
|
||||
// Document fragment that will hold the group nodes
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
// New element appended
|
||||
var element = utils.templates.append(groupsList, {
|
||||
group: group
|
||||
}, fragment);
|
||||
|
||||
// For each friend in the group
|
||||
doRenderGroupItems(0, groupsList, group, friends, element, function() {
|
||||
cb(fragment);
|
||||
});
|
||||
}
|
||||
|
||||
function getStringToBeOrdered(contact, order) {
|
||||
var ret = contact.search;
|
||||
|
||||
if (!ret) {
|
||||
ret = [];
|
||||
|
||||
order.forEach(function(field) {
|
||||
ret.push(getValue(contact, field));
|
||||
});
|
||||
|
||||
ret = contact.search = ret.join(' ');
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getValue(contact, field) {
|
||||
var out = contact[field];
|
||||
|
||||
if (out) {
|
||||
out = Array.isArray(out) ? out[0] : out;
|
||||
} else {
|
||||
out = '';
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getGroupName(contact, order) {
|
||||
var ret = getStringToBeOrdered(contact, order);
|
||||
|
||||
ret = ret.charAt(0).toUpperCase();
|
||||
ret = ret.replace(/[ÁÀ]/ig, 'A');
|
||||
ret = ret.replace(/[ÉÈ]/ig, 'E');
|
||||
ret = ret.replace(/[ÍÌ]/ig, 'I');
|
||||
ret = ret.replace(/[ÓÒ]/ig, 'O');
|
||||
ret = ret.replace(/[ÚÙ]/ig, 'U');
|
||||
|
||||
if (HEADER_LETTERS.indexOf(ret) < 0) {
|
||||
ret = '#';
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return {
|
||||
'render': render
|
||||
};
|
||||
})();
|
||||
475
gaia/shared/js/contacts/import/gmail/gmail_connector.js
Executable file
475
gaia/shared/js/contacts/import/gmail/gmail_connector.js
Executable file
|
|
@ -0,0 +1,475 @@
|
|||
/* globals Rest */
|
||||
|
||||
/* exported GmailConnector */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
Gmail Contacts connector
|
||||
|
||||
Provides the capabilities to connect to the Gmail Contacts service
|
||||
and return the contacts on a readable and importable format.
|
||||
*/
|
||||
var GmailConnector = (function GmailConnector() {
|
||||
|
||||
// Google contacts service end point,
|
||||
// force a huge number of contacts to not paginate :S
|
||||
var END_POINT =
|
||||
'https://www.google.com/m8/feeds/contacts/default/full/?max-results=10000';
|
||||
var GROUPS_END_POINT =
|
||||
'https://www.google.com/m8/feeds/groups/default/full/';
|
||||
var EXTRA_HEADERS = {
|
||||
'GData-Version': '3.0'
|
||||
};
|
||||
var GD_NAMESPACE = 'http://schemas.google.com/g/2005';
|
||||
|
||||
var CATEGORY = 'gmail';
|
||||
var URN_IDENTIFIER = 'urn:service:gmail:uid:';
|
||||
|
||||
// Will be used as a cache for the thumbnail url for each contact
|
||||
var photoUrls = {};
|
||||
|
||||
// In some cases we will need the access token, cache a copy
|
||||
var accessToken = null;
|
||||
|
||||
// We have a xml response from Google, all the entries in an array,
|
||||
// no matter if they are xml entries.
|
||||
// Despite of being a node, inject a 'uid' as this will be necessary
|
||||
// for the selection
|
||||
var nodeListToArray = function nodeListToArray(response) {
|
||||
var entries = response.getElementsByTagName('entry');
|
||||
var contacts = [];
|
||||
var numContacts = entries.length;
|
||||
for (var i = 0; i < numContacts; i++) {
|
||||
contacts.push(gContactToJson(entries[i]));
|
||||
}
|
||||
|
||||
return contacts;
|
||||
};
|
||||
|
||||
// Returns the object used to build the headers necesary by the service
|
||||
var buildRequestHeaders = function buildRequestHeaders(access_token) {
|
||||
var requestHeaders = EXTRA_HEADERS;
|
||||
requestHeaders.Authorization = 'OAuth ' + access_token;
|
||||
|
||||
return requestHeaders;
|
||||
};
|
||||
|
||||
// Gets a list of all contacts giving a valid access token
|
||||
var listAllContacts = function listAllContacts(access_token,
|
||||
callbacks) {
|
||||
// Copy the access_token
|
||||
accessToken = access_token;
|
||||
photoUrls = {};
|
||||
getContactsGroup(access_token, callbacks);
|
||||
};
|
||||
|
||||
var getContactsGroup = function getContactsGroup(access_token,
|
||||
callbacks) {
|
||||
var groupCallbacks = {
|
||||
success: function onSuccess(response) {
|
||||
// Locate the entry witch systemGroup id is 'Contacts'
|
||||
var feed = response.querySelector('feed');
|
||||
if (feed === null) {
|
||||
callbacks.error();
|
||||
return;
|
||||
}
|
||||
|
||||
var sgc = feed.querySelector('systemGroup[id="Contacts"]');
|
||||
if (sgc !== null) {
|
||||
var id = sgc.parentNode.querySelector('id').textContent;
|
||||
getContactsByGroup(id, access_token, callbacks);
|
||||
} else {
|
||||
callbacks.error();
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
if (e && e.status === 401) {
|
||||
// This is a token expired / invalid token problem
|
||||
window.console.warn('GMail Access token expired or invalid. ',
|
||||
'restarting flow!');
|
||||
callbacks.success({
|
||||
error: {
|
||||
code: 190
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
callbacks.error();
|
||||
}
|
||||
},
|
||||
timeout: callbacks.timeout
|
||||
};
|
||||
|
||||
return performAPIRequest(GROUPS_END_POINT, groupCallbacks, access_token);
|
||||
};
|
||||
|
||||
// Retrieve all the contacts for the specific groupId
|
||||
var getContactsByGroup = function getContactsByGroup(groupId, access_token,
|
||||
callbacks) {
|
||||
var listingCallbacks = {
|
||||
success: function onSuccess(response) {
|
||||
callbacks.success({
|
||||
'data': nodeListToArray(response)
|
||||
});
|
||||
},
|
||||
error: callbacks.error,
|
||||
timeout: callbacks.timeout
|
||||
};
|
||||
|
||||
var groupUrl = END_POINT + '&group=' + groupId;
|
||||
return performAPIRequest(groupUrl, listingCallbacks, access_token);
|
||||
};
|
||||
|
||||
// Given a Google contacts api url add the authentication and
|
||||
// extra headers to perform the correct request
|
||||
var performAPIRequest = function performAPIRequest(url, callbacks,
|
||||
access_token) {
|
||||
return Rest.get(url, callbacks, {
|
||||
'requestHeaders': buildRequestHeaders(access_token),
|
||||
'responseType': 'xml'
|
||||
});
|
||||
};
|
||||
|
||||
// Return the list of contacts on the device imported using this connector
|
||||
var listDeviceContacts = function listDeviceContacts(callbacks) {
|
||||
var filterOptions = {
|
||||
filterValue: CATEGORY,
|
||||
filterOp: 'contains',
|
||||
filterBy: ['category']
|
||||
};
|
||||
|
||||
var req = navigator.mozContacts.find(filterOptions);
|
||||
req.onsuccess = function() {
|
||||
callbacks.success(req.result);
|
||||
};
|
||||
req.onerror = function onError() {
|
||||
callbacks.success([]);
|
||||
};
|
||||
};
|
||||
|
||||
var getImporter = function getImporter(contactsList, access_token) {
|
||||
return new window.ContactsImporter(contactsList, access_token, this);
|
||||
};
|
||||
|
||||
var cleanContacts = function cleanContacts(contactsList, mode, cb) {
|
||||
var cleaner = new window.ContactsCleaner(contactsList);
|
||||
window.setTimeout(cleaner.start, 0);
|
||||
if (cb) {
|
||||
cb(cleaner);
|
||||
}
|
||||
};
|
||||
|
||||
var getValueForNode = function getValueForNode(doc, name, def) {
|
||||
var defaultValue = def || '';
|
||||
|
||||
if (doc == null || name == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var node = doc.querySelector(name);
|
||||
|
||||
if (node && node.textContent) {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
/*
|
||||
Given an xml contact entry from gmail contacts we will need to output
|
||||
a json object that contains the minimun viable information to display
|
||||
that contact into a list.
|
||||
|
||||
The fields for that minimun visualization object are:
|
||||
uid
|
||||
givenName
|
||||
familyName
|
||||
email1
|
||||
*/
|
||||
var adaptDataForShowing = function adaptDataForShowing(contact) {
|
||||
var output = {
|
||||
'uid': '-1',
|
||||
'givenName': '',
|
||||
'familyName': '',
|
||||
'email1': '',
|
||||
'contactPictureUri': ''
|
||||
};
|
||||
|
||||
output.uid = contact.uid;
|
||||
|
||||
if (contact.familyName) {
|
||||
output.familyName = contact.familyName;
|
||||
}
|
||||
|
||||
if (contact.email && contact.email.length > 0) {
|
||||
output.email1 = contact.email[0].value;
|
||||
}
|
||||
var tel;
|
||||
if (contact.tel && contact.tel.length > 0) {
|
||||
tel = contact.tel[0].value;
|
||||
}
|
||||
if(contact.givenName) {
|
||||
output.givenName = contact.givenName;
|
||||
} else if(!contact.familyName) {
|
||||
output.givenName = output.email1 || contact.org || tel;
|
||||
} else if(contact.familyName) {
|
||||
output.givenName = '' ;
|
||||
}
|
||||
|
||||
var photoUrl = buildContactPhotoURL(contact, accessToken);
|
||||
if (photoUrl) {
|
||||
output.contactPictureUri = photoUrl;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
var adaptDataForSaving = function adaptDataForSaving(contact) {
|
||||
return contact;
|
||||
};
|
||||
|
||||
// Transform a Google contact entry into json format.
|
||||
// The json format is the same used in Contacts api ;P
|
||||
var gContactToJson = function gContactToJson(googleContact) {
|
||||
var output = {};
|
||||
|
||||
// This field will be needed for indexing within the
|
||||
// import process, not for the api
|
||||
output.uid = getUid(googleContact);
|
||||
|
||||
output.name = [getValueForNode(googleContact, 'title')];
|
||||
|
||||
// Store the photo url, not in the contact itself
|
||||
var photoUrl = googleContact.querySelector('link[type="image/*"]');
|
||||
if (photoUrl) {
|
||||
photoUrl = photoUrl.getAttribute('href');
|
||||
} else {
|
||||
// No image link
|
||||
photoUrl = '';
|
||||
}
|
||||
photoUrls[output.uid] = photoUrl;
|
||||
|
||||
var name = googleContact.querySelector('name');
|
||||
if (name) {
|
||||
var contactName = getValueForNode(name, 'givenName');
|
||||
if (contactName) {
|
||||
output.givenName = [contactName];
|
||||
}
|
||||
var contactFamilyName = getValueForNode(name, 'familyName');
|
||||
if (contactFamilyName) {
|
||||
output.familyName = [contactFamilyName];
|
||||
}
|
||||
var contactSuffix = getValueForNode(name, 'additionalName');
|
||||
if (contactSuffix) {
|
||||
output.additionalName = [contactSuffix];
|
||||
}
|
||||
}
|
||||
|
||||
output.email = parseEmails(googleContact);
|
||||
|
||||
output.adr = parseAddress(googleContact);
|
||||
|
||||
output.tel = parsePhones(googleContact);
|
||||
|
||||
var org = googleContact.querySelector('organization');
|
||||
if (org) {
|
||||
output.org = [getValueForNode(org, 'orgName')];
|
||||
output.jobTitle = [getValueForNode(org, 'orgTitle')];
|
||||
}
|
||||
|
||||
var bday = googleContact.querySelector('birthday');
|
||||
if (bday) {
|
||||
var bdayMS = Date.parse(bday.getAttribute('when'));
|
||||
if (!isNaN(bdayMS)) {
|
||||
output.bday = new Date(bdayMS);
|
||||
}
|
||||
}
|
||||
|
||||
var content = googleContact.querySelector('content');
|
||||
if (content) {
|
||||
output.note = [content.textContent];
|
||||
}
|
||||
|
||||
output.category = [CATEGORY];
|
||||
output.url = [{
|
||||
type: ['source'],
|
||||
value: getContactURI(output)
|
||||
}];
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
var getContactURI = function getContactURI(contact) {
|
||||
return URN_IDENTIFIER + contact.uid;
|
||||
};
|
||||
|
||||
// This will be a full url like:
|
||||
// http://www.google.com/m8/feeds/contacts/<email>/base/<contact_id>
|
||||
// for a specific contact node
|
||||
var getUid = function getUid(contact) {
|
||||
return contact.querySelector('id').textContent;
|
||||
};
|
||||
|
||||
// Returns an array with the possible emails found in a contact
|
||||
// as a ContactField format
|
||||
var parseEmails = function parseEmails(googleContact) {
|
||||
var DEFAULT_EMAIL_TYPE = 'other';
|
||||
var emails = [];
|
||||
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
|
||||
'email');
|
||||
if (fields && fields.length > 0) {
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var emailField = fields.item(i);
|
||||
|
||||
// Type format: rel="http://schemas.google.com/g/2005#home"
|
||||
var type = emailField.getAttribute('rel') || DEFAULT_EMAIL_TYPE;
|
||||
if (type.indexOf('#') > -1) {
|
||||
type = type.substr(type.indexOf('#') + 1);
|
||||
}
|
||||
|
||||
emails.push({
|
||||
'type': [type],
|
||||
'value': emailField.getAttribute('address')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return emails;
|
||||
};
|
||||
|
||||
// Given a google contact returns an array of ContactAddress
|
||||
var parseAddress = function parseAddress(googleContact) {
|
||||
var addresses = [];
|
||||
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
|
||||
'structuredPostalAddress');
|
||||
if (fields && fields.length > 0) {
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var field = fields.item(i);
|
||||
var address = {};
|
||||
|
||||
address.streetAddress = getValueForNode(field, 'street');
|
||||
address.locality = getValueForNode(field, 'city');
|
||||
address.region = getValueForNode(field, 'region');
|
||||
address.postalCode = getValueForNode(field, 'postcode');
|
||||
address.countryName = getValueForNode(field, 'country');
|
||||
|
||||
addresses.push(address);
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
};
|
||||
|
||||
// Given a google contact this function returns an array of
|
||||
// ContactField with the pones stored for that contact
|
||||
var parsePhones = function parsePhones(googleContact) {
|
||||
var DEFAULT_PHONE_TYPE = 'other';
|
||||
var GMAIL_MAP = {
|
||||
'work_fax' : 'faxOffice',
|
||||
'home_fax' : 'faxHome',
|
||||
'pager' : 'other',
|
||||
'main' : 'other'
|
||||
};
|
||||
var phones = [];
|
||||
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
|
||||
'phoneNumber');
|
||||
if (fields && fields.length > 0) {
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var field = fields.item(i);
|
||||
|
||||
// Type format: rel="http://schemas.google.com/g/2005#home"
|
||||
var type = field.getAttribute('rel') || DEFAULT_PHONE_TYPE;
|
||||
if (type.indexOf('#') > -1) {
|
||||
type = type.substr(type.indexOf('#') + 1);
|
||||
}
|
||||
|
||||
phones.push({
|
||||
'type': [GMAIL_MAP[type] || type],
|
||||
'value': field.textContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return phones;
|
||||
};
|
||||
|
||||
// Given a contact from the mozcontact api, fetch the Google Contacts
|
||||
// identifier
|
||||
var getContactUid = function getContactUid(deviceContact) {
|
||||
var out = -1;
|
||||
|
||||
var url = deviceContact.url;
|
||||
if (Array.isArray(url)) {
|
||||
var targetUrls = url.filter(function(aUrl) {
|
||||
return Array.isArray(aUrl.type) &&
|
||||
aUrl.type.indexOf('source') !== -1 &&
|
||||
aUrl.value;
|
||||
});
|
||||
|
||||
if (targetUrls[0]) {
|
||||
out = resolveURI(targetUrls[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
// From a contact URL, that we expect to be a URI
|
||||
// return the google contact id if we find it on
|
||||
// the uri, -1 otherwise
|
||||
var resolveURI = function resolveURI(uri) {
|
||||
if (uri && uri.indexOf(URN_IDENTIFIER) === 0) {
|
||||
var output = uri.substr(URN_IDENTIFIER.length);
|
||||
if (output && output.length > 0) {
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
var downloadContactPicture = function downloadContactPicture(googleContact,
|
||||
access_token, callbacks) {
|
||||
var url = buildContactPhotoURL(googleContact, access_token);
|
||||
return Rest.get(url, callbacks, {
|
||||
'responseType': 'blob'
|
||||
});
|
||||
};
|
||||
|
||||
// Build the url of the photo with the access token
|
||||
var buildContactPhotoURL = function contactPhotoURL(contact, access_token) {
|
||||
if (photoUrls && photoUrls[contact.uid]) {
|
||||
return photoUrls[contact.uid] + '?access_token=' + access_token;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var startSync = function startSync() {
|
||||
//We don't sync Google contacts (yet)
|
||||
};
|
||||
|
||||
var getServiceName = (function getServiceName() {
|
||||
return 'gmail';
|
||||
})();
|
||||
|
||||
var getAutomaticLogout = (function getAutomaticLogout() {
|
||||
return true;
|
||||
})();
|
||||
|
||||
return {
|
||||
'listAllContacts': listAllContacts,
|
||||
'listDeviceContacts': listDeviceContacts,
|
||||
'getImporter': getImporter,
|
||||
'cleanContacts': cleanContacts,
|
||||
'adaptDataForShowing': adaptDataForShowing,
|
||||
'adaptDataForSaving': adaptDataForSaving,
|
||||
'getContactUid': getContactUid,
|
||||
'downloadContactPicture': downloadContactPicture,
|
||||
'startSync': startSync,
|
||||
'name': getServiceName,
|
||||
'automaticLogout': getAutomaticLogout,
|
||||
'gContactToJson': gContactToJson
|
||||
};
|
||||
|
||||
})();
|
||||
132
gaia/shared/js/contacts/import/import_init.js
Executable file
132
gaia/shared/js/contacts/import/import_init.js
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
/* globals LazyLoader, LiveConnector, GmailConnector, FacebookConnector,
|
||||
utils, importer */
|
||||
'use strict';
|
||||
|
||||
(function(document) {
|
||||
var params = parseParams(window.location.search.substring(1));
|
||||
|
||||
var serviceName = getServiceName();
|
||||
var allowedOrigin = location.origin;
|
||||
|
||||
function parseParams(paramsStr) {
|
||||
var out = {};
|
||||
var paramsList;
|
||||
if (paramsStr.indexOf('&') != -1) {
|
||||
paramsList = paramsStr.split('&');
|
||||
}
|
||||
else {
|
||||
paramsList = [paramsStr];
|
||||
}
|
||||
paramsList.forEach(function(param) {
|
||||
var paramsValues = param.split('=');
|
||||
out[paramsValues[0]] = paramsValues[1];
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getServiceName() {
|
||||
return params.service;
|
||||
}
|
||||
|
||||
var servicesConnectors = {
|
||||
'live': function(cb) {
|
||||
LazyLoader.load('/shared/js/contacts/import/live/live_connector.js',
|
||||
function() {
|
||||
cb(LiveConnector);
|
||||
});
|
||||
},
|
||||
'gmail': function gmailLoader(cb) {
|
||||
LazyLoader.load('/shared/js/contacts/import/gmail/gmail_connector.js',
|
||||
function onLoad() {
|
||||
cb(GmailConnector);
|
||||
});
|
||||
},
|
||||
'facebook': function(cb) {
|
||||
var files = [
|
||||
'/shared/js/fb/fb_request.js',
|
||||
'/shared/js/contacts/import/facebook/fb_data.js',
|
||||
'/shared/js/contacts/import/facebook/fb_utils.js',
|
||||
'/shared/js/fb/fb_reader_utils.js',
|
||||
'/shared/js/contacts/import/facebook/fb_contact_utils.js',
|
||||
'/shared/js/contacts/import/facebook/fb_query.js',
|
||||
'/shared/js/contacts/import/facebook/fb_contact.js',
|
||||
'/shared/js/contacts/import/facebook/facebook_connector.js',
|
||||
'/shared/pages/import/style/facebook.css'
|
||||
];
|
||||
if (!params.ftu) {
|
||||
files.push('/facebook/js/fb_sync.js');
|
||||
}
|
||||
LazyLoader.load(files, function() {
|
||||
cb(FacebookConnector);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getServiceConnector(cb) {
|
||||
if (serviceName) {
|
||||
servicesConnectors[serviceName](cb);
|
||||
}
|
||||
else {
|
||||
throw new Error('Service Connector not found !!!');
|
||||
}
|
||||
}
|
||||
|
||||
function tokenReady(access_token) {
|
||||
if (document.readyState === 'complete') {
|
||||
onLoad(access_token);
|
||||
}
|
||||
else {
|
||||
window.addEventListener('load', function do_load() {
|
||||
onLoad(access_token);
|
||||
window.removeEventListener('load', do_load);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onLoad(access_token) {
|
||||
utils.listeners.add({
|
||||
'#import-action': importer.ui.importAll,
|
||||
'#done-search': window.Search.exitSearchMode,
|
||||
'#groups-list': importer.ui.selection,
|
||||
'#header': [
|
||||
{
|
||||
event: 'action',
|
||||
handler: importer.ui.end
|
||||
}
|
||||
],
|
||||
'#search-start': [
|
||||
{
|
||||
event: 'click',
|
||||
handler: window.Search.enterSearchMode
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// This is done through onclick as it is going to be changed it dynamically
|
||||
document.querySelector('#select-all').onclick = importer.ui.selectAll;
|
||||
document.querySelector('#deselect-all').onclick =
|
||||
importer.ui.unSelectAll;
|
||||
|
||||
importer.ui.init();
|
||||
getServiceConnector(function(connector) {
|
||||
importer.start(access_token, connector, allowedOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('message', function getAccessToken(e) {
|
||||
if (e.origin !== allowedOrigin) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener('message', getAccessToken);
|
||||
if (e.data.type === 'token') {
|
||||
tokenReady(e.data.data);
|
||||
}
|
||||
});
|
||||
|
||||
parent.postMessage({
|
||||
type: 'messaging_ready',
|
||||
data: ''
|
||||
}, allowedOrigin);
|
||||
})(document);
|
||||
95
gaia/shared/js/contacts/import/import_status_data.js
Executable file
95
gaia/shared/js/contacts/import/import_status_data.js
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
'use strict';
|
||||
|
||||
/* globals Promise */
|
||||
/* exported ImportStatusData */
|
||||
|
||||
/*
|
||||
* Module for communication of import status data between FTU and Comms Apps via
|
||||
* Datastore.
|
||||
*
|
||||
*/
|
||||
|
||||
if (!window.ImportStatusData) {
|
||||
var ImportStatusData = (function() {
|
||||
var store = null;
|
||||
var DS_NAME = 'Import_Status_Data';
|
||||
|
||||
var datastoreLoading = false;
|
||||
var datastoreLoaded = false;
|
||||
var DS_LOADED_EVENT = 'datastore_loaded';
|
||||
|
||||
var getDatastore = function getDatastore() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!navigator.getDataStores) {
|
||||
reject({
|
||||
name: 'DatastoreNotEnabled'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (datastoreLoaded) {
|
||||
resolve(store);
|
||||
return;
|
||||
}
|
||||
|
||||
if (datastoreLoading) {
|
||||
document.addEventListener(DS_LOADED_EVENT, function loadedHandler() {
|
||||
document.removeEventListener(DS_LOADED_EVENT, loadedHandler);
|
||||
resolve(store);
|
||||
});
|
||||
}
|
||||
|
||||
datastoreLoading = true;
|
||||
navigator.getDataStores(DS_NAME).then(function(stores) {
|
||||
store = stores[0];
|
||||
}, reject).then(function() {
|
||||
datastoreLoading = false;
|
||||
datastoreLoaded = true;
|
||||
document.dispatchEvent(new CustomEvent(DS_LOADED_EVENT));
|
||||
|
||||
resolve(store);
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
var put = function put(key, obj) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
getDatastore().then(function success(store) {
|
||||
store.put(obj, key).then(resolve, reject);
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
var remove = function remove(key) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
getDatastore().then(function success(store) {
|
||||
store.remove(key).then(resolve, reject);
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
var get = function get(key) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
getDatastore().then(function success(store) {
|
||||
store.get(key).then(resolve, reject);
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
var clear = function clear() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
getDatastore().then(function success(store) {
|
||||
store.clear().then(resolve, reject);
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
put: put,
|
||||
remove: remove,
|
||||
get: get,
|
||||
clear: clear
|
||||
};
|
||||
|
||||
})();
|
||||
}
|
||||
1072
gaia/shared/js/contacts/import/importer_ui.js
Executable file
1072
gaia/shared/js/contacts/import/importer_ui.js
Executable file
File diff suppressed because it is too large
Load diff
210
gaia/shared/js/contacts/import/live/live_connector.js
Executable file
210
gaia/shared/js/contacts/import/live/live_connector.js
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
'use strict';
|
||||
|
||||
/* global Rest */
|
||||
/* global utils */
|
||||
|
||||
if (!window.LiveConnector) {
|
||||
window.LiveConnector = (function() {
|
||||
var LIVE_ENDPOINT = 'https://apis.live.net/v5.0/';
|
||||
var CONTACTS_RESOURCE = 'me/contacts';
|
||||
var PICTURE_RESOURCE = '/picture';
|
||||
|
||||
var LIVE_CATEGORY = 'live';
|
||||
|
||||
var itemsTypeMap = {
|
||||
'personal': 'personal',
|
||||
'mobile': 'mobile',
|
||||
'business': 'work',
|
||||
'other': 'other',
|
||||
'preferred': 'personal'
|
||||
};
|
||||
|
||||
function fillAddress(addrType, addrValue) {
|
||||
var out = {
|
||||
type: [addrType],
|
||||
streetAddress: addrValue.street || '',
|
||||
locality: addrValue.city || '',
|
||||
region: addrValue.state || '',
|
||||
countryName: addrValue.region || '',
|
||||
postalCode: addrValue.postal_code || ''
|
||||
};
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getURI(liveContact) {
|
||||
return 'urn:uuid:' + (liveContact.user_id || liveContact.id);
|
||||
}
|
||||
|
||||
function resolveURI(uri) {
|
||||
var components = uri.split(':');
|
||||
// The third element is the user id of the live contact
|
||||
return components[2];
|
||||
}
|
||||
|
||||
function LiveConnector() {
|
||||
}
|
||||
|
||||
LiveConnector.prototype = {
|
||||
listAllContacts: function(access_token, callbacks) {
|
||||
var uriElements = [LIVE_ENDPOINT, CONTACTS_RESOURCE, '?',
|
||||
'access_token', '=', access_token];
|
||||
|
||||
return Rest.get(uriElements.join(''), callbacks);
|
||||
},
|
||||
|
||||
listDeviceContacts: function(callbacks) {
|
||||
var filterOptions = {
|
||||
filterValue: LIVE_CATEGORY,
|
||||
filterOp: 'contains',
|
||||
filterBy: ['category']
|
||||
};
|
||||
var req = navigator.mozContacts.find(filterOptions);
|
||||
req.onsuccess = function() {
|
||||
callbacks.success(req.result);
|
||||
};
|
||||
req.onerror = function() {
|
||||
callbacks.error(req.error);
|
||||
};
|
||||
},
|
||||
|
||||
getImporter: function(contactsList, access_token) {
|
||||
return new window.ContactsImporter(contactsList, access_token, this);
|
||||
},
|
||||
|
||||
cleanContacts: function(contactsList, mode, cb) {
|
||||
// Just a placeholder for the moment
|
||||
var cleaner = new window.ContactsCleaner(contactsList);
|
||||
window.setTimeout(cleaner.start, 0);
|
||||
cb(cleaner);
|
||||
},
|
||||
|
||||
adaptDataForShowing: function(source) {
|
||||
var out = source;
|
||||
|
||||
out.uid = source.user_id || source.id;
|
||||
out.givenName = [source.first_name || ''];
|
||||
out.familyName = [source.last_name || ''];
|
||||
out.email1 = source.emails.account ||
|
||||
source.emails.preferred ||
|
||||
source.emails.personal ||
|
||||
'';
|
||||
|
||||
out.contactPictureUri = [LIVE_ENDPOINT, out.uid,
|
||||
PICTURE_RESOURCE, '?type=medium'].join('');
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
adaptDataForSaving: function live2MozContact(liveContact) {
|
||||
var out = {
|
||||
givenName: [liveContact.first_name || ''],
|
||||
familyName: [liveContact.last_name || ''],
|
||||
name: [liveContact.name || ''],
|
||||
tel: [],
|
||||
email: [],
|
||||
adr: [],
|
||||
photo: liveContact.photo,
|
||||
category: [LIVE_CATEGORY],
|
||||
url: [{
|
||||
type: ['source'],
|
||||
value: getURI(liveContact)
|
||||
}]
|
||||
};
|
||||
|
||||
// Months in JS goes from 0 to 11 :(
|
||||
var bmonth = liveContact.birth_month - 1;
|
||||
var bday = liveContact.birth_day;
|
||||
if (bmonth > -1 && bday) {
|
||||
var birthdate = out.bday = new Date();
|
||||
birthdate.setUTCDate(bday);
|
||||
birthdate.setUTCMonth(bmonth, bday);
|
||||
birthdate.setUTCFullYear(utils.misc.FLAG_YEAR_IGNORED);
|
||||
}
|
||||
|
||||
var liveEmails = liveContact.emails || {};
|
||||
var alreadyAddedEmails = {};
|
||||
Object.keys(liveEmails).forEach(function(emailType) {
|
||||
var emailValue = liveEmails[emailType];
|
||||
var present = (typeof alreadyAddedEmails[emailValue] === 'boolean');
|
||||
if (emailValue && !present) {
|
||||
out.email.push({
|
||||
type: [itemsTypeMap[emailType]],
|
||||
value: emailValue
|
||||
});
|
||||
alreadyAddedEmails[emailValue] = true;
|
||||
}
|
||||
});
|
||||
|
||||
var livePhones = liveContact.phones || {};
|
||||
Object.keys(livePhones).forEach(function(phoneType) {
|
||||
var phoneValue = livePhones[phoneType];
|
||||
if (phoneValue) {
|
||||
out.tel.push({
|
||||
type: [itemsTypeMap[phoneType]],
|
||||
value: phoneValue
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var liveAddresses = liveContact.addresses || {};
|
||||
Object.keys(liveAddresses).forEach(function(addrType) {
|
||||
var addrValue = liveAddresses[addrType];
|
||||
if (addrValue) {
|
||||
out.adr.push(fillAddress(itemsTypeMap[addrType], addrValue));
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
// It would allow to know the UID of a service contact already imported
|
||||
// on the device. That is needed by the generic importer, for live
|
||||
// a dummy implementation as we are currently not supporting updates
|
||||
getContactUid: function(deviceContact) {
|
||||
var out = '-1';
|
||||
|
||||
var url = deviceContact.url;
|
||||
if (Array.isArray(url)) {
|
||||
var targetUrls = url.filter(function(aUrl) {
|
||||
return Array.isArray(aUrl.type) &&
|
||||
aUrl.type.indexOf('source') !== -1;
|
||||
});
|
||||
if (targetUrls[0]) {
|
||||
out = resolveURI(targetUrls[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
get name() {
|
||||
return 'live';
|
||||
},
|
||||
|
||||
get automaticLogout() {
|
||||
return true;
|
||||
},
|
||||
|
||||
downloadContactPicture: function(contact, access_token, callbacks) {
|
||||
if (contact.user_id) {
|
||||
var uriElements = [LIVE_ENDPOINT, contact.user_id, PICTURE_RESOURCE,
|
||||
'?', 'access_token', '=', access_token];
|
||||
|
||||
return Rest.get(uriElements.join(''), callbacks, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
else {
|
||||
callbacks.success(null);
|
||||
}
|
||||
},
|
||||
|
||||
startSync: function() {
|
||||
// Sync not supported
|
||||
}
|
||||
};
|
||||
|
||||
return new LiveConnector();
|
||||
})();
|
||||
}
|
||||
102
gaia/shared/js/contacts/import/utilities/config.js
Executable file
102
gaia/shared/js/contacts/import/utilities/config.js
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
'use strict';
|
||||
/* global Promise */
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (typeof utils.config === 'undefined') {
|
||||
(function() {
|
||||
var configs = {};
|
||||
var initializing = {};
|
||||
var initialized = {};
|
||||
|
||||
var EVENT_INITIALIZED = 'config_initialized';
|
||||
|
||||
utils.config = utils.config || {};
|
||||
|
||||
utils.config.reset = function() {
|
||||
configs = {};
|
||||
initializing = {};
|
||||
initialized = {};
|
||||
};
|
||||
|
||||
function onLoad(xhr, file, resolveCb) {
|
||||
configs[file] = xhr.response;
|
||||
|
||||
delete initializing[file];
|
||||
initialized[file] = true;
|
||||
|
||||
document.dispatchEvent(new CustomEvent(EVENT_INITIALIZED, {
|
||||
detail: {
|
||||
file: file,
|
||||
success: true
|
||||
}
|
||||
}));
|
||||
resolveCb(configs[file]);
|
||||
}
|
||||
|
||||
utils.config.load = function(file) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var isInitialized = initialized[file];
|
||||
if (isInitialized === true) {
|
||||
resolve(configs[file]);
|
||||
return;
|
||||
}
|
||||
|
||||
var isInitializing = initializing[file];
|
||||
if (isInitializing) {
|
||||
var handler = function(expectedFile, e) {
|
||||
document.removeEventListener(EVENT_INITIALIZED, handler);
|
||||
var file = e.detail.file;
|
||||
if (expectedFile !== file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.detail.success) {
|
||||
resolve(configs[file]);
|
||||
}
|
||||
else {
|
||||
reject();
|
||||
}
|
||||
}.bind(null, file);
|
||||
|
||||
document.addEventListener(EVENT_INITIALIZED, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
initializing[file] = true;
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', file, true);
|
||||
xhr.responseType = 'json';
|
||||
|
||||
xhr.onload = function() {
|
||||
var resolvedCb = resolve;
|
||||
if (xhr.status !== 200) {
|
||||
resolvedCb = reject;
|
||||
}
|
||||
onLoad(xhr, file, resolvedCb);
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
document.dispatchEvent(new CustomEvent(EVENT_INITIALIZED, {
|
||||
detail: {
|
||||
file: file,
|
||||
success: false
|
||||
}
|
||||
}));
|
||||
delete initializing[file];
|
||||
reject(xhr.error);
|
||||
};
|
||||
|
||||
// If there is any exception we consider the file does not exist
|
||||
try {
|
||||
xhr.send(null);
|
||||
}
|
||||
catch(e) {
|
||||
console.error('Error while loading config file: ', e.message);
|
||||
onLoad({ status: 404 }, file, reject);
|
||||
}
|
||||
});
|
||||
};
|
||||
})();
|
||||
}
|
||||
188
gaia/shared/js/contacts/import/utilities/import_from_vcard.js
Executable file
188
gaia/shared/js/contacts/import/utilities/import_from_vcard.js
Executable file
|
|
@ -0,0 +1,188 @@
|
|||
/* global VCardReader, ConfirmDialog, contacts, LazyLoader, Loader */
|
||||
/* global Matcher */
|
||||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
/**
|
||||
* Loads the file passed on the activity, reads the text from it, and
|
||||
* import the info
|
||||
* Passes the id of last imported contact in the callback
|
||||
*/
|
||||
utils.importFromVcard = function(file, callback) {
|
||||
|
||||
var MERGE_DEPENDENCIES = [
|
||||
'/shared/js/simple_phone_matcher.js',
|
||||
'/shared/js/contacts/merger_adapter.js',
|
||||
'/shared/js/contacts/contacts_merger.js',
|
||||
'/shared/js/contacts/contacts_matcher.js'
|
||||
];
|
||||
|
||||
Loader.utility('Overlay', function() {
|
||||
LazyLoader.load(MERGE_DEPENDENCIES, _loaded);
|
||||
});
|
||||
|
||||
function _loaded() {
|
||||
var importedContacts = 0;
|
||||
var cancelled = false;
|
||||
var importer = null;
|
||||
var text = null;
|
||||
var progress = utils.overlay.show(
|
||||
'memoryCardContacts-reading',
|
||||
'activityBar'
|
||||
);
|
||||
|
||||
utils.overlay.showMenu();
|
||||
utils.overlay.oncancel = function oncancel() {
|
||||
cancelled = true;
|
||||
if (importer) {
|
||||
importer.finish();
|
||||
} else {
|
||||
utils.overlay.hide();
|
||||
}
|
||||
};
|
||||
|
||||
readVCard(function(vcardText) {
|
||||
if (vcardText) {
|
||||
processTextFromFile(vcardText);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
function readVCard(cb) {
|
||||
var reader;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
cb(file);
|
||||
} else {
|
||||
reader = new FileReader();
|
||||
reader.onloadend = function() {
|
||||
text = reader.result;
|
||||
if (text) {
|
||||
cb(text);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error('Error reading the file ' + ex.message);
|
||||
import_error();
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processTextFromFile(textFromFile) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new VCardReader(textFromFile);
|
||||
var match = textFromFile && textFromFile.match(/END:VCARD/g);
|
||||
var cursor = reader.getAll();
|
||||
var numContacts = match ? match.length : 0;
|
||||
var numDupsMerged = 0;
|
||||
var firstContact = null;
|
||||
|
||||
var _doContinue = function(savedContact) {
|
||||
if (savedContact) {
|
||||
if (!firstContact) {
|
||||
firstContact = savedContact;
|
||||
}
|
||||
imported_contact();
|
||||
}
|
||||
if (!cancelled) {
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
|
||||
cursor.onsuccess = function(evt) {
|
||||
if (evt.target.result) {
|
||||
var contact = evt.target.result;
|
||||
|
||||
Matcher.match(contact, 'passive', {
|
||||
onmatch: function(matches) {
|
||||
var callbacks = {
|
||||
success: function(mergedContact) {
|
||||
numDupsMerged++;
|
||||
_doContinue(mergedContact);
|
||||
},
|
||||
error: _doContinue
|
||||
};
|
||||
contacts.adaptAndMerge(contact, matches, callbacks);
|
||||
},
|
||||
onmismatch: function() {
|
||||
var req = navigator.mozContacts.save(contact);
|
||||
req.onsuccess = function() {
|
||||
_doContinue(contact);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// No more contacts
|
||||
} else {
|
||||
utils.overlay.hide();
|
||||
if (!cancelled) {
|
||||
var msgImported = {
|
||||
id: 'memoryCardContacts-imported3',
|
||||
args: {n: importedContacts}
|
||||
};
|
||||
var msgDupsMerged = numDupsMerged ? {
|
||||
id: 'contactsMerged',
|
||||
args: {numDups: numDupsMerged}
|
||||
} : null;
|
||||
|
||||
utils.status.show(msgImported, msgDupsMerged);
|
||||
}
|
||||
if (importedContacts > 0) {
|
||||
callback(importedContacts, firstContact.id);
|
||||
} else {
|
||||
callback(importedContacts);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
import_read(numContacts);
|
||||
}
|
||||
|
||||
function import_read(n) {
|
||||
progress.setClass('progressBar');
|
||||
progress.setHeaderMsg('memoryCardContacts-importing');
|
||||
progress.setTotal(n);
|
||||
}
|
||||
|
||||
function imported_contact() {
|
||||
importedContacts++;
|
||||
if (!cancelled) {
|
||||
progress.update();
|
||||
}
|
||||
}
|
||||
|
||||
function import_error(e) {
|
||||
console.error('Error importing from vcard: ' + e.message);
|
||||
// Showing error message allowing user to retry
|
||||
var cancel = {
|
||||
title: 'cancel',
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
}
|
||||
};
|
||||
|
||||
var retry = {
|
||||
title: 'retry',
|
||||
isRecommend: true,
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
// And now the action is reproduced one more time
|
||||
processTextFromFile(text);
|
||||
}
|
||||
};
|
||||
ConfirmDialog.show(null, 'memoryCardContacts-error',
|
||||
cancel, retry);
|
||||
utils.overlay.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
237
gaia/shared/js/contacts/import/utilities/import_sim_contacts.js
Executable file
237
gaia/shared/js/contacts/import/utilities/import_sim_contacts.js
Executable file
|
|
@ -0,0 +1,237 @@
|
|||
/*jshint loopfunc: true */
|
||||
/* global mozContact, LazyLoader, utils, contacts */
|
||||
/* global Matcher */
|
||||
/* exported SimContactsImporter */
|
||||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Imports contacts stored in the SIM card and saves them into
|
||||
* navigator.mozContacts. Three steps => three callback as arguments:
|
||||
* - onread: SIM card has been read properly;
|
||||
* - onimport: A Contact has been imported
|
||||
* - onfinish: contacts have been saved into navigator.mozContacts;
|
||||
* - onerror: SIM card is empty or could not be read.
|
||||
*/
|
||||
|
||||
function SimContactsImporter(targetIcc) {
|
||||
if (targetIcc === null) {
|
||||
throw new Error('We need an icc to continue with this operation');
|
||||
}
|
||||
var pointer = 0;
|
||||
var CHUNK_SIZE = 5;
|
||||
var numResponses = 0;
|
||||
var self = this;
|
||||
var mustFinish = false;
|
||||
var loadedMatch = false;
|
||||
var DEFAULT_TEL_TYPE = 'other';
|
||||
var icc = targetIcc;
|
||||
var iccId = icc.iccInfo && icc.iccInfo.iccid;
|
||||
|
||||
var numDupsMerged = 0;
|
||||
|
||||
function generateIccContactUrl(contactid) {
|
||||
var urlValue = 'urn:' + 'uuid:' + (iccId || 'iccId') + '-' + contactid;
|
||||
return [{
|
||||
type: ['source', 'sim'],
|
||||
value: urlValue
|
||||
}];
|
||||
}
|
||||
|
||||
function notifyFinish() {
|
||||
if (typeof self.onfinish === 'function') {
|
||||
window.setTimeout(self.onfinish, 0, numDupsMerged, iccId);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyImported() {
|
||||
if (typeof self.onimported === 'function') {
|
||||
window.setTimeout(self.onimported, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function continueCb() {
|
||||
numResponses++;
|
||||
pointer++;
|
||||
notifyImported();
|
||||
if (pointer < self.items.length && numResponses === CHUNK_SIZE) {
|
||||
numResponses = 0;
|
||||
mustFinish ? notifyFinish() : importSlice(pointer);
|
||||
}
|
||||
else if (pointer >= self.items.length) {
|
||||
notifyFinish();
|
||||
}
|
||||
}
|
||||
|
||||
function startMigration() {
|
||||
if (!mustFinish && Array.isArray(self.items) && self.items.length > 0) {
|
||||
importSlice(0);
|
||||
}
|
||||
else {
|
||||
notifyFinish();
|
||||
}
|
||||
}
|
||||
|
||||
function onContactsReadyForImport() {
|
||||
if (typeof self.onread === 'function') {
|
||||
// This way the total number can be known by the caller
|
||||
self.onread(self.items.length);
|
||||
}
|
||||
|
||||
if (loadedMatch) {
|
||||
startMigration();
|
||||
}
|
||||
else {
|
||||
document.addEventListener('matchLoaded', function mloaded() {
|
||||
document.removeEventListener('matchLoaded', mloaded);
|
||||
startMigration();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.start = function() {
|
||||
numDupsMerged = 0;
|
||||
|
||||
if (mustFinish) {
|
||||
notifyFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
LazyLoader.load([
|
||||
'/shared/js/simple_phone_matcher.js',
|
||||
'/shared/js/contacts/contacts_matcher.js',
|
||||
'/shared/js/contacts/contacts_merger.js',
|
||||
'/shared/js/contacts/utilities/image_thumbnail.js',
|
||||
'/shared/js/contacts/merger_adapter.js'
|
||||
], function loaded() {
|
||||
loadedMatch = true;
|
||||
document.dispatchEvent(new CustomEvent('matchLoaded'));
|
||||
});
|
||||
|
||||
var requestAdn, requestSdn;
|
||||
|
||||
// request contacts with readContacts() -- valid types are:
|
||||
// 'adn': Abbreviated Dialing Numbers
|
||||
// 'fdn': Fixed Dialing Numbers
|
||||
// 'sdn': Service Dialing Numbers
|
||||
if (icc && icc.readContacts) {
|
||||
requestAdn = icc.readContacts('adn');
|
||||
} else {
|
||||
throw new Error('Not able to obtain a SIM import function from platform');
|
||||
}
|
||||
|
||||
requestAdn.onsuccess = function onsuccess() {
|
||||
if (mustFinish) {
|
||||
notifyFinish();
|
||||
return;
|
||||
}
|
||||
self.items = requestAdn.result || [];
|
||||
|
||||
requestSdn = icc.readContacts('sdn');
|
||||
requestSdn.onsuccess = function onsuccess() {
|
||||
if (mustFinish) {
|
||||
notifyFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(requestSdn.result)) {
|
||||
self.items = self.items.concat(requestSdn.result);
|
||||
}
|
||||
onContactsReadyForImport();
|
||||
};
|
||||
requestSdn.onerror = function error() {
|
||||
if (mustFinish) {
|
||||
notifyFinish();
|
||||
return;
|
||||
}
|
||||
console.warn('Could not read SDN Contacts from SIM Card', error.name);
|
||||
onContactsReadyForImport();
|
||||
};
|
||||
|
||||
};
|
||||
requestAdn.onerror = function error() {
|
||||
if (typeof self.onerror === 'function') {
|
||||
self.onerror(requestAdn.error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
this.finish = function() {
|
||||
mustFinish = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* store mozContact elements -- each returned mozContact has two properties:
|
||||
* .name : [ string ]
|
||||
* .tel : [{ number: string, type: string }]
|
||||
* The 'name' property is only related to the mozContact element itself --
|
||||
* let's use it as the default 'givenName' value.
|
||||
*/
|
||||
function importSlice(from) {
|
||||
for (var i = from; i < from + CHUNK_SIZE && i < self.items.length; i++) {
|
||||
var item = self.items[i];
|
||||
var parsedName = utils.misc.parseName(item.name[0]);
|
||||
item.givenName = [parsedName.givenName];
|
||||
item.familyName = [parsedName.familyName];
|
||||
|
||||
if (Array.isArray(item.tel)) {
|
||||
var telItems = [];
|
||||
|
||||
for (var j = 0; j < item.tel.length; j++) {
|
||||
var aTel = item.tel[j];
|
||||
// Filtering out empty values
|
||||
if (aTel.value && aTel.value.trim()) {
|
||||
aTel.type = [DEFAULT_TEL_TYPE];
|
||||
telItems.push(aTel);
|
||||
}
|
||||
}
|
||||
item.tel = telItems;
|
||||
}
|
||||
|
||||
item.category = ['sim'];
|
||||
item.url = generateIccContactUrl(item.id);
|
||||
|
||||
// Item is presumably a mozContact but for some reason if
|
||||
// we don't create a new mozContact sometimes the save call fails
|
||||
var contact = item;
|
||||
|
||||
var cbs = {
|
||||
onmatch: function(results) {
|
||||
|
||||
var mergeCbs = {
|
||||
success: function() {
|
||||
numDupsMerged++;
|
||||
continueCb();
|
||||
},
|
||||
error: function(e) {
|
||||
window.console.error('Error while merging: ', e.name);
|
||||
continueCb();
|
||||
}
|
||||
};
|
||||
|
||||
contacts.adaptAndMerge(this, results, mergeCbs);
|
||||
}.bind(contact),
|
||||
onmismatch: function() {
|
||||
saveContact(this);
|
||||
}.bind(contact)
|
||||
};
|
||||
|
||||
Matcher.match(item, 'passive', cbs);
|
||||
}
|
||||
} // importSlice
|
||||
|
||||
|
||||
function saveContact(contact) {
|
||||
var req = window.navigator.mozContacts.save(new mozContact(contact));
|
||||
req.onsuccess = function saveSuccess() {
|
||||
continueCb();
|
||||
};
|
||||
req.onerror = function saveError() {
|
||||
console.error('SIM Import: Error importing ', contact.id,
|
||||
req.error.name);
|
||||
continueCb();
|
||||
};
|
||||
}
|
||||
}
|
||||
191
gaia/shared/js/contacts/import/utilities/misc.js
Executable file
191
gaia/shared/js/contacts/import/utilities/misc.js
Executable file
|
|
@ -0,0 +1,191 @@
|
|||
/* global mozContact, oauthflow, ImportStatusData */
|
||||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
// Scale ratio for different devices
|
||||
var SCALE_RATIO = window.devicePixelRatio || 1;
|
||||
|
||||
var LAST_IMPORT_TIMESTAMP_SUFFIX = '_last_import_timestamp';
|
||||
|
||||
function scale(size) {
|
||||
return Math.round(SCALE_RATIO * size);
|
||||
}
|
||||
|
||||
if (!utils.misc) {
|
||||
utils.misc = {};
|
||||
utils.misc.toMozContact = function(contact) {
|
||||
var outContact = contact;
|
||||
if (!(contact instanceof mozContact)) {
|
||||
outContact = new mozContact(contact);
|
||||
outContact.id = contact.id || outContact.id;
|
||||
}
|
||||
|
||||
return outContact;
|
||||
};
|
||||
|
||||
// This year indicates that the year can be ignored
|
||||
const FLAG_YEAR_IGNORED = 9996;
|
||||
utils.misc.FLAG_YEAR_IGNORED = FLAG_YEAR_IGNORED;
|
||||
|
||||
utils.misc.formatDate = function(date, customFormat) {
|
||||
var dateString = null;
|
||||
|
||||
try {
|
||||
var format = customFormat || {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
var offset = date.getTimezoneOffset() * 60 * 1000;
|
||||
var normalizedDate = new Date(date.getTime() + offset);
|
||||
|
||||
var year = normalizedDate.getFullYear();
|
||||
if (year !== FLAG_YEAR_IGNORED) {
|
||||
format.year = 'numeric';
|
||||
}
|
||||
var f = new Intl.DateTimeFormat(navigator.languages, format);
|
||||
|
||||
dateString = f.format(normalizedDate);
|
||||
} catch (err) {
|
||||
console.error('Error parsing date: ', err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return dateString;
|
||||
};
|
||||
|
||||
// Parses a name to obtain a givenName and a familyName
|
||||
// This is useful while importing contacts from SIM card
|
||||
utils.misc.parseName = function(nameString) {
|
||||
// Minimum length to consider a token as significative
|
||||
var MIN_LENGHT_SIGNIFICATIVE = 3;
|
||||
// Minimum length of a token to consider it a familyName
|
||||
var MIN_LENGHT_FN = 2;
|
||||
|
||||
var out = {
|
||||
givenName: '',
|
||||
familyName: ''
|
||||
};
|
||||
|
||||
if (!nameString) {
|
||||
return out;
|
||||
}
|
||||
var str = nameString.trim();
|
||||
if (!str) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function startsWithUpper(str) {
|
||||
var firstLetter = str.charAt(0);
|
||||
var capFirstLetter = firstLetter.toLocaleUpperCase();
|
||||
|
||||
return (capFirstLetter === firstLetter);
|
||||
}
|
||||
|
||||
// First step of the algorithm is to split the name in its tokens
|
||||
var tokens = nameString.split(/\s+/);
|
||||
var significativeTokens = {};
|
||||
for (var j = 0; j < tokens.length; j++) {
|
||||
var token = tokens[j];
|
||||
// A significative token os one with that min length and which starts with
|
||||
// uppercase letter
|
||||
if (token.length > MIN_LENGHT_SIGNIFICATIVE || startsWithUpper(token)) {
|
||||
significativeTokens[token] = true;
|
||||
}
|
||||
}
|
||||
|
||||
var totalTokens = tokens.length;
|
||||
var lastToken = totalTokens - 1;
|
||||
var rolePrevToken;
|
||||
var remainingTokens = Object.keys(significativeTokens).length;
|
||||
var outGivenNames = [], outFamilyNames = [];
|
||||
// We need to keep the totals in a different variable as a familyName
|
||||
// can be composed by more than one token
|
||||
var numFns = 0;
|
||||
|
||||
while (lastToken >= 0) {
|
||||
if (significativeTokens[tokens[lastToken]]) {
|
||||
remainingTokens--;
|
||||
}
|
||||
|
||||
var currToken = tokens[lastToken],
|
||||
nextToken = tokens[lastToken + 1],
|
||||
prevToken = tokens[lastToken - 1];
|
||||
|
||||
// The last element is taken as part of the family name
|
||||
if (!nextToken && prevToken) {
|
||||
outFamilyNames.push(currToken);
|
||||
numFns++;
|
||||
rolePrevToken = 'FN';
|
||||
}
|
||||
else if (!prevToken) {
|
||||
outGivenNames.push(currToken);
|
||||
rolePrevToken = 'GN';
|
||||
}
|
||||
else if (nextToken) {
|
||||
if (!significativeTokens[currToken]) {
|
||||
if (rolePrevToken === 'FN') {
|
||||
// Here the number of fns is not incremented as this is a non
|
||||
// significative token
|
||||
outFamilyNames.push(currToken);
|
||||
}
|
||||
else {
|
||||
outGivenNames.push(currToken);
|
||||
}
|
||||
}
|
||||
else if (currToken.length < MIN_LENGHT_FN) {
|
||||
outGivenNames.push(currToken);
|
||||
rolePrevToken = 'GN';
|
||||
}
|
||||
else {
|
||||
// Number of familyNames will be two at most
|
||||
// Preference is given to the givenName over familyName
|
||||
// We prefer two givenNames instead of two family Names
|
||||
if (remainingTokens >= 2 && numFns < 2) {
|
||||
outFamilyNames.push(currToken);
|
||||
numFns++;
|
||||
rolePrevToken = 'FN';
|
||||
}
|
||||
else {
|
||||
outGivenNames.push(currToken);
|
||||
rolePrevToken = 'GN';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastToken--;
|
||||
}
|
||||
|
||||
out.givenName = outGivenNames.reverse().join(' ').trim();
|
||||
out.familyName = outFamilyNames.reverse().join(' ').trim();
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
utils.misc.getPreferredPictureBox = function() {
|
||||
var imgThumbSize = oauthflow.params.facebook.imgThumbSize;
|
||||
var out = {
|
||||
width: scale(imgThumbSize)
|
||||
};
|
||||
|
||||
out.height = out.width;
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
utils.misc.getPreferredPictureDetail = function() {
|
||||
var imgDetailWidth = oauthflow.params.facebook.imgDetailWidth;
|
||||
return scale(imgDetailWidth);
|
||||
};
|
||||
|
||||
utils.misc.setTimestamp = function(type, callback) {
|
||||
ImportStatusData.put(type + LAST_IMPORT_TIMESTAMP_SUFFIX, Date.now())
|
||||
.then(callback);
|
||||
};
|
||||
|
||||
utils.misc.getTimestamp = function(type, callback) {
|
||||
ImportStatusData.get(type + LAST_IMPORT_TIMESTAMP_SUFFIX)
|
||||
.then(callback);
|
||||
};
|
||||
}
|
||||
196
gaia/shared/js/contacts/import/utilities/overlay.js
Executable file
196
gaia/shared/js/contacts/import/utilities/overlay.js
Executable file
|
|
@ -0,0 +1,196 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
(function() {
|
||||
utils.overlay = {};
|
||||
|
||||
var overlay = document.querySelector('#loading-overlay'),
|
||||
statusContainer = overlay.querySelector('p[role="status"]'),
|
||||
progressActivity = document.querySelector('#progress-activity'),
|
||||
progressTitle = document.querySelector('#progress-title'),
|
||||
progressElement = document.querySelector('#progress-element'),
|
||||
progressMsg = document.querySelector('#progress-msg'),
|
||||
menu = document.querySelector('#loading-overlay menu'),
|
||||
cancelButton = document.querySelector('#cancel-overlay');
|
||||
|
||||
// Constructor for the progress element
|
||||
function ProgressBar(pMsgId, pClass) {
|
||||
var counter = 0;
|
||||
var total = 0;
|
||||
var progressTextId = pMsgId || 'genericProgress';
|
||||
var clazz = pClass;
|
||||
|
||||
progressElement.setAttribute('value', 0);
|
||||
|
||||
function showMessage() {
|
||||
var attributes = progressTextId == 'infiniteProgress' ?
|
||||
{ n: counter } :
|
||||
{ current: counter, total: total };
|
||||
document.l10n.setAttributes(progressMsg,
|
||||
progressTextId,
|
||||
attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates progress bar and message. It takes an optional `value` parameter
|
||||
* to overwrite the internal counter with this value.
|
||||
* @param {Number} value Overrides internal counter.
|
||||
*/
|
||||
this.update = function(value) {
|
||||
if (value && value <= total && value >= counter) {
|
||||
counter = value;
|
||||
} else {
|
||||
counter++;
|
||||
}
|
||||
progressElement.setAttribute('value',
|
||||
((counter * 100) / total).toFixed()); // Percent fraction is not needed.
|
||||
showMessage();
|
||||
};
|
||||
|
||||
this.setTotal = function(ptotal) {
|
||||
total = ptotal;
|
||||
};
|
||||
|
||||
this.setClass = function(clazzName) {
|
||||
setClass(clazzName);
|
||||
clazz = clazzName;
|
||||
// To refresh the message according to the new clazzName
|
||||
if (clazzName === 'activityBar' || clazzName === 'spinner') {
|
||||
progressMsg.removeAttribute('data-l10n-id');
|
||||
}
|
||||
};
|
||||
|
||||
this.setHeaderMsg = function(headerMsgId) {
|
||||
progressTitle.setAttribute('data-l10n-id', headerMsgId);
|
||||
};
|
||||
} // ProgressBar
|
||||
|
||||
utils.overlay.isAnimationPlaying = false;
|
||||
utils.overlay.isShown = false;
|
||||
utils.overlay._show = function _show(messageId, progressClass, textId) {
|
||||
progressActivity.classList.remove('hide');
|
||||
if (typeof messageId === 'string') {
|
||||
progressTitle.setAttribute('data-l10n-id', messageId);
|
||||
} else {
|
||||
document.l10n.setAttributes(progressTitle,
|
||||
messageId.id,
|
||||
messageId.args);
|
||||
}
|
||||
progressMsg.textContent = null;
|
||||
progressMsg.removeAttribute('data-l10n-id');
|
||||
if (utils.overlay.isShown) {
|
||||
return;
|
||||
}
|
||||
overlay.classList.remove('hide');
|
||||
overlay.classList.remove('fade-out');
|
||||
overlay.classList.add('fade-in');
|
||||
utils.overlay.isAnimationPlaying = true;
|
||||
// Custom event that can be used to apply (screen reader) visibility
|
||||
// changes.
|
||||
window.dispatchEvent(new CustomEvent('loadingoverlayshowing'));
|
||||
overlay.addEventListener('animationend', function ov_onFadeIn(ev) {
|
||||
utils.overlay.isAnimationPlaying = false;
|
||||
overlay.removeEventListener('animationend', ov_onFadeIn);
|
||||
overlay.classList.remove('no-opacity');
|
||||
utils.overlay.isShown = true;
|
||||
});
|
||||
};
|
||||
|
||||
utils.overlay.show = function show(messageId, progressClass, textId) {
|
||||
var out;
|
||||
// In the case of an spinner this object will not be really used
|
||||
out = new ProgressBar(textId, progressClass);
|
||||
setClass(progressClass);
|
||||
utils.overlay.hideMenu(); // By default
|
||||
if (!utils.overlay.isAnimationPlaying) {
|
||||
utils.overlay._show(messageId, progressClass, textId);
|
||||
return out;
|
||||
}
|
||||
overlay.addEventListener('animationend',
|
||||
function ov_showWhenFinished(ev) {
|
||||
overlay.removeEventListener('animationend', ov_showWhenFinished);
|
||||
utils.overlay._show(messageId, progressClass, textId);
|
||||
}
|
||||
);
|
||||
return out;
|
||||
};
|
||||
|
||||
utils.overlay.showMenu = function showMenu() {
|
||||
menu.classList.add('showed');
|
||||
};
|
||||
|
||||
utils.overlay.hideMenu = function hideMenu() {
|
||||
menu.classList.remove('showed');
|
||||
};
|
||||
|
||||
Object.defineProperty(utils.overlay, 'oncancel', {
|
||||
set: function(cancelCb) {
|
||||
if (typeof cancelCb === 'function') {
|
||||
cancelButton.disabled = false;
|
||||
cancelButton.onclick = function on_cancel(e) {
|
||||
delete cancelButton.onclick;
|
||||
cancelCb();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function setAsProgress() {
|
||||
statusContainer.classList.remove('loading-icon');
|
||||
progressElement.setAttribute('max', '100');
|
||||
progressElement.setAttribute('value', '0');
|
||||
}
|
||||
|
||||
function setClass(clazzName) {
|
||||
switch (clazzName) {
|
||||
case 'spinner':
|
||||
progressElement.classList.remove('pack-activity');
|
||||
statusContainer.classList.add('loading-icon');
|
||||
progressElement.removeAttribute('max');
|
||||
progressElement.removeAttribute('value');
|
||||
break;
|
||||
case 'activityBar':
|
||||
case 'progressActivity':
|
||||
progressElement.classList.add('pack-activity');
|
||||
setAsProgress();
|
||||
break;
|
||||
case 'progressBar':
|
||||
progressElement.classList.remove('pack-activity');
|
||||
setAsProgress();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
utils.overlay._hide = function ov__hide() {
|
||||
if (!utils.overlay.isShown) {
|
||||
return;
|
||||
}
|
||||
overlay.classList.remove('fade-in');
|
||||
overlay.classList.add('fade-out');
|
||||
utils.overlay.isAnimationPlaying = true;
|
||||
// Custom event that can be used to apply (screen reader) visibility
|
||||
// changes.
|
||||
window.dispatchEvent(new CustomEvent('loadingoverlayhiding'));
|
||||
overlay.addEventListener('animationend', function ov_onFadeOut(ev) {
|
||||
utils.overlay.isAnimationPlaying = false;
|
||||
overlay.removeEventListener('animationend', ov_onFadeOut);
|
||||
progressActivity.classList.add('hide');
|
||||
overlay.classList.add('no-opacity');
|
||||
overlay.classList.add('hide');
|
||||
utils.overlay.isShown = false;
|
||||
});
|
||||
};
|
||||
utils.overlay.hide = function ov_hide() {
|
||||
if (!utils.overlay.isAnimationPlaying) {
|
||||
utils.overlay._hide();
|
||||
return;
|
||||
}
|
||||
overlay.addEventListener('animationend',
|
||||
function ov_hideWhenFinished(ev) {
|
||||
overlay.removeEventListener('animationend', ov_hideWhenFinished);
|
||||
utils.overlay._hide();
|
||||
}
|
||||
);
|
||||
};
|
||||
})();
|
||||
217
gaia/shared/js/contacts/import/utilities/sdcard.js
Executable file
217
gaia/shared/js/contacts/import/utilities/sdcard.js
Executable file
|
|
@ -0,0 +1,217 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.sdcard) {
|
||||
var SdCard = utils.sdcard = {
|
||||
NOT_INITIALIZED: 0,
|
||||
NOT_AVAILABLE: 1,
|
||||
AVAILABLE: 2,
|
||||
SHARED: 3,
|
||||
observers: {}
|
||||
};
|
||||
|
||||
SdCard.status = SdCard.NOT_INITIALIZED;
|
||||
SdCard.deviceStorages = navigator.getDeviceStorages('sdcard');
|
||||
SdCard.deviceStorage = Array.isArray(SdCard.deviceStorages) &&
|
||||
SdCard.deviceStorages[0];
|
||||
|
||||
SdCard._toStatus = function toStatus(state) {
|
||||
switch (state) {
|
||||
case 'available':
|
||||
SdCard.status = SdCard.AVAILABLE;
|
||||
break;
|
||||
// NOTE: The 'SHARED' state is equivalent to NOT_AVAILABLE, since we
|
||||
// could have inconsistencies if we allow changing the sdcard content
|
||||
// meanwhile exporting/importing.
|
||||
// It's needed to have it's own state solely for showing the proper
|
||||
// message when needed
|
||||
case 'shared':
|
||||
SdCard.status = SdCard.SHARED;
|
||||
break;
|
||||
case 'unavailable':
|
||||
case 'deleted':
|
||||
SdCard.status = SdCard.NOT_AVAILABLE;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SdCard.updateStorageState = function sd_updateStorageState(state) {
|
||||
SdCard._toStatus(state);
|
||||
|
||||
Object.keys(this.observers).forEach(function onObserver(name) {
|
||||
if (typeof(SdCard.observers[name]) === 'function') {
|
||||
SdCard.observers[name].call(null, state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (SdCard.deviceStorage) {
|
||||
SdCard.deviceStorage
|
||||
.addEventListener('change', function sd_deviceStorageChangeHandler(e) {
|
||||
SdCard.updateStorageState(e.reason);
|
||||
});
|
||||
|
||||
if (SdCard.status === SdCard.NOT_INITIALIZED) {
|
||||
SdCard.deviceStorage.available().onsuccess = (function(e) {
|
||||
SdCard.updateStorageState(e.target.result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is a SD card inserted in the device.
|
||||
* @return {Boolean} true if sdcard available, false otherwise.
|
||||
*/
|
||||
SdCard.checkStorageCard = function sd_checkStorageCard() {
|
||||
return SdCard.status === SdCard.AVAILABLE;
|
||||
};
|
||||
|
||||
SdCard.getStatus = function(cb) {
|
||||
if (!SdCard.deviceStorage) {
|
||||
SdCard._toStatus('unavailable');
|
||||
cb(SdCard.status);
|
||||
return;
|
||||
}
|
||||
|
||||
var req = SdCard.deviceStorage.available();
|
||||
|
||||
req.onsuccess = function() {
|
||||
SdCard._toStatus(req.result);
|
||||
cb(SdCard.status);
|
||||
};
|
||||
req.onerror = function() {
|
||||
console.error('Error while determining SD Status', req.error.name);
|
||||
cb(SdCard.status);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves files that adjust to the kind specified in the
|
||||
* parameters from the SD card inserted in the device.
|
||||
*
|
||||
* @param {String[]} mimes Possible mime types of the files to be found.
|
||||
* @param {String[]} exts Possible file extensions of the files to be found.
|
||||
* @param {Function} cb Callback.
|
||||
*/
|
||||
SdCard.retrieveFiles = function retrieveFilesContent(mimes, exts, cb) {
|
||||
var fileArray = [];
|
||||
var forbiddenDirectories = ['.Trash'];
|
||||
|
||||
// getDeviceStorages('sdcard') can return more than one storage unit
|
||||
// in different order, depends on the default media storage setting,
|
||||
// so we need to iterate though them to be sure we checked the proper
|
||||
// SD Card
|
||||
var storages = navigator.getDeviceStorages('sdcard');
|
||||
var numberOfStorages = storages.length;
|
||||
var currentStorage = 0;
|
||||
|
||||
var cursorOnError = function() {
|
||||
cb(this.error.name);
|
||||
};
|
||||
|
||||
// RegExp that checks if a string contains any of the extensions.
|
||||
var reExt = new RegExp('.(' + exts.join('|') + ')$');
|
||||
|
||||
var cursorOnSuccess = function(e) {
|
||||
var file = e.target.result;
|
||||
// If we don't have any more files on the storage...
|
||||
if (!file) {
|
||||
// ... but we have more storages...
|
||||
if (++currentStorage < numberOfStorages) {
|
||||
// ... check the next one
|
||||
cursor = storages[currentStorage].enumerate();
|
||||
cursor.onsuccess = cursorOnSuccess;
|
||||
cursor.onerror = cursorOnError;
|
||||
} else {
|
||||
cb(null, fileArray);
|
||||
}
|
||||
} else {
|
||||
if ((mimes.indexOf(file.type) === -1) && !reExt.test(file.name)) {
|
||||
cursor.continue();
|
||||
} else {
|
||||
if (forbiddenDirectories.indexOf(file.name) === -1) {
|
||||
fileArray.push(file);
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var cursor = storages[currentStorage].enumerate();
|
||||
cursor.onsuccess = cursorOnSuccess;
|
||||
cursor.onerror = cursorOnError;
|
||||
};
|
||||
|
||||
/**
|
||||
* Incrementally extracts vcard text from a file.
|
||||
*
|
||||
* @param {Object} file File Object.
|
||||
* @param {Function} cb Function to call for each chunk of vcards retrieved
|
||||
* from the file.
|
||||
*/
|
||||
SdCard.getTextFromFile = function(file, cb) {
|
||||
if (!file) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const END_VCARD = 'end:vcard';
|
||||
const CHUNK_SIZE = 1 * 1024 * 1024; // 1Mb
|
||||
|
||||
var fileReader = new FileReader();
|
||||
|
||||
var result = 0;
|
||||
|
||||
(function readChunk(offset, buffer) {
|
||||
if (offset >= file.size) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
var chunk = file.slice(offset, offset + CHUNK_SIZE);
|
||||
offset += CHUNK_SIZE;
|
||||
fileReader.readAsText(chunk);
|
||||
fileReader.onload = function() {
|
||||
buffer += fileReader.result;
|
||||
if (!buffer.match(/end:vcard/gi)) {
|
||||
readChunk(offset, buffer);
|
||||
return;
|
||||
}
|
||||
var lastContactInChunk =
|
||||
buffer.toLowerCase().lastIndexOf(END_VCARD) + END_VCARD.length;
|
||||
cb(buffer.substr(0, lastContactInChunk)).then(partialResult => {
|
||||
result += partialResult;
|
||||
readChunk(offset, buffer.substr(lastContactInChunk + 1));
|
||||
}).catch(error => {
|
||||
console.warn(error);
|
||||
});
|
||||
};
|
||||
})(0 /* initial offset */, '' /* initial empty buffer */);
|
||||
});
|
||||
};
|
||||
|
||||
// Subscribe a callback for sdcard changes. A name is needed
|
||||
// to refer to this observer.
|
||||
// @param {String} identifier for the observer
|
||||
// @param {Function} the callback to be invoqued on a change
|
||||
// @param {Boolean} if the callback exists, override it
|
||||
SdCard.subscribeToChanges = function(name, func, force) {
|
||||
if (this.observers[name] !== undefined && !force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.observers[name] = func;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Remove the subscription to listen to changes for the specific
|
||||
// identifier
|
||||
// @param {String} the identifier
|
||||
SdCard.unsubscribeToChanges = function(name) {
|
||||
if (this.observers[name]) {
|
||||
delete this.observers[name];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
95
gaia/shared/js/contacts/import/utilities/status.js
Executable file
95
gaia/shared/js/contacts/import/utilities/status.js
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
/* global LazyLoader */
|
||||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
utils.status = (function() {
|
||||
var STATUS_TIME = 2000;
|
||||
var statusMsg = document.querySelector('#statusMsg');
|
||||
var hidingTimeout;
|
||||
var transitionEndTimeout;
|
||||
// Additional paragraph just in case the status msg has more than one line
|
||||
var additionalLine;
|
||||
|
||||
var hideAnimationDone = function() {
|
||||
statusMsg.removeEventListener('transitionend', hideAnimationDone);
|
||||
statusMsg.classList.add('hidden');
|
||||
};
|
||||
var showAnimationDone = function() {
|
||||
if (transitionEndTimeout) {
|
||||
clearTimeout(transitionEndTimeout);
|
||||
}
|
||||
statusMsg.removeEventListener('transitionend', showAnimationDone);
|
||||
hidingTimeout = setTimeout(utils.status.hide, STATUS_TIME);
|
||||
};
|
||||
|
||||
var hideStatus = function() {
|
||||
statusMsg.addEventListener('transitionend', hideAnimationDone);
|
||||
statusMsg.classList.remove('opening');
|
||||
statusMsg.classList.remove('bannerStart');
|
||||
statusMsg.querySelector('p').removeAttribute('data-l10n-id');
|
||||
if (additionalLine) {
|
||||
statusMsg.removeChild(additionalLine);
|
||||
additionalLine = null;
|
||||
}
|
||||
};
|
||||
|
||||
var setL10nAttributes = function(node, l10n) {
|
||||
if (l10n && l10n.id) {
|
||||
navigator.mozL10n.setAttributes(node, l10n.id, l10n.args);
|
||||
return true;
|
||||
} else {
|
||||
console.error('Status arguments must be objects');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fills the DOM with the proper content and makes it visible
|
||||
* As parameters, it consums objects of the form
|
||||
* {
|
||||
* id: id,
|
||||
* args: args
|
||||
* }
|
||||
* @param mainMessage: the message to display
|
||||
* @param extra: an optional extra line for the message
|
||||
*/
|
||||
var showStatus = function(mainMessage, extra) {
|
||||
// clean listeners in case of previous race conditions
|
||||
statusMsg.removeEventListener('transitionend', showAnimationDone);
|
||||
statusMsg.removeEventListener('transitionend', hideAnimationDone);
|
||||
|
||||
LazyLoader.load([statusMsg], function _loaded() {
|
||||
// if parameters correct keep going
|
||||
if (!setL10nAttributes(statusMsg.querySelector('p'), mainMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for additional messages
|
||||
if (extra) {
|
||||
additionalLine = document.createElement('p');
|
||||
statusMsg.appendChild(additionalLine);
|
||||
setL10nAttributes(additionalLine, extra);
|
||||
}
|
||||
|
||||
// If showing already, we increase the time after the change
|
||||
if (statusMsg.classList.contains('opening')) {
|
||||
clearTimeout(hidingTimeout);
|
||||
hidingTimeout = setTimeout(hideStatus, STATUS_TIME);
|
||||
return;
|
||||
}
|
||||
|
||||
statusMsg.classList.remove('hidden');
|
||||
statusMsg.addEventListener('transitionend', showAnimationDone);
|
||||
setTimeout(function displaying() {
|
||||
statusMsg.classList.add('opening');
|
||||
statusMsg.classList.add('bannerStart');
|
||||
transitionEndTimeout = setTimeout(showAnimationDone, STATUS_TIME);
|
||||
}, 10); // Give the opportunity to paint the UI component
|
||||
});
|
||||
};
|
||||
return {
|
||||
show: showStatus,
|
||||
hide: hideStatus
|
||||
};
|
||||
})();
|
||||
885
gaia/shared/js/contacts/import/utilities/vcard_parser.js
Executable file
885
gaia/shared/js/contacts/import/utilities/vcard_parser.js
Executable file
|
|
@ -0,0 +1,885 @@
|
|||
/* global contacts, LazyLoader, utils, Rest, MimeMapper */
|
||||
/* global Matcher */
|
||||
/* exported VCFReader */
|
||||
|
||||
'use strict';
|
||||
|
||||
function parseDataUri(str) {
|
||||
var re = /^data:(.+);(charset=([^;]+))?;?base64,(.*)$/;
|
||||
var matches = re.exec(str);
|
||||
|
||||
if (matches) {
|
||||
return {
|
||||
mime: matches[1],
|
||||
charset: matches[3],
|
||||
value: matches[4]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureMimeType(type) {
|
||||
type = type.toLowerCase();
|
||||
if (!MimeMapper.isSupportedType(type)) {
|
||||
type = MimeMapper.guessTypeFromExtension(type) || type;
|
||||
}
|
||||
return type || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, expected to be in base64 format, an a
|
||||
* content type, returns the corresponding Blob.
|
||||
* If cannot be parsed, will return undefined.
|
||||
*
|
||||
* @param {string} data as a string.
|
||||
* @param {string} content type of the expected blob. Can be null.
|
||||
* @param {int} size of the slice to be parsed.
|
||||
*/
|
||||
function b64toBlob(b64Data, contentType, sliceSize) {
|
||||
if (!b64Data) {
|
||||
console.error('No b64 data provided to convert to blob.');
|
||||
return;
|
||||
}
|
||||
|
||||
contentType = contentType || '';
|
||||
sliceSize = sliceSize || 1024;
|
||||
|
||||
function charCodeFromCharacter(c) {
|
||||
return c.charCodeAt(0);
|
||||
}
|
||||
|
||||
var blob;
|
||||
try {
|
||||
var byteCharacters = atob(b64Data);
|
||||
var byteArrays = [];
|
||||
|
||||
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
var byteNumbers = [];
|
||||
for (var i = 0, l = slice.length; i < l; i++) {
|
||||
byteNumbers.push(charCodeFromCharacter(slice[i]));
|
||||
}
|
||||
var byteArray = new Uint8Array(byteNumbers);
|
||||
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
blob = new Blob(byteArrays, {type: contentType});
|
||||
} catch (e) {
|
||||
console.error('Error parsing base64 data');
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
var VCFReader = (function _VCFReader() {
|
||||
var ReBasic = /^([^:]+):(.+)$/;
|
||||
var ReTuple = /([a-zA-Z]+)=(.+)/;
|
||||
var WHITE_SPACE = ' ';
|
||||
|
||||
// Default tel type class
|
||||
var DEFAULT_PHONE_TYPE = 'other';
|
||||
|
||||
// basic type vcard to mobile type
|
||||
var VCARD_SIMPLE_TYPES = {
|
||||
'fax' : 'faxOther',
|
||||
'faxother' : 'faxOther',
|
||||
'home' : 'home',
|
||||
'internet' : 'internet',
|
||||
'cell' : 'mobile',
|
||||
'pager' : 'pager',
|
||||
'personal' : 'home',
|
||||
'pref' : 'pref',
|
||||
'text' : 'text',
|
||||
'textphone' : 'textphone',
|
||||
'voice' : 'voice',
|
||||
'work' : 'work'
|
||||
};
|
||||
|
||||
// complex type vcard to mobile type
|
||||
var VCARD_COMPLEX_TYPES = {
|
||||
'fax,work' : 'faxOffice',
|
||||
'fax,home' : 'faxHome',
|
||||
'voice,work' : 'work',
|
||||
'voice,home' : 'home'
|
||||
};
|
||||
|
||||
var REST_TIMEOUT = 5000;
|
||||
|
||||
function _parseTuple(p) {
|
||||
var match = p.match(ReTuple);
|
||||
return match ? [match[1].toLowerCase(), match[2]] : ['type', p];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line and creates an object with the line type (name), its meta
|
||||
* properties and its actual value.
|
||||
*
|
||||
* @param {string} line Line to be parsed from a VCF.
|
||||
* @return {{key: string, data: {meta, value}}}
|
||||
* @private
|
||||
*/
|
||||
function _parseLine(line) {
|
||||
var parsed = ReBasic.exec(line);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// `tuples` is an array containing all the extra properties
|
||||
var tuples = parsed[1].split(/[;,]/);
|
||||
var fieldName = tuples.shift().toLowerCase();
|
||||
// Value will be an empty array if there is only whitespace and
|
||||
// semi-colons.
|
||||
var fieldValue = /[^\s;]/.test(parsed[2]) ? parsed[2].split(';') : [];
|
||||
var meta = {
|
||||
type: []
|
||||
};
|
||||
|
||||
var len = tuples.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var tuple = _parseTuple(tuples[i]);
|
||||
if (tuple[0] === 'type') {
|
||||
meta.type.push(tuple[1]);
|
||||
} else {
|
||||
meta[tuple[0]] = tuple[1];
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a photo, we correct the value and type
|
||||
if (fieldName === 'photo') {
|
||||
if (meta.type.length) {
|
||||
meta.type = ensureMimeType(meta.type[0]);
|
||||
}
|
||||
|
||||
fieldValue = parsed[2];
|
||||
// vCard 3.0 expects a mandatory encoding = 'b' parameter for base64
|
||||
// encoded images, and vCard 2.1 expects a 'BASE64' one.
|
||||
if (meta.encoding && (meta.encoding.toLowerCase() === 'b' ||
|
||||
meta.encoding.toLowerCase() === 'base64')) {
|
||||
meta.encoding = 'base64';
|
||||
} else {
|
||||
var parsedUri = parseDataUri(fieldValue);
|
||||
if (parsedUri) {
|
||||
meta.type = ensureMimeType(parsedUri.mime);
|
||||
meta.encoding = 'base64';
|
||||
fieldValue = parsedUri.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: fieldName,
|
||||
data: {
|
||||
meta: meta,
|
||||
value: fieldValue
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse vCard entries split by lines and pass the converted object back to
|
||||
* the main thread.
|
||||
*
|
||||
* @param {string[][]} cardArray Array of array of strings representing vcard.
|
||||
* @param {function} cb Callback to call on finishe.
|
||||
*/
|
||||
var _parseEntries = function(cardArray, cb) {
|
||||
var parsedCards = [];
|
||||
|
||||
function sendIfFinished(contactObj) {
|
||||
parsedCards.push(contactObj);
|
||||
if (parsedCards.length === cardArray.length) {
|
||||
cb(parsedCards);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < cardArray.length; i++) {
|
||||
var lines = cardArray[i];
|
||||
// If there is no array of strings at cardArray[i] (representing a single
|
||||
// vcard), push a null value to account for a processed card.
|
||||
if (!lines) {
|
||||
sendIfFinished(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fields = {};
|
||||
var len = lines.length;
|
||||
|
||||
for (var j = 0; j < len; j++) {
|
||||
var line = lines[j];
|
||||
var parsedLine = _parseLine(line);
|
||||
if (!parsedLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fields[parsedLine.key]) {
|
||||
fields[parsedLine.key] = [];
|
||||
}
|
||||
|
||||
fields[parsedLine.key].push(parsedLine.data);
|
||||
}
|
||||
|
||||
// If there isn't either a First Name (fn) or a Name (n) this card is not
|
||||
// a valid one, ignore.
|
||||
if (!fields.fn && !fields.n) {
|
||||
sendIfFinished(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
vcardToContact(fields, sendIfFinished);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches Quoted-Printable characters in a string
|
||||
* @type {RegExp}
|
||||
*/
|
||||
var qpRegexp = /=([a-zA-Z0-9]{2})/g;
|
||||
|
||||
/**
|
||||
* Decodes a string encoded in Quoted-Printable format.
|
||||
* @param {string} str String to be decoded.
|
||||
* @return {string}
|
||||
*/
|
||||
var _decodeQuoted = function(str) {
|
||||
return decodeURIComponent(
|
||||
str.replace(qpRegexp, '%$1'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodes Quoted-Printable encoding into UTF-8
|
||||
* http://en.wikipedia.org/wiki/Quoted-printable
|
||||
*
|
||||
* @param {object} metaObj Checks for 'encoding' key to be quoted printable.
|
||||
* @param {string} value String to be decoded.
|
||||
* @return {string}
|
||||
*/
|
||||
function _decodeQP(metaObj, value) {
|
||||
var isQP = metaObj && metaObj.encoding &&
|
||||
(/quoted-printable/i).test(metaObj.encoding);
|
||||
|
||||
if (isQP) {
|
||||
value = _decodeQuoted(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
var NAME_PARTS = [
|
||||
'familyName',
|
||||
'givenName',
|
||||
'additionalName',
|
||||
'honorificPrefix',
|
||||
'honorificSuffix'
|
||||
];
|
||||
|
||||
/**
|
||||
* Takes an object with vCard properties and a mozContact object and returns
|
||||
* the latter with the computed name fields properly filled, inferred from
|
||||
* `vcardObj`.
|
||||
*
|
||||
* @param {Object} vcardObj
|
||||
* @param {Object} contactObj a mozContact to be filled with name fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
function _processName(vcardObj, contactObj) {
|
||||
// Set First Name right away as the 'name' property
|
||||
if (vcardObj.fn && vcardObj.fn.length) {
|
||||
var fnMeta = vcardObj.fn[0].meta;
|
||||
var fnValue = vcardObj.fn[0].value[0];
|
||||
// For the name field, we want the data as it is
|
||||
contactObj.name = [_decodeQP(fnMeta, fnValue)];
|
||||
}
|
||||
|
||||
if (vcardObj.n && vcardObj.n.length) {
|
||||
var values = vcardObj.n[0].value;
|
||||
var meta = vcardObj.n[0].meta;
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var namePart = values[i];
|
||||
if (namePart && NAME_PARTS[i]) {
|
||||
// For the rest of the fields, we want to keep the merged data
|
||||
// separated the same way, in different positions of the array.
|
||||
// see bug 981674
|
||||
contactObj[NAME_PARTS[i]] = _decodeQP(meta, namePart).split(',');
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a contact name at this point, make `name` be the
|
||||
// unification of all the name parts.
|
||||
if (!contactObj.name) {
|
||||
contactObj.name = [_decodeQP(meta, values.join(' ').trim())];
|
||||
}
|
||||
}
|
||||
contactObj.givenName = contactObj.givenName || contactObj.name;
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
var ADDR_PARTS = [null, null, 'streetAddress', 'locality', 'region',
|
||||
'postalCode', 'countryName'
|
||||
];
|
||||
|
||||
/**
|
||||
* Takes an object with vCard properties and a mozContact object and returns
|
||||
* the latter with the computed address fields properly filled, inferred from
|
||||
* `vcardObj`.
|
||||
*
|
||||
* @param {Object} vcardObj
|
||||
* @param {Object} contactObj a mozContact to be filled with name fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
function _processAddr(vcardObj, contactObj) {
|
||||
if (!vcardObj.adr) {
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
contactObj.adr = [];
|
||||
for (var i = 0; i < vcardObj.adr.length; i++) {
|
||||
var cur = {};
|
||||
var adr = vcardObj.adr[i];
|
||||
if (adr.meta && adr.meta.type) {
|
||||
cur.type = adr.meta.type;
|
||||
}
|
||||
|
||||
for (var j = 2; j < adr.value.length; j++) {
|
||||
var decoded = _decodeQP(adr.meta, adr.value[j]);
|
||||
// Because of adding empty fields while parsing the vCard
|
||||
// merging contacts sometimes doesn't work as expected
|
||||
// Check Bug 935636 for reference
|
||||
if (decoded !== '') {
|
||||
cur[ADDR_PARTS[j]] = decoded;
|
||||
}
|
||||
}
|
||||
|
||||
contactObj.adr.push(cur);
|
||||
}
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an object with vCard properties and a mozContact object and returns
|
||||
* the latter with the computed phone, email and url fields properly filled,
|
||||
* inferred from `vcardObj`.
|
||||
*
|
||||
* @param {Object} vcardObj
|
||||
* @param {Object} contactObj a mozContact to be filled with name fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
function _processComm(vcardObj, contactObj) {
|
||||
contactObj.tel = [];
|
||||
|
||||
['tel', 'email', 'url'].forEach(function field2field(field) {
|
||||
if (!vcardObj[field]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var len = vcardObj[field].length;
|
||||
var hasTypeMapper = function(x) {
|
||||
return x.trim().toLowerCase();
|
||||
};
|
||||
var notTypeMapper = function(v, key) {
|
||||
return v.meta[key].trim().toLowerCase();
|
||||
};
|
||||
var noType = function(field) {
|
||||
return field !== 'type';
|
||||
};
|
||||
var noPref = function(field) {
|
||||
return field !== 'pref';
|
||||
};
|
||||
var typeFilter = function(metaValue) {
|
||||
return !!VCARD_SIMPLE_TYPES[metaValue];
|
||||
};
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var v = vcardObj[field][i];
|
||||
var metaValues;
|
||||
var cur = {};
|
||||
|
||||
if (v.meta) {
|
||||
if (v.value) {
|
||||
cur.value = _decodeQP(v.meta, v.value[0]);
|
||||
cur.value = cur.value.replace(/^tel:/i, '');
|
||||
}
|
||||
|
||||
if (v.meta.type) {
|
||||
metaValues = ([].slice.call(v.meta.type)).map(hasTypeMapper);
|
||||
} else {
|
||||
metaValues = Object.keys(v.meta).filter(noType).map(
|
||||
notTypeMapper.bind(null, v));
|
||||
}
|
||||
|
||||
if (metaValues.indexOf('pref') !== -1) {
|
||||
cur.pref = true;
|
||||
metaValues = metaValues.filter(noPref);
|
||||
}
|
||||
|
||||
/*
|
||||
* metaValues is an array of types. If metaValues length is:
|
||||
* 0: it returns the default type (No type was defined),
|
||||
* 1: If the element matches a simple type, returns the simple type,
|
||||
* if not, it returns the default type
|
||||
* 2: If the elements match a complex type, it returns the complex
|
||||
* type. If not, then try to match the elements with a simple type,
|
||||
* in order. If one of the elements matches a simple type, it
|
||||
* returns the first element that matches a simple type or if there
|
||||
* exists no matching, it returns the default type value.
|
||||
* otherwise --> Returns the first element that matches a simple type
|
||||
* or if there exists no matching, it returns the default type value
|
||||
* */
|
||||
switch (metaValues.length) {
|
||||
case 0:
|
||||
cur.type = [DEFAULT_PHONE_TYPE];
|
||||
break;
|
||||
case 1:
|
||||
cur.type = [VCARD_SIMPLE_TYPES[metaValues[0]] ||
|
||||
DEFAULT_PHONE_TYPE];
|
||||
break;
|
||||
case 2:
|
||||
var complexType1 = metaValues[0] + ',' + metaValues[1];
|
||||
var complexType2 = metaValues[1] + ',' + metaValues[0];
|
||||
cur.type = [VCARD_COMPLEX_TYPES[complexType1] ||
|
||||
VCARD_COMPLEX_TYPES[complexType2] ||
|
||||
VCARD_SIMPLE_TYPES[metaValues[0]] ||
|
||||
VCARD_SIMPLE_TYPES[metaValues[1]] ||
|
||||
DEFAULT_PHONE_TYPE];
|
||||
break;
|
||||
default:
|
||||
cur.type = [
|
||||
VCARD_SIMPLE_TYPES[metaValues.filter(typeFilter).shift()] ||
|
||||
DEFAULT_PHONE_TYPE
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!contactObj[field]) {
|
||||
contactObj[field] = [];
|
||||
}
|
||||
|
||||
contactObj[field].push(cur);
|
||||
}
|
||||
});
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
function _isValidDate(dateValue) {
|
||||
return !isNaN(Date.parse(dateValue));
|
||||
}
|
||||
|
||||
function _processFields(vcardObj, contactObj) {
|
||||
['org', 'title', 'bday', 'anniversary'].forEach(function(field) {
|
||||
if (!vcardObj[field]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var v = vcardObj[field][0];
|
||||
|
||||
if (!v || !Array.isArray(v.value) || v.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dateValue;
|
||||
if (field === 'bday') {
|
||||
dateValue = v.value[0];
|
||||
if (_isValidDate(dateValue)) {
|
||||
contactObj.bday = new Date(dateValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'anniversary') {
|
||||
dateValue = v.value[0];
|
||||
if (_isValidDate(dateValue)) {
|
||||
contactObj.anniversary = new Date(dateValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'title') {
|
||||
field = 'jobTitle';
|
||||
}
|
||||
|
||||
switch (typeof v) {
|
||||
case 'object':
|
||||
contactObj[field] = [_decodeQP(v.meta, v.value[0])];
|
||||
break;
|
||||
case 'string':
|
||||
contactObj[field] = [v];
|
||||
break;
|
||||
}
|
||||
});
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
function _processPhoto(vcardObj, contactObj, cb) {
|
||||
if (!Array.isArray(vcardObj.photo) ||
|
||||
vcardObj.photo.length === 0 ||
|
||||
!vcardObj.photo[0] ||
|
||||
!vcardObj.photo[0].value) {
|
||||
return cb(contactObj);
|
||||
}
|
||||
|
||||
var photo = vcardObj.photo[0];
|
||||
var photoContents = photo.value.trim();
|
||||
if (!photoContents) {
|
||||
return cb(contactObj);
|
||||
}
|
||||
if (photoContents && photo.meta && photo.meta.encoding === 'base64') {
|
||||
var blob = b64toBlob(photoContents, photo.meta.type);
|
||||
if (blob) {
|
||||
utils.thumbnailImage(blob, function gotThumbnail(thumbnail) {
|
||||
if (blob !== thumbnail) {
|
||||
contactObj.photo = [blob, thumbnail];
|
||||
} else {
|
||||
contactObj.photo = [blob];
|
||||
}
|
||||
cb(contactObj);
|
||||
});
|
||||
} else {
|
||||
cb(contactObj);
|
||||
}
|
||||
}
|
||||
// Else we assume it is a http url
|
||||
else {
|
||||
var callbacks = {
|
||||
success: function(blobPic) {
|
||||
if (blobPic) {
|
||||
contactObj.photo = [blobPic];
|
||||
}
|
||||
cb(contactObj);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Error getting photo for contact');
|
||||
cb(contactObj);
|
||||
},
|
||||
timeout: function() {
|
||||
console.error('Timeout getting photo for contact');
|
||||
cb(contactObj);
|
||||
}
|
||||
};
|
||||
Rest.get(photoContents, callbacks, {
|
||||
responseType: 'blob',
|
||||
operationsTimeout: REST_TIMEOUT
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parsed vCard to a mozContact.
|
||||
*
|
||||
* @param {Object} vcard JSON representation of an vCard.
|
||||
* @param {Function} cb Function to call with the resulting moxContact object
|
||||
* @return {Object, null} An object implementing mozContact interface.
|
||||
*/
|
||||
var vcardToContact = function(vcard, cb) {
|
||||
if (!vcard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var obj = {};
|
||||
_processName(vcard, obj);
|
||||
_processAddr(vcard, obj);
|
||||
_processComm(vcard, obj);
|
||||
_processFields(vcard, obj);
|
||||
|
||||
_processPhoto(vcard, obj, function(contactObj) {
|
||||
cb(utils.misc.toMozContact(contactObj));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Class used to parse vCard files (http://tools.ietf.org/html/rfc6350).
|
||||
*
|
||||
* @param {String} contents vCard formatted text.
|
||||
* @constructor
|
||||
*/
|
||||
var VCFReader = function(contents) {
|
||||
this.contents = contents;
|
||||
this.processed = 0;
|
||||
this.finished = false;
|
||||
this.currentChar = 0;
|
||||
|
||||
this.numDupsMerged = 0;
|
||||
};
|
||||
|
||||
// Number of contacts processed at a given time.
|
||||
VCFReader.CONCURRENCY = 5;
|
||||
|
||||
/**
|
||||
* Used to stop contact processing.
|
||||
*/
|
||||
VCFReader.prototype.finish = function() {
|
||||
this.finished = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Starting point of vcard processing.
|
||||
* @param {function} cb Function to call after the process is finished.
|
||||
*/
|
||||
VCFReader.prototype.process = function(cb) {
|
||||
/**
|
||||
* Calculate the total amount of contacts to be imported. This number could
|
||||
* change in case there are vcards with syntax errors or that our processor
|
||||
* can't parse.
|
||||
*/
|
||||
var self = this;
|
||||
|
||||
var match = this.contents.match(/end:vcard/gi);
|
||||
// If there are no matches, then this probably isn't a vcard and we should
|
||||
// stop processing.
|
||||
if (!match) {
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.importedContacts = [];
|
||||
this.total = match.length;
|
||||
this.onread && this.onread(this.total);
|
||||
this.ondone = function(numImported) {
|
||||
cb(numImported, self.numDupsMerged);
|
||||
};
|
||||
|
||||
LazyLoader.load(['/shared/js/simple_phone_matcher.js',
|
||||
'/shared/js/mime_mapper.js',
|
||||
'/shared/js/contact_photo_helper.js',
|
||||
'/shared/js/contacts/import/utilities/misc.js',
|
||||
'/shared/js/contacts/contacts_matcher.js',
|
||||
'/shared/js/contacts/contacts_merger.js',
|
||||
'/shared/js/contacts/utilities/image_thumbnail.js',
|
||||
'/shared/js/contacts/merger_adapter.js',
|
||||
'/shared/js/contacts/utilities/http_rest.js'
|
||||
], function() {
|
||||
// Start processing the text
|
||||
// The data pump flows as follows:
|
||||
// 1) splitlines() reads char-by-char to split the text into lines
|
||||
// 2) each line is parsed to parseEntries() when we have accumulated
|
||||
// enough lines to represent CONCURRENCY cards.
|
||||
// 3) call post() for each contact found by parseEntries()
|
||||
// 4) perform matching for each contact
|
||||
// 5) save the contact if appropriate
|
||||
// 6) call onParsed() which either goes back to (4) for the next contact
|
||||
// or goes back to (1) to get the next set of lines
|
||||
//
|
||||
// Note: The async callbacks in the matching and mozContacts.save() code
|
||||
// prevent this code from being purely recursive.
|
||||
this.splitLines();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when every contact is effectively saved.
|
||||
*
|
||||
* @param {Error} err Error object in case there was one.
|
||||
* @param {mozContact} ct Contact that has been just saved.
|
||||
*/
|
||||
VCFReader.prototype.onParsed = function(err, ct) {
|
||||
this.processed += 1;
|
||||
this.importedContacts.push(ct);
|
||||
|
||||
this.onimported && this.onimported(ct && ct.name);
|
||||
if (this.finished || this.processed === this.total) {
|
||||
this.ondone(this.importedContacts);
|
||||
return;
|
||||
}
|
||||
|
||||
var processed = this.processed;
|
||||
if (processed < this.total && processed % VCFReader.CONCURRENCY === 0) {
|
||||
this.splitLines();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This will be called every time we manage to process a contact
|
||||
* @param {object[]} contactObjects Objects with contact structure.
|
||||
*/
|
||||
VCFReader.prototype.post = function(contactObjects) {
|
||||
var _onParsed = this.onParsed.bind(this);
|
||||
var cursor = 0;
|
||||
var self = this;
|
||||
|
||||
function afterSave(ct, e) {
|
||||
_onParsed(e, ct);
|
||||
|
||||
cursor += 1;
|
||||
if (cursor < contactObjects.length) {
|
||||
saveContact(contactObjects[cursor]);
|
||||
}
|
||||
}
|
||||
|
||||
function saveContact(ct) {
|
||||
if (!ct) {
|
||||
afterSave(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var contact = utils.misc.toMozContact(ct);
|
||||
var afterSaveFn = afterSave.bind(null, contact);
|
||||
var matchCbs = {
|
||||
onmatch: function(matches) {
|
||||
var callbacks = {
|
||||
success: function(mergedContact) {
|
||||
self.numDupsMerged++;
|
||||
afterSave(mergedContact, null);
|
||||
},
|
||||
error: afterSaveFn
|
||||
};
|
||||
contacts.adaptAndMerge(contact, matches, callbacks);
|
||||
},
|
||||
|
||||
onmismatch: function() {
|
||||
VCFReader._save(contact, afterSaveFn);
|
||||
}
|
||||
};
|
||||
Matcher.match(contact, 'passive', matchCbs);
|
||||
}
|
||||
|
||||
saveContact(contactObjects[cursor]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a single raw entry into the phone contacts
|
||||
*
|
||||
* @param {Object} item represents a single vCard entry.
|
||||
* @param {Function} cb Callback.
|
||||
*/
|
||||
VCFReader._save = function(item, cb) {
|
||||
var req = navigator.mozContacts.save(utils.misc.toMozContact(item));
|
||||
req.onsuccess = cb;
|
||||
req.onerror = cb;
|
||||
};
|
||||
|
||||
var reBeginCard = /begin:vcard$/i;
|
||||
var reEndCard = /end:vcard$/i;
|
||||
var reVersion = /^VERSION:([\d\.]*)/i;
|
||||
|
||||
/**
|
||||
* Splits vcard text into arrays of lines (one for each vcard field) and
|
||||
* sends an array of arrays of lines over to process.
|
||||
*/
|
||||
VCFReader.prototype.splitLines = function() {
|
||||
var currentLine = '';
|
||||
var currentVersion = 0;
|
||||
var inLabel = false;
|
||||
var multiline = false;
|
||||
|
||||
var cardArray = [
|
||||
[]
|
||||
];
|
||||
|
||||
/**
|
||||
* Number of cards processed. Quite faster than looking at `cardArray`
|
||||
* length.
|
||||
* @type {number}
|
||||
*/
|
||||
var cardsProcessed = 0;
|
||||
|
||||
// We start at the last cursor position
|
||||
var i = this.currentChar;
|
||||
|
||||
var callPost = this.post.bind(this);
|
||||
|
||||
for (var l = this.contents.length; i < l; i++) {
|
||||
this.currentChar = i;
|
||||
var ch = this.contents[i];
|
||||
if (currentVersion >= 4 && ch === '"') {
|
||||
inLabel = !inLabel;
|
||||
currentLine += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore beginning whitespace that indicates multiline field.
|
||||
if (multiline === true) {
|
||||
if ((/[\r\t\s\n]/).test(ch)) {
|
||||
continue;
|
||||
} else {
|
||||
//currentLine += '\n'
|
||||
multiline = false;
|
||||
}
|
||||
}
|
||||
|
||||
var next = this.contents[i + 1];
|
||||
if (inLabel || (ch !== '\n' && ch !== '\r')) {
|
||||
// If we have a quoted-printable sign for multiline (/=\n/) AND
|
||||
// we are not in a data URI field, ignore the character.
|
||||
if (!(/^PHOTO/i).test(currentLine) && ch === '=' && next &&
|
||||
next.search(/(\r|\n)/) !== -1) {
|
||||
multiline = true;
|
||||
continue;
|
||||
}
|
||||
currentLine += ch;
|
||||
|
||||
// Continue only if this is not the last char in the string
|
||||
if (i !== l - 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field is a photo, the field could be multiline, to know if the
|
||||
// following line is part of the photo, we must check if the first char
|
||||
// of the following line is a space.
|
||||
var firstCharNextLine = this.contents[i + 2];
|
||||
if ((firstCharNextLine === WHITE_SPACE) && (/[\r\t\s\n]/).test(next) &&
|
||||
(/^PHOTO/i).test(currentLine)) {
|
||||
multiline = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point, we know that ch is a newline, and in the vcard format,
|
||||
// if we have a space after a newline, it indicates multiline field.
|
||||
if (next && (next === WHITE_SPACE || next === '\t')) {
|
||||
multiline = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reBeginCard.test(currentLine)) {
|
||||
currentLine = '';
|
||||
currentVersion = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the current line indicates the end of a card,
|
||||
if (reEndCard.test(currentLine)) {
|
||||
cardsProcessed += 1;
|
||||
|
||||
if (cardsProcessed === VCFReader.CONCURRENCY ||
|
||||
(cardsProcessed + this.processed) === this.total) {
|
||||
_parseEntries(cardArray, callPost);
|
||||
break;
|
||||
}
|
||||
|
||||
currentLine = '';
|
||||
|
||||
cardArray.push([]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
var matches = reVersion.exec(currentLine);
|
||||
if (matches === null) {
|
||||
cardArray[cardArray.length - 1].push(currentLine);
|
||||
} else {
|
||||
currentVersion = parseFloat(matches[1] || '0');
|
||||
}
|
||||
}
|
||||
currentLine = '';
|
||||
}
|
||||
};
|
||||
|
||||
VCFReader._decodeQuoted = _decodeQuoted;
|
||||
VCFReader.processAddr = _processAddr;
|
||||
VCFReader.processName = _processName;
|
||||
VCFReader.vcardToContact = vcardToContact;
|
||||
|
||||
//Expose some utility methods
|
||||
VCFReader.utils = {
|
||||
parseDataUri: parseDataUri,
|
||||
b64toBlob: b64toBlob
|
||||
};
|
||||
|
||||
return VCFReader;
|
||||
})();
|
||||
758
gaia/shared/js/contacts/import/utilities/vcard_reader.js
Executable file
758
gaia/shared/js/contacts/import/utilities/vcard_reader.js
Executable file
|
|
@ -0,0 +1,758 @@
|
|||
/* global LazyLoader, utils, Rest, MimeMapper */
|
||||
/* exported VCardReader */
|
||||
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
/* WARNING this module LazyLoads these modules
|
||||
* '/shared/js/mime_mapper.js',
|
||||
* '/shared/js/contacts/utilities/image_thumbnail.js',
|
||||
* '/shared/js/contacts/import/utilities/misc.js',
|
||||
* '/shared/js/contacts/utilities/http_rest.js'
|
||||
*/
|
||||
var dependencies = [
|
||||
'/shared/js/mime_mapper.js',
|
||||
'/shared/js/contacts/utilities/image_thumbnail.js',
|
||||
'/shared/js/contacts/import/utilities/misc.js',
|
||||
'/shared/js/contacts/utilities/http_rest.js'
|
||||
];
|
||||
|
||||
var ReBasic = /^([^:]+):(.+)$/;
|
||||
var ReTuple = /([a-zA-Z]+)=(.+)/;
|
||||
var WHITE_SPACE = ' ';
|
||||
var reBeginCard = /begin:vcard$/i;
|
||||
var reEndCard = /end:vcard$/i;
|
||||
var reVersion = /^VERSION:/i;
|
||||
|
||||
// Default tel type class
|
||||
var DEFAULT_PHONE_TYPE = 'other';
|
||||
|
||||
// basic type vcard to mobile type
|
||||
var VCARD_SIMPLE_TYPES = {
|
||||
'fax' : 'faxOther',
|
||||
'faxother' : 'faxOther',
|
||||
'home' : 'home',
|
||||
'internet' : 'internet',
|
||||
'cell' : 'mobile',
|
||||
'pager' : 'pager',
|
||||
'personal' : 'home',
|
||||
'pref' : 'pref',
|
||||
'text' : 'text',
|
||||
'textphone' : 'textphone',
|
||||
'voice' : 'voice',
|
||||
'work' : 'work'
|
||||
};
|
||||
|
||||
// complex type vcard to mobile type
|
||||
var VCARD_COMPLEX_TYPES = {
|
||||
'fax,work' : 'faxOffice',
|
||||
'fax,home' : 'faxHome',
|
||||
'voice,work' : 'work',
|
||||
'voice,home' : 'home'
|
||||
};
|
||||
|
||||
var REST_TIMEOUT = 5000;
|
||||
|
||||
/**
|
||||
* VCardReader object. It allows to read sequentially a set of
|
||||
* vCard objects present on a vCard File.
|
||||
*
|
||||
* Example of use:
|
||||
*
|
||||
* var reader = new VCardReader(vcardData);
|
||||
* var cursor = reader.getAll();
|
||||
*
|
||||
* cursor.onsuccess = function(evt) {
|
||||
* var contact = evt.target.result;
|
||||
* if(contact) {
|
||||
* // do something
|
||||
* cursor.continue();
|
||||
* }
|
||||
* else {
|
||||
* // We've just finished
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
window.VCardReader = function VCardReader(data) {
|
||||
var content = data.replace(/\r?\n/g, '\n');
|
||||
var currentIndex = 0;
|
||||
|
||||
this.getAll = function() {
|
||||
return new Cursor();
|
||||
};
|
||||
|
||||
function Cursor() {
|
||||
var self = this,
|
||||
depsLoaded = false;
|
||||
|
||||
this._loadDependenciesAndRead = function() {
|
||||
if (depsLoaded) {
|
||||
self._doRead();
|
||||
} else {
|
||||
LazyLoader.load(dependencies, function() {
|
||||
depsLoaded = true;
|
||||
self._doRead();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this._doRead = function() {
|
||||
_readNext(function(contact) {
|
||||
self.result = contact;
|
||||
if (self.callback) {
|
||||
self.callback({
|
||||
target: self
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Object.defineProperty(this, 'onsuccess', {
|
||||
set: function(cb) {
|
||||
self.callback = cb;
|
||||
self._loadDependenciesAndRead();
|
||||
}
|
||||
});
|
||||
|
||||
this.continue = function() {
|
||||
// If there was no previous result. do nothing
|
||||
if (this.result) {
|
||||
this._loadDependenciesAndRead();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _getCardLines() {
|
||||
var currentLine = '';
|
||||
var inLabel = false;
|
||||
var multiline = false;
|
||||
|
||||
var lines = [];
|
||||
|
||||
// We start at the last cursor position
|
||||
var i = currentIndex;
|
||||
|
||||
for (var l = content.length; i < l; i++) {
|
||||
currentIndex = i;
|
||||
var ch = content[i];
|
||||
if (ch === '"') {
|
||||
inLabel = !inLabel;
|
||||
currentLine += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore beginning whitespace that indicates multiline field.
|
||||
if (multiline === true) {
|
||||
if ((/[\r\t\s\n]/).test(ch)) {
|
||||
continue;
|
||||
} else {
|
||||
multiline = false;
|
||||
}
|
||||
}
|
||||
|
||||
var next = content[i + 1];
|
||||
if (inLabel || (ch !== '\n' && ch !== '\r')) {
|
||||
// If we have a quoted-printable sign for multiline (/=\n/) AND
|
||||
// we are not in a data URI field, ignore the character.
|
||||
if (!(/^PHOTO/i).test(currentLine) && ch === '=' && next &&
|
||||
next.search(/(\r|\n)/) !== -1) {
|
||||
multiline = true;
|
||||
continue;
|
||||
}
|
||||
currentLine += ch;
|
||||
|
||||
// Continue only if this is not the last char in the string
|
||||
if (i !== l - 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field is a photo, the field could be multiline, to know if the
|
||||
// following line is part of the photo, we must check if the first char
|
||||
// of the following line is a space.
|
||||
var firstCharNextLine = content[i + 2];
|
||||
if ((firstCharNextLine === WHITE_SPACE) && (/[\r\t\s\n]/).test(next) &&
|
||||
(/^PHOTO/i).test(currentLine)) {
|
||||
multiline = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point, we know that ch is a newline, and in the vcard format,
|
||||
// if we have a space after a newline, it indicates multiline field.
|
||||
if (next && (next === WHITE_SPACE || next === '\t')) {
|
||||
multiline = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reBeginCard.test(currentLine)) {
|
||||
currentLine = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the current line indicates the end of a card,
|
||||
if (reEndCard.test(currentLine)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentLine && !reVersion.test(currentLine)) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
currentLine = '';
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
return lines;
|
||||
}
|
||||
|
||||
// look if there is more cards to read
|
||||
function _checkFinished() {
|
||||
return !content.substr(currentIndex).match(/end:vcard/gi);
|
||||
}
|
||||
|
||||
function _readNext(cb) {
|
||||
var fields = {};
|
||||
var lines;
|
||||
var line;
|
||||
var parsedLine;
|
||||
|
||||
if (_checkFinished()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
lines = _getCardLines();
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
line = lines[i];
|
||||
parsedLine = _parseLine(line);
|
||||
|
||||
if (parsedLine) {
|
||||
if (!fields[parsedLine.key]) {
|
||||
fields[parsedLine.key] = [];
|
||||
}
|
||||
fields[parsedLine.key].push(parsedLine.data);
|
||||
}
|
||||
}
|
||||
|
||||
// If there isn't either a First Name (fn) or a Name (n) this card is not
|
||||
// a valid one, ignore and try to parse the next one.
|
||||
if (!fields.fn && !fields.n) {
|
||||
console.warn('Invalid vCard found. Skipping');
|
||||
|
||||
if (_checkFinished()) {
|
||||
cb();
|
||||
} else {
|
||||
_readNext(function(contact) {
|
||||
cb(contact);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
vcardToContact(fields, cb);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseDataUri(str) {
|
||||
var re = /^data:(.+);(charset=([^;]+))?;?base64,(.*)$/;
|
||||
var matches = re.exec(str);
|
||||
|
||||
if (matches) {
|
||||
return {
|
||||
mime: matches[1],
|
||||
charset: matches[3],
|
||||
value: matches[4]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureMimeType(type) {
|
||||
type = type.toLowerCase();
|
||||
if (!MimeMapper.isSupportedType(type)) {
|
||||
type = MimeMapper.guessTypeFromExtension(type) || type;
|
||||
}
|
||||
return type || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, expected to be in base64 format, an a
|
||||
* content type, returns the corresponding Blob.
|
||||
* If cannot be parsed, will return undefined.
|
||||
*
|
||||
* @param {string} data as a string.
|
||||
* @param {string} content type of the expected blob. Can be null.
|
||||
* @param {int} size of the slice to be parsed.
|
||||
*/
|
||||
function b64toBlob(b64Data, contentType, sliceSize) {
|
||||
if (!b64Data) {
|
||||
console.error('No b64 data provided to convert to blob.');
|
||||
return;
|
||||
}
|
||||
|
||||
contentType = contentType || '';
|
||||
sliceSize = sliceSize || 1024;
|
||||
|
||||
function charCodeFromCharacter(c) {
|
||||
return c.charCodeAt(0);
|
||||
}
|
||||
|
||||
var blob;
|
||||
try {
|
||||
var byteCharacters = atob(b64Data);
|
||||
var byteArrays = [];
|
||||
|
||||
for (var offset = 0; offset < byteCharacters.length;
|
||||
offset += sliceSize) {
|
||||
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
var byteNumbers = [];
|
||||
for (var i = 0, l = slice.length; i < l; i++) {
|
||||
byteNumbers.push(charCodeFromCharacter(slice[i]));
|
||||
}
|
||||
var byteArray = new Uint8Array(byteNumbers);
|
||||
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
blob = new Blob(byteArrays, {type: contentType});
|
||||
} catch (e) {
|
||||
console.error('Error parsing base64 data');
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line and creates an object with the line type (name), its meta
|
||||
* properties and its actual value.
|
||||
*
|
||||
* @param {string} line Line to be parsed from a VCF.
|
||||
* @return {{key: string, data: {meta, value}}}
|
||||
* @private
|
||||
*/
|
||||
function _parseLine(line) {
|
||||
var parsed = ReBasic.exec(line);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// `tuples` is an array containing all the extra properties
|
||||
var tuples = parsed[1].split(/[;,]/);
|
||||
var fieldName = tuples.shift().toLowerCase();
|
||||
fieldName = fieldName.split('.').pop(); // If grouped element, get real name
|
||||
|
||||
// Value will be an empty array if there is only whitespace and
|
||||
// semi-colons.
|
||||
var fieldValue = /[^\s;]/.test(parsed[2]) ? parsed[2].split(';') : [];
|
||||
var meta = {
|
||||
type: []
|
||||
};
|
||||
|
||||
var len = tuples.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var tuple = _parseTuple(tuples[i]);
|
||||
if (tuple[0] === 'type') {
|
||||
meta.type.push(tuple[1]);
|
||||
} else {
|
||||
meta[tuple[0]] = tuple[1];
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a photo, we correct the value and type
|
||||
if (fieldName === 'photo') {
|
||||
if (meta.type.length) {
|
||||
meta.type = ensureMimeType(meta.type[0]);
|
||||
}
|
||||
|
||||
fieldValue = parsed[2];
|
||||
// vCard 3.0 expects a mandatory encoding = 'b' parameter for base64
|
||||
// encoded images, and vCard 2.1 expects a 'BASE64' one.
|
||||
if (meta.encoding && (meta.encoding.toLowerCase() === 'b' ||
|
||||
meta.encoding.toLowerCase() === 'base64')) {
|
||||
meta.encoding = 'base64';
|
||||
} else {
|
||||
var parsedUri = parseDataUri(fieldValue);
|
||||
if (parsedUri) {
|
||||
meta.type = ensureMimeType(parsedUri.mime);
|
||||
meta.encoding = 'base64';
|
||||
fieldValue = parsedUri.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: fieldName,
|
||||
data: {
|
||||
meta: meta,
|
||||
value: fieldValue
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _parseTuple(p) {
|
||||
var match = p.match(ReTuple);
|
||||
return match ? [match[1].toLowerCase(), match[2]] : ['type', p];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parsed vCard to a mozContact.
|
||||
*
|
||||
* @param {Object} vcard JSON representation of an vCard.
|
||||
* @param {Function} cb Function to call with the resulting moxContact object
|
||||
* @return {Object, null} An object implementing mozContact interface.
|
||||
*/
|
||||
function vcardToContact(vcard, cb) {
|
||||
if (!vcard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var obj = {};
|
||||
_processName(vcard, obj);
|
||||
_processAddr(vcard, obj);
|
||||
_processComm(vcard, obj);
|
||||
_processFields(vcard, obj);
|
||||
|
||||
_processPhoto(vcard, obj, function(contactObj) {
|
||||
cb(utils.misc.toMozContact(contactObj));
|
||||
});
|
||||
|
||||
return utils.misc.toMozContact(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches Quoted-Printable characters in a string
|
||||
* @type {RegExp}
|
||||
*/
|
||||
var qpRegexp = /=([a-zA-Z0-9]{2})/g;
|
||||
|
||||
/**
|
||||
* Decodes a string encoded in Quoted-Printable format.
|
||||
* @param {string} str String to be decoded.
|
||||
* @return {string}
|
||||
*/
|
||||
var _decodeQuoted = function(str) {
|
||||
return decodeURIComponent(
|
||||
str.replace(qpRegexp, '%$1'));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Decodes Quoted-Printable encoding into UTF-8
|
||||
* http://en.wikipedia.org/wiki/Quoted-printable
|
||||
*
|
||||
* @param {object} metaObj Checks for 'encoding' key to be quoted printable.
|
||||
* @param {string} value String to be decoded.
|
||||
* @return {string}
|
||||
*/
|
||||
function _decodeQP(metaObj, value) {
|
||||
var isQP = metaObj && metaObj.encoding &&
|
||||
(/quoted-printable/i).test(metaObj.encoding);
|
||||
|
||||
if (isQP) {
|
||||
value = _decodeQuoted(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
var NAME_PARTS = [
|
||||
'familyName',
|
||||
'givenName',
|
||||
'additionalName',
|
||||
'honorificPrefix',
|
||||
'honorificSuffix'
|
||||
];
|
||||
|
||||
/**
|
||||
* Takes an object with vCard properties and a mozContact object and returns
|
||||
* the latter with the computed name fields properly filled, inferred from
|
||||
* `vcardObj`.
|
||||
*
|
||||
* @param {Object} vcardObj
|
||||
* @param {Object} contactObj a mozContact to be filled with name fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
function _processName(vcardObj, contactObj) {
|
||||
// Set First Name right away as the 'name' property
|
||||
if (vcardObj.fn && vcardObj.fn.length) {
|
||||
var fnMeta = vcardObj.fn[0].meta;
|
||||
var fnValue = vcardObj.fn[0].value[0];
|
||||
// For the name field, we want the data as it is
|
||||
contactObj.name = [_decodeQP(fnMeta, fnValue)];
|
||||
}
|
||||
|
||||
if (vcardObj.n && vcardObj.n.length) {
|
||||
var values = vcardObj.n[0].value;
|
||||
var meta = vcardObj.n[0].meta;
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var namePart = values[i];
|
||||
if (namePart && NAME_PARTS[i]) {
|
||||
// For the rest of the fields, we want to keep the merged data
|
||||
// separated the same way, in different positions of the array.
|
||||
// see bug 981674
|
||||
contactObj[NAME_PARTS[i]] = _decodeQP(meta, namePart).split(',');
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a contact name at this point, make `name` be the
|
||||
// unification of all the name parts.
|
||||
if (!contactObj.name) {
|
||||
contactObj.name = [_decodeQP(meta, values.join(' ').trim())];
|
||||
}
|
||||
}
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
var ADDR_PARTS = [null, null, 'streetAddress', 'locality', 'region',
|
||||
'postalCode', 'countryName'
|
||||
];
|
||||
|
||||
/**
|
||||
* Takes an object with vCard properties and a mozContact object and returns
|
||||
* the latter with the computed address fields properly filled, inferred from
|
||||
* `vcardObj`.
|
||||
*
|
||||
* @param {Object} vcardObj
|
||||
* @param {Object} contactObj a mozContact to be filled with name fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
function _processAddr(vcardObj, contactObj) {
|
||||
if (!vcardObj.adr) {
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
contactObj.adr = [];
|
||||
for (var i = 0; i < vcardObj.adr.length; i++) {
|
||||
var cur = {};
|
||||
var adr = vcardObj.adr[i];
|
||||
if (adr.meta && adr.meta.type) {
|
||||
cur.type = adr.meta.type;
|
||||
}
|
||||
|
||||
for (var j = 2; j < adr.value.length; j++) {
|
||||
var decoded = _decodeQP(adr.meta, adr.value[j]);
|
||||
// Because of adding empty fields while parsing the vCard
|
||||
// merging contacts sometimes doesn't work as expected
|
||||
// Check Bug 935636 for reference
|
||||
if (decoded !== '') {
|
||||
cur[ADDR_PARTS[j]] = decoded;
|
||||
}
|
||||
}
|
||||
|
||||
contactObj.adr.push(cur);
|
||||
}
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an object with vCard properties and a mozContact object and returns
|
||||
* the latter with the computed phone, email and url fields properly filled,
|
||||
* inferred from `vcardObj`.
|
||||
*
|
||||
* @param {Object} vcardObj
|
||||
* @param {Object} contactObj a mozContact to be filled with name fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
function _processComm(vcardObj, contactObj) {
|
||||
contactObj.tel = [];
|
||||
|
||||
['tel', 'email', 'url'].forEach(function field2field(field) {
|
||||
if (!vcardObj[field]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var len = vcardObj[field].length;
|
||||
var hasTypeMapper = function(x) {
|
||||
return x.trim().toLowerCase();
|
||||
};
|
||||
var notTypeMapper = function(v, key) {
|
||||
return v.meta[key].trim().toLowerCase();
|
||||
};
|
||||
var noType = function(field) {
|
||||
return field !== 'type';
|
||||
};
|
||||
var noPref = function(field) {
|
||||
return field !== 'pref';
|
||||
};
|
||||
var typeFilter = function(metaValue) {
|
||||
return !!VCARD_SIMPLE_TYPES[metaValue];
|
||||
};
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var v = vcardObj[field][i];
|
||||
var metaValues;
|
||||
var cur = {};
|
||||
|
||||
if (v.meta) {
|
||||
if (v.value) {
|
||||
cur.value = _decodeQP(v.meta, v.value[0]);
|
||||
cur.value = cur.value.replace(/^tel:/i, '');
|
||||
}
|
||||
|
||||
if (v.meta.type) {
|
||||
metaValues = ([].slice.call(v.meta.type)).map(hasTypeMapper);
|
||||
} else {
|
||||
metaValues = Object.keys(v.meta).filter(noType).map(
|
||||
notTypeMapper.bind(null, v));
|
||||
}
|
||||
|
||||
if (metaValues.indexOf('pref') !== -1) {
|
||||
cur.pref = true;
|
||||
metaValues = metaValues.filter(noPref);
|
||||
}
|
||||
|
||||
/*
|
||||
* metaValues is an array of types. If metaValues length is:
|
||||
* 0: it returns the default type (No type was defined),
|
||||
* 1: If the element matches a simple type, returns the simple type,
|
||||
* if not, it returns the default type
|
||||
* 2: If the elements match a complex type, it returns the complex
|
||||
* type. If not, then try to match the elements with a simple type,
|
||||
* in order. If one of the elements matches a simple type, it
|
||||
* returns the first element that matches a simple type or if there
|
||||
* exists no matching, it returns the default type value.
|
||||
* otherwise --> Returns the first element that matches a simple type
|
||||
* or if there exists no matching, it returns the default type value
|
||||
* */
|
||||
switch (metaValues.length) {
|
||||
case 0:
|
||||
cur.type = [DEFAULT_PHONE_TYPE];
|
||||
break;
|
||||
case 1:
|
||||
cur.type = [VCARD_SIMPLE_TYPES[metaValues[0]] ||
|
||||
DEFAULT_PHONE_TYPE];
|
||||
break;
|
||||
case 2:
|
||||
var complexType1 = metaValues[0] + ',' + metaValues[1];
|
||||
var complexType2 = metaValues[1] + ',' + metaValues[0];
|
||||
cur.type = [VCARD_COMPLEX_TYPES[complexType1] ||
|
||||
VCARD_COMPLEX_TYPES[complexType2] ||
|
||||
VCARD_SIMPLE_TYPES[metaValues[0]] ||
|
||||
VCARD_SIMPLE_TYPES[metaValues[1]] ||
|
||||
DEFAULT_PHONE_TYPE];
|
||||
break;
|
||||
default:
|
||||
cur.type = [
|
||||
VCARD_SIMPLE_TYPES[metaValues.filter(typeFilter).shift()] ||
|
||||
DEFAULT_PHONE_TYPE
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!contactObj[field]) {
|
||||
contactObj[field] = [];
|
||||
}
|
||||
|
||||
contactObj[field].push(cur);
|
||||
}
|
||||
});
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
function _isValidDate(dateValue) {
|
||||
return !isNaN(Date.parse(dateValue));
|
||||
}
|
||||
|
||||
function _processFields(vcardObj, contactObj) {
|
||||
['org', 'title', 'bday', 'anniversary'].forEach(function(field) {
|
||||
if (!vcardObj[field]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var v = vcardObj[field][0];
|
||||
|
||||
var dateValue;
|
||||
if (field === 'bday') {
|
||||
dateValue = v.value[0];
|
||||
if (_isValidDate(dateValue)) {
|
||||
contactObj.bday = new Date(dateValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'anniversary') {
|
||||
dateValue = v.value[0];
|
||||
if (_isValidDate(dateValue)) {
|
||||
contactObj.anniversary = new Date(dateValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'title') {
|
||||
field = 'jobTitle';
|
||||
}
|
||||
|
||||
switch (typeof v) {
|
||||
case 'object':
|
||||
contactObj[field] = [_decodeQP(v.meta, v.value[0])];
|
||||
break;
|
||||
case 'string':
|
||||
contactObj[field] = [v];
|
||||
break;
|
||||
}
|
||||
});
|
||||
return contactObj;
|
||||
}
|
||||
|
||||
function _processPhoto(vcardObj, contactObj, cb) {
|
||||
if (!Array.isArray(vcardObj.photo) ||
|
||||
vcardObj.photo.length === 0 ||
|
||||
!vcardObj.photo[0] ||
|
||||
!vcardObj.photo[0].value) {
|
||||
return cb(contactObj);
|
||||
}
|
||||
|
||||
var photo = vcardObj.photo[0];
|
||||
var photoContents = photo.value.trim();
|
||||
if (!photoContents) {
|
||||
return cb(contactObj);
|
||||
}
|
||||
if (photoContents && photo.meta && photo.meta.encoding === 'base64') {
|
||||
var blob = b64toBlob(photoContents, photo.meta.type);
|
||||
if (blob) {
|
||||
utils.thumbnailImage(blob, function gotThumbnail(thumbnail) {
|
||||
if (blob !== thumbnail) {
|
||||
contactObj.photo = [blob, thumbnail];
|
||||
} else {
|
||||
contactObj.photo = [blob];
|
||||
}
|
||||
cb(contactObj);
|
||||
});
|
||||
} else {
|
||||
cb(contactObj);
|
||||
}
|
||||
}
|
||||
// Else we assume it is a http url
|
||||
else {
|
||||
var callbacks = {
|
||||
success: function(blobPic) {
|
||||
if (blobPic) {
|
||||
contactObj.photo = [blobPic];
|
||||
}
|
||||
cb(contactObj);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Error getting photo for contact');
|
||||
cb(contactObj);
|
||||
},
|
||||
timeout: function() {
|
||||
console.error('Timeout getting photo for contact');
|
||||
cb(contactObj);
|
||||
}
|
||||
};
|
||||
Rest.get(photoContents, callbacks, {
|
||||
responseType: 'blob',
|
||||
operationsTimeout: REST_TIMEOUT
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.VCardReader.b64toBlob = b64toBlob;
|
||||
window.VCardReader.parseDataUri = parseDataUri;
|
||||
|
||||
})();
|
||||
24
gaia/shared/js/contacts/merger_adapter.js
Executable file
24
gaia/shared/js/contacts/merger_adapter.js
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
var contacts = window.contacts || {};
|
||||
|
||||
contacts.adaptAndMerge = function(incomingContact, matches, callbacks) {
|
||||
var listIds = Object.keys(matches);
|
||||
var totalMatches = listIds.length;
|
||||
|
||||
// First contact here we take as the master
|
||||
var masterContact = matches[listIds[0]].matchingContact;
|
||||
var matchingContacts = [];
|
||||
for (var j = 1; j < totalMatches; j++) {
|
||||
matchingContacts.push(matches[listIds[j]]);
|
||||
}
|
||||
// Finally the last matching is the incoming itself
|
||||
// XXX: it has no `matchings` entry as it does not come from the matching
|
||||
// algorithm.
|
||||
// Now only used here but consider to provide a constructor in the future.
|
||||
matchingContacts.push({
|
||||
matchingContact: incomingContact
|
||||
});
|
||||
|
||||
contacts.Merger.merge(masterContact, matchingContacts, callbacks);
|
||||
};
|
||||
182
gaia/shared/js/contacts/multi_contact.js
Executable file
182
gaia/shared/js/contacts/multi_contact.js
Executable file
|
|
@ -0,0 +1,182 @@
|
|||
'use strict';
|
||||
|
||||
/* exported MultiContact */
|
||||
/* globals Promise, LazyLoader, contacts */
|
||||
|
||||
// ATTENTION: This library lazy loads contacts_merger.js
|
||||
|
||||
var MultiContact = (function() {
|
||||
var datastores = Object.create(null);
|
||||
var datastoresLoading = false;
|
||||
var DS_READY_EVENT = 'ds_ready';
|
||||
|
||||
var MOZ_CONTACTS_OWNER = 'app://communications.gaiamobile.org';
|
||||
|
||||
function getDatastore(owner) {
|
||||
if (datastores[owner]) {
|
||||
return Promise.resolve(datastores[owner]);
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (datastoresLoading === true) {
|
||||
document.addEventListener(DS_READY_EVENT, function handler() {
|
||||
document.removeEventListener(DS_READY_EVENT, handler);
|
||||
resolve(datastores[owner]);
|
||||
});
|
||||
}
|
||||
else {
|
||||
datastoresLoading = true;
|
||||
|
||||
navigator.getDataStores('contacts').then(function success(dsList) {
|
||||
dsList.forEach(function(aDs) {
|
||||
datastores[aDs.owner] = aDs;
|
||||
});
|
||||
// This is needed because mozContact DB is not exposed as a Datastore
|
||||
// Once bug 1016838 lands this will not be needed
|
||||
datastores[MOZ_CONTACTS_OWNER] = new MozContactsDatastore();
|
||||
resolve(datastores[owner]);
|
||||
datastoresLoading = false;
|
||||
document.dispatchEvent(new CustomEvent(DS_READY_EVENT));
|
||||
}, function err(error) {
|
||||
console.error('Error while obtaining datastores: ', error.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Adapter object to obtain data from the mozContacts as if it were a DS
|
||||
// Once bug 1016838 lands this will not be needed
|
||||
function MozContactsDatastore() {
|
||||
}
|
||||
|
||||
MozContactsDatastore.prototype = {
|
||||
get: function(id) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
var options = {
|
||||
filterBy: ['id'],
|
||||
filterOp: 'equals',
|
||||
filterValue: id
|
||||
};
|
||||
|
||||
var req = navigator.mozContacts.find(options);
|
||||
|
||||
req.onsuccess = function() {
|
||||
resolve(JSON.parse(JSON.stringify(req.result[0])));
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
},
|
||||
get name() {
|
||||
return 'mozContacts';
|
||||
}
|
||||
};
|
||||
|
||||
function getData(entry) {
|
||||
if (!entry || !entry.id || !Array.isArray(entry.entryData)) {
|
||||
return Promise.reject({
|
||||
name: 'InvalidEntry'
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var operations = [];
|
||||
|
||||
var entryData = entry.entryData;
|
||||
|
||||
var mozContactId;
|
||||
entryData.forEach(function fetchEntry(aEntry) {
|
||||
var owner = aEntry.origin;
|
||||
if (owner === MOZ_CONTACTS_OWNER) {
|
||||
mozContactId = aEntry.uid;
|
||||
}
|
||||
|
||||
getDatastore(owner).then(function success(datastore) {
|
||||
operations.push(datastore.get(aEntry.uid));
|
||||
// It is needed to wait to have all operations ready
|
||||
if (operations.length === entryData.length) {
|
||||
execute(operations, resolve, reject, {
|
||||
targetId: entry.id,
|
||||
mozContactId: mozContactId
|
||||
});
|
||||
}
|
||||
}, function error(err) {
|
||||
console.error('Error while obtaining datastore: ', err.name);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function execute(operations, resolve, reject, options) {
|
||||
Promise.all(operations).then(function success(results) {
|
||||
if (results.length === 1) {
|
||||
results[0].id = options.targetId;
|
||||
resolve(results[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.mozContactId) {
|
||||
results = reorderResults(results, options.mozContactId);
|
||||
}
|
||||
|
||||
LazyLoader.load('/shared/js/contacts/contacts_merger.js',
|
||||
function() {
|
||||
var matchings = createMatchingContacts(results);
|
||||
|
||||
contacts.Merger.inMemoryMerge(results[0], matchings).then(
|
||||
function success(mergedResult) {
|
||||
mergedResult.id = options.targetId;
|
||||
resolve(mergedResult);
|
||||
}, function error(err) {
|
||||
console.log('Error while merging: ', err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}, function error(err) {
|
||||
console.error('Error while getting data: ', err.name);
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// This function reorders the contact data results array in order to
|
||||
// put the mozContact in the first position, as mozContact data will be taken
|
||||
// precedence over other datastore data
|
||||
function reorderResults(results, mozContactId) {
|
||||
var out = [];
|
||||
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
if (results[j].id === mozContactId) {
|
||||
out.unshift(results[j]);
|
||||
}
|
||||
else {
|
||||
out.push(results[j]);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// This function adapts the result array to the input object expected by
|
||||
// the contacts_merger module
|
||||
function createMatchingContacts(results) {
|
||||
var out = [];
|
||||
|
||||
for (var j = 1; j < results.length; j++) {
|
||||
out.push({
|
||||
matchingContact: results[j]
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
return {
|
||||
'getData': getData
|
||||
};
|
||||
})();
|
||||
696
gaia/shared/js/contacts/search.js
Executable file
696
gaia/shared/js/contacts/search.js
Executable file
|
|
@ -0,0 +1,696 @@
|
|||
'use strict';
|
||||
/* global Normalizer */
|
||||
/* global utils */
|
||||
/* global HtmlHelper */
|
||||
/* global LazyLoader */
|
||||
/* global ImageLoader */
|
||||
/* global fb */
|
||||
/* exported Search */
|
||||
|
||||
|
||||
(function(exports) {
|
||||
var inSearchMode = false,
|
||||
searchView,
|
||||
searchBox,
|
||||
searchList,
|
||||
searchNoResult,
|
||||
searchProgress,
|
||||
searchTimer = null,
|
||||
contactNodes = null,
|
||||
selectableForm = null,
|
||||
// On the steady state holds the list result of the current search
|
||||
searchableNodes = null,
|
||||
currentTextToSearch = '',
|
||||
currentSearchTerms = [],
|
||||
prevTextToSearch = '',
|
||||
// Pointer to the nodes which are currently on the result list
|
||||
currentSet = {},
|
||||
searchTextCache = {},
|
||||
canReuseSearchables = false,
|
||||
blurList = false,
|
||||
theClones = {},
|
||||
CHUNK_SIZE = 10,
|
||||
// Default to invalid page size and recalculate when first row added
|
||||
searchPageSize = -1,
|
||||
// Limit search result to hardLimit contacts, recalc with page size
|
||||
hardLimit = 25,
|
||||
emptySearch = true,
|
||||
remainingPending = true,
|
||||
imgLoader,
|
||||
searchEnabled = false,
|
||||
source = null,
|
||||
navigationController = null;
|
||||
|
||||
// The _source argument should be an adapter object that provides access
|
||||
// to the contact nodes in the app. This is done by defining the following
|
||||
// functions on the adapter object:
|
||||
// getNodes() An Array of all contact DOM nodes
|
||||
// getFirstNode() First contact DOM node
|
||||
// getNextNode(node) Given a node, find the next node
|
||||
// expectMoreNodes() True if nodes will be added via appendNodes()
|
||||
// clone(node) Clone the given contact node
|
||||
// getNodeById(id) Get the node matching the given ID, or null
|
||||
// getSearchText(node) Get the search text from the given node
|
||||
// click(event) Click event handler to use
|
||||
var init = function load(_source, defaultEnabled, navigation) {
|
||||
|
||||
searchView = document.getElementById('search-view');
|
||||
searchList = document.getElementById('search-list');
|
||||
|
||||
selectableForm = document.getElementById('selectable-form');
|
||||
|
||||
if (!_source) {
|
||||
throw new Error('Search requires a contact source!');
|
||||
}
|
||||
|
||||
source = _source;
|
||||
|
||||
if (typeof source.click === 'function') {
|
||||
searchList.addEventListener('click', source.click);
|
||||
}
|
||||
|
||||
searchEnabled = !!defaultEnabled;
|
||||
|
||||
navigationController = navigation || window.MainNavigation;
|
||||
};
|
||||
|
||||
var initialized = false;
|
||||
|
||||
var ignoreReturnKey = function ignoreReturnKey(evt) {
|
||||
if (evt.keyCode == 13) { // VK_Return
|
||||
evt.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
var doInit = function doInit() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
utils.listeners.add({
|
||||
'#cancel-search': exitSearchMode,
|
||||
'#search-contact': [
|
||||
{
|
||||
event: 'keypress',
|
||||
handler: ignoreReturnKey
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
searchBox = document.getElementById('search-contact');
|
||||
var resetButton = searchBox.nextElementSibling;
|
||||
resetButton.addEventListener('ontouchstart' in window ? 'touchstart' :
|
||||
'mousedown', function() {
|
||||
searchBox.value = '';
|
||||
searchBox.focus();
|
||||
resetState();
|
||||
window.setTimeout(fillInitialSearchPage, 0);
|
||||
});
|
||||
|
||||
searchList.parentNode.addEventListener('touchstart', function() {
|
||||
if (searchableNodes && remainingPending) {
|
||||
addRemainingResults(searchableNodes, searchPageSize);
|
||||
}
|
||||
blurList = true;
|
||||
});
|
||||
searchNoResult = document.getElementById('no-result');
|
||||
searchProgress = document.getElementById('search-progress');
|
||||
searchBox.addEventListener('blur', function() {
|
||||
window.setTimeout(onSearchBlur, 0);
|
||||
});
|
||||
|
||||
searchBox.addEventListener('focus', function() {
|
||||
blurList = false;
|
||||
});
|
||||
|
||||
LazyLoader.load([
|
||||
'/contacts/js/fb_resolver.js',
|
||||
'/shared/js/contacts/utilities/image_loader.js'
|
||||
], function() {
|
||||
imgLoader = new ImageLoader('#groups-list-search', 'li');
|
||||
imgLoader.setResolver(fb.resolver);
|
||||
});
|
||||
};
|
||||
|
||||
var clearHighlights = function(node) {
|
||||
// We traverse the DOM tree and remove highlighting marks.
|
||||
// getElements instead of querySelector here because of
|
||||
// performance.
|
||||
var highlights = node.getElementsByTagName('mark');
|
||||
while(highlights.length) {
|
||||
var parent = highlights[0].parentNode;
|
||||
while(highlights[0].firstChild) {
|
||||
parent.insertBefore(highlights[0].firstChild, highlights[0]);
|
||||
}
|
||||
|
||||
// This removes the item from 'highlights' HTMLCollection as well
|
||||
// via live DOM updating.
|
||||
parent.removeChild(highlights[0]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a contact DOM node, this function highlights the search terms it
|
||||
* contains. First, it extract the text contents, highlights every chunk and
|
||||
* then rebuild the content of the node respecting the location of the
|
||||
* <strong> tag. To know where every chunk starts and ends, we use the
|
||||
* locateHTMLTag so we know what we have to highlight.
|
||||
*
|
||||
* Highlighting is done in the createHighlightHTML() function inside the
|
||||
* HtmlHelper module.
|
||||
*/
|
||||
var highlightNode = function(node) {
|
||||
function doHighlight(text) {
|
||||
if (text === '' || text === ' ') {
|
||||
return document.createTextNode('');
|
||||
}
|
||||
|
||||
return HtmlHelper.createHighlightHTML(text, currentSearchTerms);
|
||||
}
|
||||
|
||||
function locateHTMLTag(text) {
|
||||
var tagLocation = [];
|
||||
tagLocation.push(text.indexOf('<'));
|
||||
tagLocation.push(text.indexOf('>', tagLocation[0]) + 1);
|
||||
tagLocation.push(text.indexOf('<', tagLocation[1]));
|
||||
tagLocation.push(text.indexOf('>', tagLocation[2]) + 1);
|
||||
return tagLocation;
|
||||
}
|
||||
|
||||
var textNode = node.querySelector('.contact-text bdi');
|
||||
if (textNode === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var text = textNode.innerHTML;
|
||||
var tagPos = locateHTMLTag(text);
|
||||
var beforeTag = text.substring(0, tagPos[0]);
|
||||
var inTag = text.substring(tagPos[1], tagPos[2]);
|
||||
var afterTag = text.substring(tagPos[3]);
|
||||
var realText = [beforeTag, inTag, afterTag];
|
||||
|
||||
if (tagPos[0] == -1) {
|
||||
var content = doHighlight(Normalizer.unescapeHTML(text));
|
||||
|
||||
// clear the previous content (if any)
|
||||
textNode.textContent = '';
|
||||
textNode.appendChild(content);
|
||||
} else {
|
||||
for (var i = 0, len = realText.length; i < len; i++) {
|
||||
realText[i] = doHighlight(Normalizer.unescapeHTML(realText[i]));
|
||||
}
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
var strongTag = document.createElement('strong');
|
||||
strongTag.appendChild(realText[1]);
|
||||
|
||||
fragment.appendChild(realText[0]);
|
||||
fragment.appendChild(strongTag);
|
||||
fragment.appendChild(realText[2]);
|
||||
|
||||
// clear the previous content (if any)
|
||||
textNode.textContent = '';
|
||||
textNode.appendChild(fragment);
|
||||
}
|
||||
};
|
||||
|
||||
var updateSearchList = function updateSearchList(cb) {
|
||||
if (!inSearchMode) {
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(function() {
|
||||
// Resetting state
|
||||
resetStateAndCache();
|
||||
|
||||
search(cb);
|
||||
});
|
||||
};
|
||||
|
||||
var resetStateAndCache = function resetStateAndCache() {
|
||||
contactNodes = null;
|
||||
searchTextCache = {};
|
||||
resetState();
|
||||
};
|
||||
|
||||
|
||||
// Search mode instructions
|
||||
var exitSearchMode = function exitSearchMode(evt) {
|
||||
if (evt) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
searchView.classList.remove('insearchmode');
|
||||
if (selectableForm) {
|
||||
selectableForm.classList.remove('insearchmode');
|
||||
}
|
||||
|
||||
if (navigationController) {
|
||||
navigationController.back();
|
||||
}
|
||||
|
||||
window.setTimeout(function exit_search() {
|
||||
hideProgressResults();
|
||||
|
||||
searchBox.value = '';
|
||||
|
||||
// Resetting state
|
||||
contactNodes = null;
|
||||
searchTextCache = {};
|
||||
resetState();
|
||||
|
||||
inSearchMode = false;
|
||||
}, 0);
|
||||
|
||||
window.removeEventListener('input', onInput);
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
prevTextToSearch = '';
|
||||
currentTextToSearch = '';
|
||||
currentSearchTerms = [];
|
||||
searchableNodes = null;
|
||||
canReuseSearchables = false;
|
||||
currentSet = {};
|
||||
// We don't know if the user will launch a new search later
|
||||
theClones = {};
|
||||
utils.dom.removeChildNodes(searchList);
|
||||
emptySearch = true;
|
||||
remainingPending = true;
|
||||
}
|
||||
|
||||
function addRemainingResults(nodes, from) {
|
||||
if (remainingPending !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
for (var i = from; i < hardLimit && i < nodes.length; i++) {
|
||||
var node = nodes[i].node;
|
||||
var clon = getClone(node);
|
||||
highlightNode(clon);
|
||||
fragment.appendChild(clon);
|
||||
currentSet[node.dataset.uuid] = clon;
|
||||
}
|
||||
|
||||
if (fragment.hasChildNodes()) {
|
||||
searchList.appendChild(fragment);
|
||||
imgLoader.reload();
|
||||
}
|
||||
|
||||
remainingPending = false;
|
||||
}
|
||||
|
||||
function onSearchBlur(e) {
|
||||
if (canReuseSearchables && searchableNodes &&
|
||||
searchView.classList.contains('insearchmode') && blurList) {
|
||||
// All the searchable nodes have to be added
|
||||
addRemainingResults(searchableNodes, searchPageSize);
|
||||
}
|
||||
else if (emptySearch === true && remainingPending === true) {
|
||||
var lastNode = searchList.querySelector('li:last-child');
|
||||
if (lastNode) {
|
||||
var lastNodeUid = lastNode.dataset.uuid;
|
||||
var startNode = source.getNextNode(source.getNodeById(lastNodeUid));
|
||||
fillIdentityResults(startNode, hardLimit - searchPageSize);
|
||||
remainingPending = false;
|
||||
|
||||
imgLoader.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillIdentityResults(startNode, number) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
var contact = startNode;
|
||||
for (var i = 0; i < number && contact; i++) {
|
||||
var clonedNode = getClone(contact);
|
||||
fragment.appendChild(clonedNode);
|
||||
currentSet[contact.dataset.uuid] = clonedNode;
|
||||
contact = source.getNextNode(contact);
|
||||
}
|
||||
|
||||
if (fragment.hasChildNodes()) {
|
||||
searchList.appendChild(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
function getClone(node) {
|
||||
var id = node.dataset.uuid;
|
||||
var out = theClones[id];
|
||||
|
||||
if (!out) {
|
||||
out = source.clone(node);
|
||||
cacheClone(id, out);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function cacheClone(id, clone) {
|
||||
theClones[id] = clone;
|
||||
}
|
||||
|
||||
function onInput(e) {
|
||||
if (e.target.id === searchBox.id) {
|
||||
search();
|
||||
}
|
||||
}
|
||||
|
||||
var enterSearchMode = function searchMode(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
if (!inSearchMode) {
|
||||
window.addEventListener('input', onInput);
|
||||
searchView.classList.add('insearchmode');
|
||||
if (selectableForm) {
|
||||
selectableForm.classList.add('insearchmode');
|
||||
}
|
||||
|
||||
doInit();
|
||||
fillInitialSearchPage();
|
||||
inSearchMode = true;
|
||||
emptySearch = true;
|
||||
if (navigationController) {
|
||||
navigationController.go('search-view', 'none');
|
||||
}
|
||||
|
||||
setTimeout(function nextTick() {
|
||||
searchBox.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function fillInitialSearchPage(done) {
|
||||
hideProgressResults();
|
||||
|
||||
var startContact = source.getFirstNode();
|
||||
var numToFill = searchPageSize;
|
||||
|
||||
// Calculate rows visible on a single page the first time we get a row
|
||||
// that we can measure.
|
||||
if (startContact && searchPageSize < 1) {
|
||||
fillIdentityResults(startContact, 1);
|
||||
|
||||
var viewHeight = searchList.getBoundingClientRect().height;
|
||||
var rowHeight = searchList.children[0].getBoundingClientRect().height;
|
||||
searchPageSize = Math.ceil(viewHeight / rowHeight);
|
||||
hardLimit = ~~(3.5 * searchPageSize);
|
||||
|
||||
startContact = source.getNextNode(startContact);
|
||||
numToFill = searchPageSize - 1;
|
||||
}
|
||||
|
||||
fillIdentityResults(startContact, numToFill);
|
||||
|
||||
if (typeof done === 'function') {
|
||||
done();
|
||||
}
|
||||
|
||||
if (imgLoader) {
|
||||
imgLoader.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch(contacts, from, searchText, pattern, state) {
|
||||
// Check whether the user enter a new term or not
|
||||
if (currentTextToSearch.localeCompare(searchText) !== 0) {
|
||||
canReuseSearchables = false;
|
||||
window.console.warn('**** Cancelling current search ****');
|
||||
return;
|
||||
}
|
||||
|
||||
// Search the next chunk of contacts
|
||||
var end = from + CHUNK_SIZE;
|
||||
currentSearchTerms = pattern.source.split(/\s+/);
|
||||
for (var c = from; c < end && c < contacts.length; c++) {
|
||||
var contact = contacts[c].node || contacts[c];
|
||||
var contactText = contacts[c].text || getSearchText(contacts[c]);
|
||||
contactText = Normalizer.unescapeHTML(contactText);
|
||||
if (!checkContactMatch(currentSearchTerms, contactText)) {
|
||||
if (contact.dataset.uuid in currentSet) {
|
||||
searchList.removeChild(currentSet[contact.dataset.uuid]);
|
||||
delete currentSet[contact.dataset.uuid];
|
||||
}
|
||||
} else {
|
||||
if (state.count === 0) {
|
||||
hideProgressResults();
|
||||
}
|
||||
// Only an initial page of elements is loaded in the search list
|
||||
if (Object.keys(currentSet).length < searchPageSize &&
|
||||
!(contact.dataset.uuid in currentSet)) {
|
||||
var clonedNode = getClone(contact);
|
||||
currentSet[contact.dataset.uuid] = clonedNode;
|
||||
searchList.appendChild(clonedNode);
|
||||
}
|
||||
|
||||
if (currentSet[contact.dataset.uuid]) {
|
||||
// We clear the highlights here because parts of the node could
|
||||
// been already highlighted from a previous, more general search.
|
||||
clearHighlights(currentSet[contact.dataset.uuid]);
|
||||
highlightNode(currentSet[contact.dataset.uuid]);
|
||||
}
|
||||
|
||||
state.searchables.push({
|
||||
node: contact,
|
||||
text: contactText
|
||||
});
|
||||
state.count++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are still searching through the list, then schedule
|
||||
// the next batch of search comparisons.
|
||||
if (c < contacts.length) {
|
||||
searchTimer = window.setTimeout(function do_search() {
|
||||
searchTimer = null;
|
||||
doSearch(contacts, from + CHUNK_SIZE, searchText,
|
||||
pattern, state);
|
||||
}, 0);
|
||||
return;
|
||||
|
||||
// If we expect to get more nodes, for example if the source is
|
||||
// still loading, then delay finalizing the end of the search
|
||||
} else if (source.expectMoreNodes()) {
|
||||
// Since we're blocked waiting on more contacts to be provided,
|
||||
// use some delay here to avoid a tight spin loop.
|
||||
var delay = 250;
|
||||
searchTimer = window.setTimeout(function do_search() {
|
||||
doSearch(contacts, Math.min(end, contacts.length), searchText,
|
||||
pattern, state);
|
||||
}, delay);
|
||||
return;
|
||||
|
||||
// Or we are complete with no results found
|
||||
} else if (state.count === 0) {
|
||||
showNoResults();
|
||||
|
||||
canReuseSearchables = false;
|
||||
|
||||
// Or we are complete with results that might have images to render
|
||||
} else {
|
||||
imgLoader.reload();
|
||||
searchableNodes = state.searchables;
|
||||
canReuseSearchables = true;
|
||||
// If the user wished to scroll let's add the remaining results
|
||||
if (blurList === true) {
|
||||
searchTimer = window.setTimeout(function() {
|
||||
searchTimer = null;
|
||||
addRemainingResults(searchableNodes, searchPageSize);
|
||||
},0);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof state.searchDoneCb === 'function') {
|
||||
state.searchDoneCb();
|
||||
}
|
||||
}
|
||||
|
||||
var enableSearch = function enableSearch() {
|
||||
if (searchEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchEnabled = true;
|
||||
// We perform the search when all the info have been loaded and the
|
||||
// user wrote something in the entry field
|
||||
invalidateCache();
|
||||
search();
|
||||
};
|
||||
|
||||
// Allow the main contacts list to asynchronously tell us about additional
|
||||
// nodes as they are loaded.
|
||||
var appendNodes = function appendNodes(nodes) {
|
||||
if (!nodes || !nodes.length || !contactNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
contactNodes.push.apply(contactNodes, nodes);
|
||||
|
||||
// If there are no searches in progress, then we are done
|
||||
if (!currentTextToSearch || !canReuseSearchables || !searchableNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a current search then we need to determine whether the
|
||||
// new nodes should show up in that search.
|
||||
for (var i = 0, n = nodes.length; i < n; ++i) {
|
||||
var node = nodes[i];
|
||||
var nodeText = Normalizer.unescapeHTML(getSearchText(node));
|
||||
if (!checkContactMatch(currentSearchTerms, nodeText)) {
|
||||
searchableNodes.push({
|
||||
node: node,
|
||||
text: nodeText
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var checkContactMatch = function checkContactMatch(searchTerms, text) {
|
||||
for (var i=0, m=searchTerms.length; i < m; i++){
|
||||
if (!RegExp(searchTerms[i], 'i').test(text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var search = function performSearch(searchDoneCb) {
|
||||
prevTextToSearch = currentTextToSearch;
|
||||
|
||||
currentTextToSearch = Normalizer.toAscii(searchBox.value.trim());
|
||||
currentTextToSearch = Normalizer.escapeRegExp(currentTextToSearch);
|
||||
var thisSearchText = String(currentTextToSearch);
|
||||
|
||||
if (thisSearchText.length === 0) {
|
||||
resetState();
|
||||
window.setTimeout(fillInitialSearchPage, 0, searchDoneCb);
|
||||
}
|
||||
else {
|
||||
showProgress();
|
||||
if (!searchEnabled) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
emptySearch = false;
|
||||
// The remaining results have not been added yet
|
||||
remainingPending = true;
|
||||
var pattern = new RegExp(thisSearchText, 'i');
|
||||
var contactsToSearch = getContactsToSearch(thisSearchText,
|
||||
prevTextToSearch);
|
||||
var state = {
|
||||
count: 0,
|
||||
searchables: [],
|
||||
searchDoneCb: searchDoneCb
|
||||
};
|
||||
searchTimer = window.setTimeout(function do_search() {
|
||||
searchTimer = null;
|
||||
doSearch(contactsToSearch, 0, thisSearchText, pattern, state);
|
||||
},0);
|
||||
}
|
||||
};
|
||||
|
||||
function getSearchText(contact) {
|
||||
var out = '';
|
||||
|
||||
var uuid = contact.dataset.uuid;
|
||||
if (uuid) {
|
||||
out = searchTextCache[uuid];
|
||||
if (!out) {
|
||||
out = source.getSearchText(contact);
|
||||
searchTextCache[uuid] = out;
|
||||
}
|
||||
}
|
||||
else {
|
||||
window.console.error('Search: Not uuid found for the provided node');
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
var getContactsDom = function contactsDom() {
|
||||
if (!contactNodes) {
|
||||
contactNodes = source.getNodes();
|
||||
}
|
||||
return contactNodes;
|
||||
};
|
||||
|
||||
var getContactsToSearch = function getContactsToSearch(newText, prevText) {
|
||||
var out;
|
||||
if (canReuseSearchables && newText.length > prevText.length &&
|
||||
prevText.length > 0 && newText.indexOf(prevText) === 0) {
|
||||
out = searchableNodes || getContactsDom();
|
||||
} else {
|
||||
utils.dom.removeChildNodes(searchList);
|
||||
currentSet = {};
|
||||
out = getContactsDom();
|
||||
canReuseSearchables = false;
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
var isInSearchMode = function isInSearchMode() {
|
||||
return inSearchMode;
|
||||
};
|
||||
|
||||
var invalidateCache = function s_invalidate() {
|
||||
if (searchTimer) {
|
||||
window.clearTimeout(searchTimer);
|
||||
searchTimer = null;
|
||||
}
|
||||
currentTextToSearch = '';
|
||||
canReuseSearchables = false;
|
||||
searchableNodes = null;
|
||||
contactNodes = null;
|
||||
currentSet = {};
|
||||
searchTextCache = {};
|
||||
};
|
||||
|
||||
var removeContact = function s_removeContact(id) {
|
||||
var contact = searchList.querySelector('li[data-uuid=\"' + id + '\"]');
|
||||
searchList.removeChild(contact);
|
||||
};
|
||||
|
||||
var selectRow = function s_selectRow(id, value) {
|
||||
var check = searchList.querySelector(
|
||||
'#search-view input[value="' + id + '"]');
|
||||
if (check) {
|
||||
check.checked = value;
|
||||
}
|
||||
};
|
||||
|
||||
function showProgress() {
|
||||
searchNoResult.classList.add('hide');
|
||||
searchProgress.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showNoResults() {
|
||||
searchNoResult.classList.remove('hide');
|
||||
searchProgress.classList.add('hidden');
|
||||
}
|
||||
|
||||
function hideProgressResults() {
|
||||
searchNoResult.classList.add('hide');
|
||||
searchProgress.classList.add('hidden');
|
||||
}
|
||||
|
||||
exports.Search = {
|
||||
'init': init,
|
||||
'invalidateCache': invalidateCache,
|
||||
'appendNodes': appendNodes,
|
||||
'removeContact': removeContact,
|
||||
'search': search,
|
||||
'enterSearchMode': enterSearchMode,
|
||||
'exitSearchMode': exitSearchMode,
|
||||
'isInSearchMode': isInSearchMode,
|
||||
'enableSearch': enableSearch,
|
||||
'selectRow': selectRow,
|
||||
'updateSearchList': updateSearchList
|
||||
};
|
||||
})(window);
|
||||
20
gaia/shared/js/contacts/sms_integration.js
Executable file
20
gaia/shared/js/contacts/sms_integration.js
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
'use strict';
|
||||
/* global MozActivity */
|
||||
/* exported SmsIntegration */
|
||||
/* jshint nonew: false */
|
||||
|
||||
var SmsIntegration = {
|
||||
sendSms: function si_sendSms(number) {
|
||||
try {
|
||||
new MozActivity({
|
||||
name: 'new',
|
||||
data: {
|
||||
type: 'websms/sms',
|
||||
target: number
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('WebActivities unavailable? : ' + e);
|
||||
}
|
||||
}
|
||||
};
|
||||
49
gaia/shared/js/contacts/utilities/dom.js
Executable file
49
gaia/shared/js/contacts/utilities/dom.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.dom) {
|
||||
(function(document) {
|
||||
var Dom = utils.dom = {};
|
||||
|
||||
// remove all children of a given parent node
|
||||
Dom.removeChildNodes = function(node) {
|
||||
while (node.hasChildNodes()) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
// Given a container, applys the class passed to all the
|
||||
// nodes matching a selector.
|
||||
// CAUTION: Please be careful using this function as it could
|
||||
// be quite expensive depending on the container and selector.
|
||||
Dom.addClassToNodes = function(container, selector, clazz) {
|
||||
var nodes = container.querySelectorAll(selector);
|
||||
for (var i = 0, n = nodes.length; i < n; ++i) {
|
||||
nodes[i].classList.add(clazz);
|
||||
}
|
||||
};
|
||||
|
||||
// Given a container, removes the class passed to all the
|
||||
// nodes matching a selector.
|
||||
// CAUTION: Please be careful using this function as it could
|
||||
// be quite expensive depending on the container and selector.
|
||||
Dom.removeClassFromNodes = function(container, selector, clazz) {
|
||||
var nodes = container.querySelectorAll(selector);
|
||||
for (var i = 0, n = nodes.length; i < n; ++i) {
|
||||
nodes[i].classList.remove(clazz);
|
||||
}
|
||||
};
|
||||
|
||||
Dom.updatePhoto = function(photo, dest) {
|
||||
var background = '';
|
||||
if (photo != null) {
|
||||
background = 'url(' + URL.createObjectURL(photo) + ')';
|
||||
}
|
||||
dest.style.backgroundImage = background;
|
||||
// Only for testing purposes
|
||||
dest.dataset.photoReady = 'true';
|
||||
};
|
||||
|
||||
})(document);
|
||||
}
|
||||
37
gaia/shared/js/contacts/utilities/event_listeners.js
Executable file
37
gaia/shared/js/contacts/utilities/event_listeners.js
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
/*jshint loopfunc: true */
|
||||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.listeners) {
|
||||
(function(document) {
|
||||
var Listeners = utils.listeners = {};
|
||||
|
||||
Listeners.add = function(config) {
|
||||
try {
|
||||
for (var id in config) {
|
||||
var handler = config[id];
|
||||
var nodes = document.querySelectorAll(id);
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
if (Array.isArray(handler)) {
|
||||
handler.forEach(function handle(item) {
|
||||
if (!item.hasOwnProperty('event') &&
|
||||
!item.hasOwnProperty('handler')) {
|
||||
return;
|
||||
}
|
||||
node.addEventListener(item.event, item.handler);
|
||||
});
|
||||
} else {
|
||||
node.addEventListener('click', handler);
|
||||
}
|
||||
} // nodes
|
||||
} // Handlers
|
||||
}
|
||||
catch (e) {
|
||||
window.console.error('Error while registering listener for: ', id, e);
|
||||
}
|
||||
}; // Add function
|
||||
|
||||
})(document);
|
||||
}
|
||||
92
gaia/shared/js/contacts/utilities/http_rest.js
Executable file
92
gaia/shared/js/contacts/utilities/http_rest.js
Executable file
|
|
@ -0,0 +1,92 @@
|
|||
'use strict';
|
||||
|
||||
if (!window.Rest) {
|
||||
window.Rest = (function() {
|
||||
|
||||
function RestRequest(xhr) {
|
||||
var cancelled = false;
|
||||
this.cancel = function oncancel() {
|
||||
cancelled = true;
|
||||
window.setTimeout(xhr.abort.bind(xhr), 0);
|
||||
};
|
||||
this.isCancelled = function isCancelled() {
|
||||
return cancelled;
|
||||
};
|
||||
}
|
||||
|
||||
function Rest() { }
|
||||
|
||||
Rest.prototype = {
|
||||
get: function(uri, callback, pOptions) {
|
||||
var DEFAULT_TIMEOUT = 30000;
|
||||
var options = pOptions || {};
|
||||
|
||||
var xhr = new XMLHttpRequest({
|
||||
mozSystem: true
|
||||
});
|
||||
var outReq = new RestRequest(xhr);
|
||||
|
||||
xhr.open('GET', uri, true);
|
||||
var responseType = options.responseType || 'json';
|
||||
xhr.responseType = responseType;
|
||||
var responseProperty = responseType === 'xml' ?
|
||||
'responseXML' : 'response';
|
||||
|
||||
xhr.timeout = options.operationsTimeout || DEFAULT_TIMEOUT;
|
||||
if (!xhr.timeout || xhr.timeout === DEFAULT_TIMEOUT &&
|
||||
(parent && parent.config && parent.config.operationsTimeout)) {
|
||||
xhr.timeout = parent.config.operationsTimeout;
|
||||
}
|
||||
|
||||
if (options.requestHeaders) {
|
||||
for (var header in options.requestHeaders) {
|
||||
xhr.setRequestHeader(header, options.requestHeaders[header]);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onload = function(e) {
|
||||
if (xhr.status === 200 || xhr.status === 400 || xhr.status === 0) {
|
||||
if (callback && typeof callback.success === 'function') {
|
||||
setTimeout(function() {
|
||||
callback.success(xhr[responseProperty]);
|
||||
},0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error('HTTP error executing GET. ',
|
||||
uri, ' Status: ', xhr.status);
|
||||
if (callback && typeof callback.error === 'function') {
|
||||
setTimeout(function errorHandler() {
|
||||
callback.error({ status: xhr.status });
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}; // onload
|
||||
|
||||
xhr.ontimeout = function(e) {
|
||||
console.error('Timeout!!! while HTTP GET: ', uri);
|
||||
if (callback && typeof callback.timeout === 'function') {
|
||||
setTimeout(callback.timeout, 0);
|
||||
}
|
||||
}; // ontimeout
|
||||
|
||||
xhr.onerror = function(e) {
|
||||
console.error('Error while executing HTTP GET: ', uri,
|
||||
': ', e);
|
||||
if (callback && typeof callback.error === 'function' &&
|
||||
!outReq.isCancelled()) {
|
||||
setTimeout(function() {
|
||||
callback.error(e);
|
||||
},0);
|
||||
}
|
||||
}; // onerror
|
||||
|
||||
xhr.send();
|
||||
|
||||
return outReq;
|
||||
} // get
|
||||
}; // prototype
|
||||
|
||||
return new Rest();
|
||||
})();
|
||||
}
|
||||
103
gaia/shared/js/contacts/utilities/ice_store.js
Executable file
103
gaia/shared/js/contacts/utilities/ice_store.js
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
/* global Promise, Event */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Helper library to deal with in case of emergency (ICE)
|
||||
* Contacts.
|
||||
* Those contacts are suppose to be first responders to
|
||||
* an emergency. More info:
|
||||
* https://wiki.mozilla.org/Gaia/Comms/2.1/Ice_Contacts
|
||||
*
|
||||
* The information will be stored in a DS for sharing
|
||||
* between contacts and dialer.
|
||||
* The content will be an array of two contacts ids.
|
||||
* This helper will be used for fetching:
|
||||
* ICEStore.getContacts().then(function(iceContacts) {
|
||||
* ...
|
||||
* });
|
||||
* and setting the data:
|
||||
* ICEStore.setContacts(ids).then(function() {
|
||||
* ...
|
||||
* });
|
||||
*/
|
||||
(function(exports) {
|
||||
var STORE_NAME = 'ice_contacts';
|
||||
var store = null;
|
||||
var status = 'not_initialized';
|
||||
var FIELD = 'ICE_CONTACTS';
|
||||
|
||||
// Get a reference to the store, or returns it if
|
||||
// we already have.
|
||||
function init() {
|
||||
if (store !== null) {
|
||||
return Promise.resolve(store);
|
||||
}
|
||||
|
||||
if (!navigator.getDataStores) {
|
||||
return Promise.reject('No DS supported');
|
||||
}
|
||||
|
||||
if (status === 'initialising') {
|
||||
document.addEventListener('ice_ready', function ready() {
|
||||
document.removeEventListener('ice_ready', ready);
|
||||
if (store === null) {
|
||||
return Promise.reject('Error getting store');
|
||||
} else {
|
||||
return Promise.resolve(store);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
status = 'initialising';
|
||||
return new Promise(function(resolve, reject) {
|
||||
navigator.getDataStores(STORE_NAME).then(function(stores) {
|
||||
if (!stores || stores.length < 0) {
|
||||
document.dispatchEvent(new Event('on_ice_ready'));
|
||||
return reject('No DS found');
|
||||
} else {
|
||||
store = stores[0];
|
||||
status = 'initialised';
|
||||
document.dispatchEvent(new Event('ice_ready'));
|
||||
return resolve(store);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var ICEStore = {
|
||||
/**
|
||||
* Set the in case of emergency contacts.
|
||||
* @param {Array} iceContacts array of contacts id.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
setContacts: function(iceContacts) {
|
||||
return init().then(function() {
|
||||
return store.put(iceContacts, FIELD).then(function() {
|
||||
return Promise.resolve(iceContacts);
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Retrieves the in case of emergency contacts
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getContacts: function() {
|
||||
return init().then(function() {
|
||||
return store.get(FIELD);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Subscribe to changes in the store
|
||||
* @param {Function} Callback when a change happens
|
||||
*/
|
||||
onChange: function(cb) {
|
||||
return init().then(function() {
|
||||
store.addEventListener('change', function() {
|
||||
store.get(FIELD).then(cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.ICEStore = ICEStore;
|
||||
})(window);
|
||||
207
gaia/shared/js/contacts/utilities/image_loader.js
Executable file
207
gaia/shared/js/contacts/utilities/image_loader.js
Executable file
|
|
@ -0,0 +1,207 @@
|
|||
/* exported ImageLoader */
|
||||
'use strict';
|
||||
|
||||
if (!window.ImageLoader) {
|
||||
var ImageLoader = function ImageLoader(pContainer, pItems) {
|
||||
var container, items, itemsSelector, lastScrollTime, scrollLatency = 100,
|
||||
scrollTimer, itemHeight, total, imgsLoading = 0,
|
||||
loadImage = defaultLoadImage, self = this;
|
||||
|
||||
init(pContainer, pItems);
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*
|
||||
*/
|
||||
function init(pContainer, pItems) {
|
||||
itemsSelector = pItems;
|
||||
container = document.querySelector(pContainer);
|
||||
|
||||
attachHandlers();
|
||||
window.addEventListener('image-loader-resume', resuming);
|
||||
window.addEventListener('image-loader-pause', unload);
|
||||
load();
|
||||
}
|
||||
|
||||
function resuming() {
|
||||
window.clearTimeout(scrollTimer);
|
||||
attachHandlers();
|
||||
update();
|
||||
}
|
||||
|
||||
function onUpdate(evt) {
|
||||
evt.stopPropagation();
|
||||
onScroll();
|
||||
}
|
||||
|
||||
function load() {
|
||||
window.clearTimeout(scrollTimer);
|
||||
scrollTimer = null;
|
||||
items = container.querySelectorAll(itemsSelector);
|
||||
// All items have the same height
|
||||
itemHeight = items[0] ? items[0].offsetHeight : 1;
|
||||
total = items.length;
|
||||
// Initial check if items should appear
|
||||
window.setTimeout(update, 0);
|
||||
}
|
||||
|
||||
function attachHandlers() {
|
||||
container.addEventListener('scroll', onScroll);
|
||||
document.addEventListener('onupdate', onUpdate);
|
||||
}
|
||||
|
||||
function unload() {
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
document.removeEventListener('onupdate', onUpdate);
|
||||
window.clearTimeout(scrollTimer);
|
||||
scrollTimer = null;
|
||||
}
|
||||
|
||||
function setResolver(pResolver) {
|
||||
loadImage = pResolver;
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (imgsLoading > 0) {
|
||||
// Stop the pending images load
|
||||
window.stop();
|
||||
imgsLoading = 0;
|
||||
}
|
||||
// Clearing and setting a timer on every scroll event is too slow on
|
||||
// some mobile devices. Therefore, set a timer once here and then
|
||||
// check how long it has been since the last scroll event in the
|
||||
// timer handler to determine what to do.
|
||||
lastScrollTime = Date.now();
|
||||
if (!scrollTimer) {
|
||||
scrollTimer = window.setTimeout(updateFromScroll, scrollLatency);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFromScroll() {
|
||||
scrollTimer = null;
|
||||
// If we scrolled more since the timer was set, then we need to
|
||||
// delay again. Otherwise, go ahead and update now.
|
||||
var deltaLatency = lastScrollTime - Date.now() + scrollLatency;
|
||||
if (deltaLatency > 0) {
|
||||
scrollTimer = window.setTimeout(updateFromScroll, deltaLatency);
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the image contained in a DOM Element.
|
||||
*/
|
||||
function defaultLoadImage(item) {
|
||||
var image = item.querySelector('span[data-type=img][data-src]');
|
||||
if (!image) {
|
||||
image = item.querySelector('img[data-src]');
|
||||
if (!image) {
|
||||
// Image by default
|
||||
image = item.querySelector('span[data-type=img][data-group]');
|
||||
if (image) {
|
||||
item.dataset.visited = 'true';
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
++imgsLoading;
|
||||
var tmp = new Image();
|
||||
var src = tmp.src = image.dataset.src;
|
||||
tmp.onload = function onload() {
|
||||
--imgsLoading;
|
||||
image.style.backgroundImage = 'url(' + src + ')';
|
||||
if (tmp.complete) {
|
||||
item.dataset.visited = 'true';
|
||||
}
|
||||
tmp = null;
|
||||
};
|
||||
|
||||
tmp.onabort = tmp.onerror = function onerror() {
|
||||
--imgsLoading;
|
||||
item.dataset.visited = 'false';
|
||||
tmp = null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the set of items are in the current viewport
|
||||
*
|
||||
*/
|
||||
function update() {
|
||||
if (total === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var viewTop = container.scrollTop;
|
||||
// Index is always inside or below viewport
|
||||
var index = Math.floor(viewTop / itemHeight);
|
||||
var containerHeight = container.offsetHeight;
|
||||
|
||||
// Goes backward
|
||||
for (var i = index; i >= 0; i--) {
|
||||
var item = items[i];
|
||||
if (item) {
|
||||
if (item.offsetTop + itemHeight < viewTop) {
|
||||
break; // Over
|
||||
}
|
||||
|
||||
if (item.dataset.visited !== 'true' &&
|
||||
item.offsetTop <= viewTop + containerHeight) {
|
||||
loadImage(item, self); // Inside
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Goes forward
|
||||
for (var j = index + 1; j < total; j++) {
|
||||
var theItem = items[j];
|
||||
if (!theItem) {
|
||||
// Returning because of index out of bound
|
||||
return;
|
||||
}
|
||||
|
||||
if (theItem.offsetTop > viewTop + containerHeight) {
|
||||
return; // Below
|
||||
}
|
||||
|
||||
if (theItem.dataset.visited !== 'true') {
|
||||
loadImage(theItem, self);
|
||||
}
|
||||
}
|
||||
} // update
|
||||
|
||||
function releaseImage(item) {
|
||||
var image = item.querySelector('span[data-type=img][data-src]');
|
||||
if (!image) {
|
||||
// Image by default
|
||||
image = item.querySelector('span[data-type=img][data-group]');
|
||||
if (image) {
|
||||
item.dataset.visited = 'false';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
image.style.backgroundImage = 'none';
|
||||
item.dataset.visited = 'false';
|
||||
return image;
|
||||
}
|
||||
|
||||
// It's necessary to prevent unit test errors
|
||||
function destroy() {
|
||||
unload();
|
||||
window.removeEventListener('image-loader-pause', unload);
|
||||
window.removeEventListener('image-loader-resume', resuming);
|
||||
container = items = itemsSelector = lastScrollTime = scrollLatency = null;
|
||||
scrollTimer = itemHeight = total = imgsLoading = loadImage = null;
|
||||
}
|
||||
|
||||
this.reload = load;
|
||||
this.unload = unload;
|
||||
this.setResolver = setResolver;
|
||||
this.defaultLoad = defaultLoadImage;
|
||||
this.releaseImage = releaseImage;
|
||||
this.destroy = destroy;
|
||||
};
|
||||
|
||||
}
|
||||
41
gaia/shared/js/contacts/utilities/image_square.js
Executable file
41
gaia/shared/js/contacts/utilities/image_square.js
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (typeof utils.squareImage === 'undefined') {
|
||||
utils.squareImage = function(blob, callback) {
|
||||
var img = document.createElement('img');
|
||||
var url = URL.createObjectURL(blob);
|
||||
img.src = url;
|
||||
|
||||
function cleanupImg() {
|
||||
img.src = '';
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
img.onerror = cleanupImg;
|
||||
|
||||
img.onload = function onBlobLoad() {
|
||||
var width = img.width;
|
||||
var height = img.height;
|
||||
|
||||
if (width === height) {
|
||||
cleanupImg();
|
||||
callback(blob);
|
||||
} else {
|
||||
var canvas = document.createElement('canvas');
|
||||
var min = canvas.width = canvas.height = Math.min(width, height);
|
||||
var context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
context.drawImage(img, (width - min) / 2, (height - min) / 2, min, min,
|
||||
0, 0, min, min);
|
||||
cleanupImg();
|
||||
canvas.toBlob(function onCanvasToBlob(canvasBlob) {
|
||||
context = null;
|
||||
canvas.width = canvas.height = 0;
|
||||
canvas = null;
|
||||
callback(canvasBlob);
|
||||
}, 'image/jpeg', 0.95);
|
||||
}
|
||||
};
|
||||
}; // utils.squareImage
|
||||
} // if
|
||||
77
gaia/shared/js/contacts/utilities/image_thumbnail.js
Executable file
77
gaia/shared/js/contacts/utilities/image_thumbnail.js
Executable file
|
|
@ -0,0 +1,77 @@
|
|||
'use strict';
|
||||
|
||||
/* global ImageUtils, LazyLoader */
|
||||
|
||||
/*
|
||||
* WARNING: This module LazyLoads
|
||||
*
|
||||
* '/shared/js/contacts/import/utilities/config.js',
|
||||
* '/shared/js/image_utils.js'
|
||||
*
|
||||
*/
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
(function(utils) {
|
||||
var DEPENDENCIES = [
|
||||
'/shared/js/contacts/import/utilities/config.js',
|
||||
'/shared/js/image_utils.js'
|
||||
];
|
||||
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
|
||||
var CONFIG_FILE = '/config-images.json';
|
||||
|
||||
// Default data for creating a thumbnail. This will be used if a config file
|
||||
// is not found
|
||||
var DEFAULT_CONFIG = {
|
||||
'thumbnail' : {
|
||||
'format': 'image/jpeg',
|
||||
'size': 65,
|
||||
'quality': 1.0
|
||||
}
|
||||
};
|
||||
var THUMB_CONFIG = DEFAULT_CONFIG.thumbnail;
|
||||
|
||||
if (typeof utils.thumbnailImage !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtains the thumbnail configuration by looking for the config file
|
||||
// If no config file is found, then default values are taken
|
||||
function getThumbsConfig() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
utils.config.load(CONFIG_FILE).then(function resolved(config) {
|
||||
resolve(config);
|
||||
}, function rejected() {
|
||||
resolve(DEFAULT_CONFIG);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scaleImage(blob, configData) {
|
||||
var thumbConfig = configData && configData.thumbnail || {};
|
||||
|
||||
var thumbnailEdge = (thumbConfig.size || THUMB_CONFIG.size) * dpr;
|
||||
var format = thumbConfig.format || THUMB_CONFIG.format;
|
||||
var encodingQuality = thumbConfig.quality || THUMB_CONFIG.quality;
|
||||
|
||||
return ImageUtils.resizeAndCropToCover(blob, thumbnailEdge,
|
||||
thumbnailEdge, format,
|
||||
encodingQuality);
|
||||
}
|
||||
|
||||
// We keep the aspect ratio and make the smallest edge be
|
||||
// |thumbnailEdge| long.
|
||||
utils.thumbnailImage = function(blob, callback) {
|
||||
LazyLoader.load(DEPENDENCIES, function() {
|
||||
getThumbsConfig().then(function(config) {
|
||||
return scaleImage(blob, config);
|
||||
}).then(callback).catch(function(err) {
|
||||
console.error('Error while converting image to thumbnail:', err.name);
|
||||
callback(blob);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
})(utils);
|
||||
261
gaia/shared/js/contacts/utilities/templates.js
Executable file
261
gaia/shared/js/contacts/utilities/templates.js
Executable file
|
|
@ -0,0 +1,261 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.templates) {
|
||||
(function() {
|
||||
var Templates = utils.templates = {};
|
||||
|
||||
/**
|
||||
* Returns a target HTMLElement from a selector or HTMLElement itself
|
||||
*
|
||||
* @param {HTMLElement or Selector} element target element.
|
||||
*
|
||||
* @return {HTMLElement} HTMLElement according to the selector or itself.
|
||||
*
|
||||
*
|
||||
*/
|
||||
function getTarget(element) {
|
||||
var target = element;
|
||||
if (!element.tagName) {
|
||||
target = document.querySelector(element);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a target HTML element which contains a template set
|
||||
* returns the template that will have to be applied over the data
|
||||
*
|
||||
* @param {HTMLElement} target which contains the template.
|
||||
* @param {Object} data to be used on the template.
|
||||
*
|
||||
* @return {HTMLElement} HTMLElement with the template.
|
||||
*
|
||||
*/
|
||||
function getTemplate(target, data) {
|
||||
// Multi templates temporarily disabled to avoid eval usage
|
||||
// TODO: Implement an alternative for eval
|
||||
var templates = target.querySelectorAll('*[data-template]');
|
||||
if (templates.length === 0) {
|
||||
throw new Error('No template declared');
|
||||
}
|
||||
if (templates.length > 1 && templates.item(0).dataset.condition) {
|
||||
throw new Error('Only one template supported in this version');
|
||||
}
|
||||
|
||||
return {
|
||||
template: templates.item(0),
|
||||
isMulti: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function used to replace data on a template
|
||||
*
|
||||
* @param {Object} data the data to be used on the template.
|
||||
*
|
||||
* @return {function} to be used.
|
||||
*
|
||||
*/
|
||||
function templateReplace(data) {
|
||||
return function(text, property) {
|
||||
var out;
|
||||
if (property.indexOf('.') === -1) {
|
||||
out = data[property];
|
||||
if (Array.isArray(out) && out.length > 0) {
|
||||
out = out[0];
|
||||
}
|
||||
} else {
|
||||
throw new Error('Dotted expressions not supported');
|
||||
}
|
||||
|
||||
if (typeof out === 'undefined') {
|
||||
out = text;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds (append or prepend) a new instance HTMLElement (or array of)
|
||||
* of a template
|
||||
* The template is assumed to be a child of the element
|
||||
* passed as parameter
|
||||
* The new element will be appended as a child
|
||||
*
|
||||
* @param {HTMLElement} ele container lement that contains the template
|
||||
* and which will contain the new instance. Can be an HTMLElement
|
||||
* or a CSS selector.
|
||||
*
|
||||
* @param {object or array} data with the data displayed by the template.
|
||||
*
|
||||
* @param {String} mode oneOf ('A','P').
|
||||
*
|
||||
* @return {HTMLElement} (or last element if data is an array).
|
||||
*
|
||||
*
|
||||
*/
|
||||
function add(element, data, mode, targetNode) {
|
||||
// It is supported both the element itself or a selector
|
||||
var target = getTarget(element);
|
||||
var newElem;
|
||||
|
||||
var theData = [data];
|
||||
if (data instanceof Array) {
|
||||
theData = data;
|
||||
}
|
||||
|
||||
// Optimization to avoid trying to find a template when
|
||||
// only one is needed
|
||||
var multiTemplate = true;
|
||||
var template;
|
||||
var idx = 0;
|
||||
theData.forEach(function(oneData) {
|
||||
// Pseudo-field with the index
|
||||
oneData._idx_ = idx++;
|
||||
// A suitable template for the data is firstly found
|
||||
if (multiTemplate === true) {
|
||||
var tresult = getTemplate(target, oneData);
|
||||
template = tresult.template;
|
||||
if (tresult.isMulti === false) {
|
||||
multiTemplate = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (template) {
|
||||
newElem = this.render(template, oneData);
|
||||
target = targetNode || target;
|
||||
if (mode === 'A') {
|
||||
target.appendChild(newElem);
|
||||
} else if (mode === 'P') { // Append mode
|
||||
if (target.firstChild) {
|
||||
target.insertBefore(newElem, element.firstChild);
|
||||
} else {
|
||||
target.appendChild(newElem);
|
||||
}
|
||||
} // prepend mode
|
||||
|
||||
} // if template
|
||||
|
||||
}.bind(Templates)); // forEach data
|
||||
|
||||
return newElem;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends a new instance HTMLElement (or array of) of a template
|
||||
*
|
||||
* The template is assumed to be a child of the element passed
|
||||
* as parameter
|
||||
* The new element will be appended as a child
|
||||
*
|
||||
* @param {HTMLElement or String} ele container element that
|
||||
* contains the template and which will contain the new instance.
|
||||
* Can be an HTMLElement or a CSS selector.
|
||||
*
|
||||
* @param {object or array} data with the data displayed by the template.
|
||||
*
|
||||
* @return {HTMLelement} (or last element if data is an array).
|
||||
*
|
||||
*
|
||||
*/
|
||||
Templates.append = function(element, data, targetNode) {
|
||||
var f = add.bind(this);
|
||||
|
||||
return f(element, data, 'A', targetNode);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Prepends a new instance (or array of) of a template
|
||||
*
|
||||
* The template is assumed to be a child of the element passed
|
||||
* as parameter
|
||||
*
|
||||
* @param {HTMLElement or String} ele container element that
|
||||
* contains the template and which will contain the new instance.
|
||||
* Can be an HTMLElement or a CSS selector.
|
||||
*
|
||||
* @param {Object or Array} data with the data displayed.
|
||||
*
|
||||
* @return {HTMLElement} added.
|
||||
*
|
||||
*
|
||||
*/
|
||||
Templates.prepend = function(element, data, targetNode) {
|
||||
var f = add.bind(this);
|
||||
|
||||
return f(element, data, 'P', targetNode);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Renders the content specified by a template with object data
|
||||
*
|
||||
* @param {HTMLElement} eleTemplate the template itself.
|
||||
* @param {Object} data the data to be used.
|
||||
*
|
||||
* @return {HTMLElement} according to the template and with the data.
|
||||
*
|
||||
*
|
||||
*/
|
||||
Templates.render = function(eleTemplate, data) {
|
||||
var newElem = eleTemplate.cloneNode(true);
|
||||
newElem.removeAttribute('data-template');
|
||||
newElem.removeAttribute('data-condition');
|
||||
|
||||
/* var pattern = /#(\w+)#/g; */
|
||||
var pattern = /#(\w+[\w.]*)#/g;
|
||||
|
||||
var inner = newElem.innerHTML;
|
||||
|
||||
// Replace function
|
||||
var replaceFunction = templateReplace(data);
|
||||
var ninner = inner.replace(pattern, replaceFunction);
|
||||
|
||||
newElem.innerHTML = ninner;
|
||||
|
||||
var attrs = newElem.attributes;
|
||||
|
||||
var total = attrs.length;
|
||||
for (var c = 0; c < total; c++) {
|
||||
var val = attrs[c].value;
|
||||
var nval = val.replace(pattern, replaceFunction);
|
||||
|
||||
newElem.setAttribute(attrs[c].name, nval);
|
||||
}
|
||||
|
||||
if (!newElem.id) {
|
||||
if (data.id) {
|
||||
newElem.id = data.id;
|
||||
}
|
||||
}
|
||||
|
||||
return newElem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears a container element
|
||||
*
|
||||
* @param {HTMLElement or String} element (selector or HTML element).
|
||||
*
|
||||
*
|
||||
*/
|
||||
Templates.clear = function(element) {
|
||||
var target = getTarget(element);
|
||||
|
||||
var templates = target.querySelectorAll('*[data-template]');
|
||||
|
||||
target.innerHTML = '';
|
||||
|
||||
var total = templates.length;
|
||||
|
||||
for (var c = 0; c < total; c++) {
|
||||
target.appendChild(templates.item(c));
|
||||
}
|
||||
};
|
||||
}) ();
|
||||
} // window.templates
|
||||
Loading…
Reference in a new issue