VulpesOS/gaia/apps/communications/contacts/js/views/form.js
OverSu 0041cd9660 Update shared Gaia workflows and Sargo development tooling
Fix camera switching and Contacts/SMS activity routing across shared hosts.
Integrate Marketplace installation and native standalone recovery tools,
Bluetooth runtime packages and trusted certificates.

Update English/French READMEs and the shared changelog with tested scope
and remaining radio/audio/Bluetooth limitations. Preview17 remains the
existing APK; no new native image is released.

Validation: shared JavaScript and native Python tests; Android packaging
and payload parity from an isolated publication checkout.
2026-10-08 14:27:17 +02:00

1509 lines
45 KiB
JavaScript
Executable file

'use strict';
/* global ActivityHandler */
/* global ConfirmDialog */
/* global ContactPhotoHelper */
/* global Contacts */
/* global ContactsTag */
/* global fb */
/* global LazyLoader */
/* global MozActivity */
/* global Normalizer */
/* global ContactsService */
/* global utils */
/* global TAG_OPTIONS */
/* global ActionMenu */
/* global ICEData */
/* global MergeHelper */
/* global MainNavigation */
/* global MatchService */
/* global ContactsService */
/* global Matcher */
/* global Search */
/* global TagSelector */
var contacts = window.contacts || {};
contacts.Form = (function() {
var counters = {
'tel': 0,
'email': 0,
'adr': 0,
'date': 0,
'note': 0
};
var currentContact = {};
var dom,
contactForm,
deleteContactButton,
addNewDateButton,
thumb,
thumbAction,
saveButton,
formHeader,
formTitle,
currentContactId,
givenName,
company,
familyName,
configs,
_,
formView,
throbber,
mode,
cancelHandler,
mergeHandler,
nonEditableValues,
deviceContact,
fbContact,
currentPhoto;
const CONTACTS_APP_ORIGIN = location.origin;
var FB_CLASS = 'facebook';
var INVALID_CLASS = 'invalid';
// Remove icon button id
var IMG_DELETE_CLASS = 'img-delete-button';
// The size we want our contact photos to be
var PHOTO_WIDTH = 320;
var PHOTO_HEIGHT = 320;
// bug 1038414: ask for an image about 2MP before
// doing the crop to save memory in both apps
var MAX_PHOTO_SIZE = 200000;
var touchstart = 'ontouchstart' in window ? 'touchstart' : 'mousedown';
// Indicates whether a tel number has been deleted
// (useful for warning about ICE Contacts)
var deletedTelNumber = false;
var textFieldsCache = {
_textFields: null,
get: function textFieldsCache_get() {
if (!this._textFields) {
var fields = contactForm.querySelectorAll('input[data-field]');
var fbFields =
Array.slice(contactForm.querySelectorAll(
'.facebook input[data-field]'));
var invalidFields =
Array.slice(contactForm.querySelectorAll(
'.invalid input[data-field]'));
this._textFields = Array.filter(fields, function(field) {
return (fbFields.indexOf(field) === -1 &&
invalidFields.indexOf(field) === -1);
});
}
return this._textFields;
},
clear: function textFieldsCache_clear() {
this._textFields = null;
}
};
var initContainers = function cf_initContainers() {
deleteContactButton = dom.querySelector('#delete-contact');
thumb = dom.querySelector('#thumbnail-photo');
thumbAction = dom.querySelector('#thumbnail-action');
thumbAction.querySelector('#photo-button').onclick = photoAction;
saveButton = dom.querySelector('#save-button');
addNewDateButton = dom.querySelector('#add-new-date');
contactForm = dom.getElementById('contact-form');
formHeader = dom.querySelector('#contact-form-header');
formTitle = dom.getElementById('contact-form-title');
currentContactId = dom.getElementById('contact-form-id');
givenName = dom.getElementById('givenName');
company = dom.getElementById('org');
familyName = dom.getElementById('familyName');
formView = dom.getElementById('view-contact-form');
throbber = dom.getElementById('throbber');
var phonesContainer = dom.getElementById('contacts-form-phones');
var emailContainer = dom.getElementById('contacts-form-emails');
var addressContainer = dom.getElementById('contacts-form-addresses');
var dateContainer = dom.getElementById('contacts-form-dates');
var noteContainer = dom.getElementById('contacts-form-notes');
var phoneTemplate = dom.getElementById('add-phone-#i#');
var emailTemplate = dom.getElementById('add-email-#i#');
var addressTemplate = dom.getElementById('add-address-#i#');
var dateTemplate = dom.getElementById('add-date-#i#');
var noteTemplate = dom.getElementById('add-note-#i#');
configs = {
'tel': {
template: phoneTemplate,
tags: TAG_OPTIONS['phone-type'],
fields: ['value', 'type', 'carrier'],
container: phonesContainer
},
'email': {
template: emailTemplate,
tags: TAG_OPTIONS['email-type'],
fields: ['value', 'type'],
container: emailContainer
},
'adr': {
template: addressTemplate,
tags: TAG_OPTIONS['address-type'],
fields: [
'type',
'streetAddress',
'postalCode',
'locality',
'countryName'
],
container: addressContainer
},
'date': {
template: dateTemplate,
tags: TAG_OPTIONS['date-type'],
fields: ['value', 'type'],
container: dateContainer
},
'note': {
template: noteTemplate,
tags: TAG_OPTIONS['address-type'],
fields: ['note'],
container: noteContainer
}
};
};
var init = function cf_init(tags, currentDom) {
dom = currentDom || document;
_ = navigator.mozL10n.get;
initContainers();
dom.addEventListener('input', function input(event) {
checkDisableButton();
});
contactForm.addEventListener(touchstart, function click(event) {
var tgt = event.target;
if (tgt.tagName == 'BUTTON' && tgt.getAttribute('type') == 'reset') {
event.preventDefault();
var input = tgt.previousElementSibling;
if (input.getAttribute('name').startsWith('tel') &&
input.dataset.field === 'value') {
var telId = input.id;
var telIndex = telId.substring(telId.indexOf('_') + 1);
var carrierField =
document.getElementById('carrier' + '_' + telIndex);
carrierField.parentNode.classList.add(INVALID_CLASS);
textFieldsCache.clear();
deletedTelNumber = true;
}
input.value = '';
checkDisableButton();
}
});
thumbAction.addEventListener(touchstart, function click(event) {
// Removing current photo
if (event.target.tagName == 'BUTTON') {
saveButton.removeAttribute('disabled');
}
});
formView.addEventListener('ValueModified', function onValueModified(event) {
if (!event.detail) {
return;
}
if (!emptyForm() && event.detail.prevValue !== event.detail.newValue) {
saveButton.removeAttribute('disabled');
}
});
// Add listeners
utils.listeners.add({
'#contact-form-header': [
{
event: 'action',
handler: Contacts.cancel // Cancel edition
}
],
'#save-button': saveContact,
'#contact-form button[data-field-type]': newField
});
};
// Renders the birthday as per the locale
function renderDate(date, bdayInputText) {
if (!date) {
return;
}
bdayInputText.textContent = utils.misc.formatDate(date, {
year: 'numeric',
month: 'numeric',
day: 'numeric'
});
bdayInputText.classList.remove('placeholder');
}
function onInputDate(bdayInputText, e) {
renderDate(e.target.valueAsDate, bdayInputText);
}
var newField = function newField(evt) {
return contacts.Form.onNewFieldClicked(evt);
};
var render = function cf_render(contact, callback, pFbContactData,
fromUpdateActivity) {
var fbContactData = pFbContactData || [];
fbContact = fbContactData[2] || {};
nonEditableValues = fbContactData[1] || {};
deviceContact = contact;
var renderedContact = fbContactData[0] || deviceContact;
resetForm();
// We need to check against the string 'undefined' because of bug 951829.
renderedContact && renderedContact.id && renderedContact.id != 'undefined' ?
showEdit(renderedContact, fromUpdateActivity) : showAdd(renderedContact);
// reset the scroll from (possible) previous renders
contactForm.parentNode.scrollTop = 0;
if (callback) {
callback();
}
};
function extractValue(value) {
if (value) {
if (Array.isArray(value) && value.length > 0 && value[0]) {
return value[0].trim();
} else if (!Array.isArray(value)) {
return value.trim();
}
}
return '';
// The last statement allows to return the value in case we are getting
// just params from an activity and not a mozContact.
}
function fillDates(contact) {
if (contact.bday) {
contact.date = [];
contact.date.push({
type: 'birthday',
value: contact.bday
});
}
if (contact.anniversary) {
contact.date = contact.date || [];
contact.date.push({
type: 'anniversary',
value: contact.anniversary
});
}
}
function renderPhoto(contact) {
if (contact.photo && contact.photo.length > 0) {
currentPhoto = ContactPhotoHelper.getFullResolution(contact);
var button = addRemoveIconToPhoto();
// Only can be removed a device contact photo
if (!(deviceContact.photo && deviceContact.photo.length > 0)) {
button.classList.add('hide');
// Avoid saving the image to the Contacts DB
thumbAction.classList.add(FB_CLASS);
}
}
utils.dom.updatePhoto(currentPhoto, thumb);
}
var showEdit = function showEdit(contact, fromUpdateActivity) {
mode = 'edit';
if (!contact || !contact.id) {
return;
}
if (!fromUpdateActivity) {
saveButton.setAttribute('disabled', 'disabled');
}
saveButton.setAttribute('data-l10n-id', 'update');
currentContact = contact;
deleteContactButton.parentNode.classList.remove('hide');
formTitle.setAttribute('data-l10n-id', 'editContact');
currentContactId.value = contact.id;
givenName.value = extractValue(contact.givenName);
familyName.value = extractValue(contact.familyName);
company.value = extractValue(contact.org);
if (nonEditableValues[company.value]) {
var nodeClass = company.parentNode.classList;
nodeClass.add(FB_CLASS);
}
renderPhoto(contact);
fillDates(contact);
['tel', 'email', 'adr', 'date', 'note'].forEach(function(field) {
renderTemplate(field, contact[field]);
});
deleteContactButton.onclick = function deleteClicked(event) {
var msg = 'deleteConfirmMsg';
var yesObject = {
title: 'delete',
isDanger: true,
callback: function onAccept() {
deleteContact(currentContact);
ConfirmDialog.hide();
if (ActivityHandler.currentlyHandling) {
formHeader.triggerAction();
}
}
};
var noObject = {
title: 'cancel',
callback: function onCancel() {
ConfirmDialog.hide();
}
};
ConfirmDialog.show(null, msg, noObject, yesObject);
};
};
// Checks whether is an ICE contact or not
function isIceContact(contact, cb) {
LazyLoader.load('js/utilities/ice_data.js', function() {
ICEData.getActiveIceContacts().then(function iceloaded(iceContacts) {
cb(iceContacts.indexOf(contact.id) !== -1);
});
});
}
var showAdd = function showAdd(params) {
mode = 'add';
formView.classList.remove('skin-organic');
if (!params || params == -1 || !params.id) {
currentContact = {};
}
saveButton.setAttribute('disabled', 'disabled');
saveButton.setAttribute('data-l10n-id', 'done');
deleteContactButton.parentNode.classList.add('hide');
formTitle.setAttribute('data-l10n-id', 'addContact');
params = params || {};
givenName.value = extractValue(params.givenName);
familyName.value = extractValue(params.lastName || params.familyName);
company.value = extractValue(params.company || params.org);
renderPhoto(params);
fillDates(params);
['tel', 'email', 'adr', 'date', 'note'].forEach(function(field) {
renderTemplate(field, params[field]);
});
checkDisableButton();
};
// template, fields, cont, counter
/**
* Render Template
*
* @param {string} type Type of template, eg. 'tel'
* @param {object[]} toRender
*/
var renderTemplate = function cf_rendTemplate(type, toRender) {
if (!Array.isArray(toRender)) {
toRender = [{value: toRender}];
}
for (var i = 0; i < toRender.length; i++) {
insertField(type, toRender[i] || {});
}
};
function checkAddDateButton() {
addNewDateButton.disabled = (getActiveFormDates() >= 2);
}
function getActiveFormDates() {
var fbDates = dom.querySelectorAll('.date-template' + '.' + FB_CLASS);
return counters.date - fbDates.length;
}
var onNewFieldClicked = function onNewFieldClicked(evt) {
var type = evt.target.dataset.fieldType;
evt.preventDefault();
// Workaround until 809452 is fixed.
// What we are avoiding with this condition is removing / restoring
// a field when the event is simulated by a ENTER Keyboard click
if (evt.explicitOriginalTarget === evt.target) {
contacts.Form.insertField(type, null, [
'inserted',
'displayed'
]);
}
textFieldsCache.clear();
// For dates only two instances
if (type === 'date') {
// Disable the add date button if necessary
checkAddDateButton();
}
return false;
};
function checkCarrierTel(carrierInput, event) {
var telInput = event.target;
var value = telInput.value;
if (!value || !value.trim()) {
deletedTelNumber = true;
// If it was not previously filled then it will be disabled
if (!telInput.dataset.wasFilled) {
carrierInput.setAttribute('disabled', 'disabled');
}
else {
// Otherwise marked as invalid in order not to submit it
carrierInput.parentNode.classList.add(INVALID_CLASS);
textFieldsCache.clear();
}
}
else {
deletedTelNumber = false;
// Marked as filled
telInput.dataset.wasFilled = true;
// Enabling and marking as valid
carrierInput.removeAttribute('disabled');
carrierInput.parentNode.classList.remove(INVALID_CLASS);
}
}
/**
* We cannot relay on the counter, but in the next id after the
* last field.
* See bug 1113134 for related explanation.
*/
function getNextTemplateId(container) {
var nodes = container.childNodes;
if (!nodes || nodes.length === 0) {
return 0;
}
var lastNode = nodes[nodes.length - 1];
var value = lastNode.dataset.index;
return value ? parseInt(value) + 1 : 0;
}
var insertField = function insertField(type, object, targetClasses) {
if (!type || !configs[type]) {
console.error('Inserting field with unknown type');
return;
}
var obj = object || {};
var config = configs[type];
var template = config.template;
var tags = ContactsTag.filterTags(type, null, config.tags);
var container = config.container;
var default_type = tags[0] && tags[0].type || '';
var currField = {};
var infoFromFB = false;
config.fields.forEach(function(currentElem) {
var def = (currentElem === 'type') ? default_type : '';
var defObj = (typeof(obj) === 'string') ? obj : obj[currentElem];
var value = '';
var isDate = (defObj && typeof defObj.getMonth === 'function');
currField[currentElem] = (defObj && typeof(defObj) === 'object' &&
!isDate ? defObj.toString() : defObj);
value = currField[currentElem] || def;
if (currentElem === 'type') {
currField.type_value = value;
// Do localization for built-in types
if (isBuiltInType(value, tags)) {
currField.type_l10n_id = value;
value = _(value) || value;
}
}
if (!isDate) {
currField[currentElem] = Normalizer.escapeHTML(value, true);
}
if (!infoFromFB && value && nonEditableValues[value]) {
infoFromFB = true;
}
});
currField.i = getNextTemplateId(container);
var rendered = utils.templates.render(template, currField);
// Controlling that if no tel phone is present carrier field is disabled
if (type === 'tel') {
var carrierInput = rendered.querySelector('input[data-field="carrier"]');
var telInput = rendered.querySelector('input[data-field="value"]');
var cb = checkCarrierTel.bind(null, carrierInput);
telInput.addEventListener('input', cb, true);
checkCarrierTel(carrierInput, {target: telInput});
}
// Adding listener to properly render dates
if (type === 'date') {
var dateInput = rendered.querySelector('input[type="date"]');
// Setting the max value as today's date
var currentDate = new Date();
dateInput.setAttribute('max', currentDate.getFullYear() + '-' +
(currentDate.getMonth() + 1) + '-' +
currentDate.getDate());
var dateInputText = dateInput.previousElementSibling;
if (currField.value) {
dateInput.valueAsDate = currField.value;
renderDate(currField.value, dateInputText);
}
else {
dateInputText.setAttribute('data-l10n-id', 'date-span-placeholder');
}
dateInput.addEventListener('input',
onInputDate.bind(null, dateInputText));
}
if (infoFromFB) {
var nodeClass = rendered.classList;
nodeClass.add(FB_CLASS);
}
// The remove button should not appear on FB disabled fields
if (!rendered.classList.contains(FB_CLASS)) {
var removeEl = removeFieldIcon(rendered.id, type);
rendered.insertBefore(removeEl, rendered.firstChild);
}
// Add event listeners
var boxTitle = rendered.querySelector('legend.action');
if (boxTitle) {
boxTitle.addEventListener('click', onGoToSelectTag);
}
// This will happen when the fields are added by the user on demand
if (Array.isArray(targetClasses)) {
rendered.classList.add(targetClasses[0]);
window.setTimeout(() => rendered.classList.add(targetClasses[1]));
}
container.classList.remove('empty');
container.appendChild(rendered);
counters[type]++;
// Finally we need to check the status of the add date button
if (type === 'date') {
checkAddDateButton();
}
};
var onGoToSelectTag = function onGoToSelectTag(evt) {
evt.preventDefault();
var target = evt.currentTarget;
LazyLoader.load('/contacts/js/utilities/tagSelector.js', function() {
TagSelector.show(target.children[0]);
});
return false;
};
var deleteContact = function deleteContact(contact) {
var deleteSuccess = function deleteSuccess() {
if (window.Search && Search.isInSearchMode()) {
Search.invalidateCache();
Search.removeContact(contact.id);
Search.exitSearchMode();
}
MainNavigation.home();
};
ContactsService.remove(
contact,
function(e) {
if (e) {
console.error('Error removing the contact');
return;
}
deleteSuccess();
}
);
};
var getCurrentPhoto = function cf_getCurrentPhoto() {
var photo;
var isFacebook = thumbAction.classList.contains(FB_CLASS);
if (!isFacebook) {
photo = currentPhoto;
}
return photo; // we return undefined on purpose here
};
var CATEGORY_WHITE_LIST = ['gmail', 'live'];
function updateCategoryForImported(contact) {
if (Array.isArray(contact.category)) {
var total = CATEGORY_WHITE_LIST.length;
var idx = -1;
for (var i = 0; i < total; i++) {
idx = contact.category.indexOf(CATEGORY_WHITE_LIST[i]);
if (idx !== -1) {
break;
}
}
if (idx !== -1) {
contact.category[idx] = contact.category[idx] + '/updated';
}
}
}
var fillContact = function(contact, done) {
createName(contact);
getPhones(contact);
getEmails(contact);
getAddresses(contact);
getNotes(contact);
getDates(contact);
var currentPhoto = getCurrentPhoto();
if (!currentPhoto) {
done(contact);
return;
}
utils.thumbnailImage(currentPhoto, function gotTumbnail(thumbnail) {
if (currentPhoto !== thumbnail) {
contact.photo = [currentPhoto, thumbnail];
} else {
contact.photo = [currentPhoto];
}
done(contact);
});
};
var saveContact = function saveContact() {
saveButton.setAttribute('disabled', 'disabled');
showThrobber();
var cancelObject = {
title: 'ok',
callback: function onCancel() {
ConfirmDialog.hide();
continueSavingContact();
}
};
if (deletedTelNumber) {
isIceContact(currentContact, function(result) {
if (result === true) {
var msgId = 'ICEContactDelTel';
var selector = 'div:not([data-template]) .textfield[type="tel"]';
var telInputs = document.querySelectorAll(selector);
var hasNumber = false;
for (var i = 0, len = telInputs.length; i < len; i++) {
if (telInputs[i].value.trim()) {
hasNumber = true;
break;
}
}
if (!hasNumber) {
msgId = 'ICEContactDelTelAll';
ICEData.removeICEContact(currentContact.id);
}
ConfirmDialog.show(null, {'id': msgId},
cancelObject);
}
else {
continueSavingContact();
}
});
}
else {
continueSavingContact();
}
};
function continueSavingContact() {
currentContact = currentContact || {};
currentContact = deviceContact || currentContact;
var deviceGivenName = currentContact.givenName || [''];
var deviceFamilyName = currentContact.familyName || [''];
var myContact = {
id: document.getElementById('contact-form-id').value,
additionalName: [''],
name: ['']
};
var inputs = { givenName, familyName };
for (var field in inputs) {
var value = inputs[field].value;
if (value && value.length > 0) {
myContact[field] = [value];
} else {
myContact[field] = null;
}
}
if (!company.parentNode.classList.contains(FB_CLASS) &&
company.value && company.value.length > 0) {
myContact.org = [company.value];
}
if (currentContact.category) {
myContact.category = currentContact.category;
}
fillContact(myContact, function contactFilled(myContact) {
// Check if all fields but address are empty
// and inspect address by it self.
var fields = ['givenName', 'familyName', 'org', 'tel',
'email', 'note', 'bday', 'anniversary', 'adr'];
// Load what we need to check myContact
LazyLoader.load('/contacts/js/utilities/mozContact.js', function() {
if (utils.mozContact.haveEmptyFields(myContact, fields)) {
return;
}
var contact;
if (myContact.id) { //Editing a contact
currentContact.tel = [];
currentContact.email = [];
currentContact.org = [];
currentContact.adr = [];
currentContact.note = [];
currentContact.photo = [];
currentContact.bday = null;
currentContact.anniversary = null;
var readOnly = ['id', 'updated', 'published'];
for (var field in myContact) {
if (readOnly.indexOf(field) == -1) {
currentContact[field] = myContact[field];
}
}
contact = currentContact;
if (fb.isFbContact(contact)) {
// If it is a FB Contact not linked it will be automatically linked
// As now there is additional contact data entered by the user
if (!fb.isFbLinked(contact)) {
var fbContact = new fb.Contact(contact);
// Here the contact has been promoted to linked but not saved yet
fbContact.promoteToLinked();
}
setPropagatedFlag('givenName', deviceGivenName[0], contact);
setPropagatedFlag('familyName', deviceFamilyName[0], contact);
createName(contact);
}
} else {
contact = utils.misc.toMozContact(myContact);
}
updateCategoryForImported(contact);
var callbacks = cookMatchingCallbacks(contact);
cancelHandler = doCancel.bind(callbacks);
formHeader.addEventListener('action', cancelHandler);
doMatch(contact, callbacks);
});
});
}
var cookMatchingCallbacks = function cookMatchingCallbacks(contact) {
return {
onmatch: function(results) {
MatchService.showDuplicateContacts();
mergeHandler = function mergeHandler(e) {
if (e.origin !== CONTACTS_APP_ORIGIN) {
return;
}
var data = e.data;
switch (data.type) {
case 'duplicate_contacts_loaded':
// UI ready, passing duplicate contacts
var duplicateContacts = {};
Object.keys(results).forEach(function(id) {
duplicateContacts[id] = {
matchingContactId: id,
matchings: results[id].matchings
};
});
window.postMessage({
type: 'show_duplicate_contacts',
data: {
name: getCompleteName(getDisplayName(contact)),
duplicateContacts: duplicateContacts
}
}, CONTACTS_APP_ORIGIN);
break;
case 'merge_duplicate_contacts':
window.removeEventListener('message', mergeHandler);
Contacts.goBack();
// List of duplicate contacts to merge (identifiers)
var list = [];
var ids = data.data;
Object.keys(ids).forEach(id => {
list.push(results[id]);
});
doMerge(contact, list, function finished() {
// Contacts merged goes to contact list automatically without
// saving, the merger does the live better to us
if (ActivityHandler.currentlyHandling) {
ActivityHandler.postNewSuccess(contact);
}
window.postMessage({
type: 'duplicate_contacts_merged',
data: ids
}, CONTACTS_APP_ORIGIN);
});
break;
case 'ready':
// The list of duplicate contacts has been loaded
formHeader.removeEventListener('action', cancelHandler);
hideThrobber();
break;
case 'window_close':
// If user igonores duplicate contacts we save the contact
window.removeEventListener('message', mergeHandler);
Contacts.goBack();
doSave(contact, true);
break;
}
};
window.addEventListener('message', mergeHandler);
},
onmismatch: function() {
// Saving because there aren't duplicate contacts
doSave(contact);
formHeader.removeEventListener('action', cancelHandler);
}
};
};
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;
}
// Fills the contact data to display if no givenName and familyName
function getDisplayName(contact) {
if (hasName(contact)) {
return { givenName: contact.givenName, familyName: contact.familyName };
}
var givenName = [];
if (Array.isArray(contact.name) && contact.name.length > 0) {
givenName.push(contact.name[0]);
} else if (contact.org && contact.org.length > 0) {
givenName.push(contact.org[0]);
} else if (contact.tel && contact.tel.length > 0) {
givenName.push(contact.tel[0].value);
} else if (contact.email && contact.email.length > 0) {
givenName.push(contact.email[0].value);
} else {
givenName.push(_('noName'));
}
return { givenName: givenName, modified: true };
}
function hasName(contact) {
return (Array.isArray(contact.givenName) && contact.givenName[0] &&
contact.givenName[0].trim()) ||
(Array.isArray(contact.familyName) && contact.familyName[0] &&
contact.familyName[0].trim());
}
var doMerge = function doMerge(contact, list, cb) {
var callbacks = {
success: cb,
error: function(e) {
console.error('Failed merging duplicate contacts: ', e.name);
cb();
}
};
LazyLoader.load('/contacts/js/utilities/merge_helper.js', function() {
MergeHelper.merge(contact, list).then(callbacks.success, callbacks.error);
});
};
var doCancel = function doCancel() {
formHeader.removeEventListener('action', cancelHandler);
window.removeEventListener('message', mergeHandler);
this.onmatch = this.onmismatch = null;
window.postMessage({
type: 'abort',
data: ''
}, CONTACTS_APP_ORIGIN);
hideThrobber();
};
var doMatch = function doMatch(contact, callbacks) {
LazyLoader.load(['/shared/js/text_normalizer.js',
'/shared/js/simple_phone_matcher.js',
'/shared/js/contacts/contacts_matcher.js'], function() {
Matcher.match(contact, 'active', callbacks);
});
};
var doSave = function doSave(contact, noTransition) {
// Deleting auxiliary objects created for dates
delete contact.date;
// When we're adding new contact, it shouldn't have id at the beginning.
// If We have one, means we are editing the current contact.
// We will use this information below.
var editingContact = contact.id !== 'undefined';
ContactsService.save(
utils.misc.toMozContact(contact),
function(e) {
if (e) {
hideThrobber();
console.error('Error saving contact', e);
return;
}
hideThrobber();
// Reloading contact, as it only allows to be updated once
if (ActivityHandler.currentlyHandling) {
ActivityHandler.postNewSuccess(contact);
}
if (!noTransition) {
Contacts.cancel();
}
// Since editing current contact returns to the details view,
// and adding a new one returns to the contacts list,
// we call setCurrent() only in the first case,
// so NFC listeners are not set on the Contact List
// (Bug 1041455).
if (editingContact) {
Contacts.setCurrent(contact);
// The change event may arrive while the edit form is still visible.
Contacts.updateContactDetail(contact.id);
}
}
);
};
var showThrobber = function showThrobber() {
throbber.classList.remove('hide');
};
var hideThrobber = function hideThrobber() {
throbber.classList.add('hide');
};
/**
* Creates a complete name from the received contact's `givenName` and
* `familyName` fields.
*
* @param {object} contact MozContactObject to process
*/
var createName = function createName(contact) {
var givenName = '', familyName = '';
if (Array.isArray(contact.givenName)) {
givenName = contact.givenName[0].trim();
}
if (Array.isArray(contact.familyName)) {
familyName = contact.familyName[0].trim();
}
var completeName = (givenName + ' ' + familyName).trim();
contact.name = completeName ? [completeName] : [];
};
var setPropagatedFlag = function setPropagatedFlag(field, value, contact) {
if (!Array.isArray(contact[field]) || !contact[field][0] ||
!contact[field][0].trim()) {
// Here the user is deleting completely the field then we get the
// original facebook field value
fb.setPropagatedFlag(field, contact);
contact[field] = fbContact[field];
} else if (contact[field][0] !== value) {
// The user is changing the value of the field then we have a local field.
// It implies not propagation
fb.removePropagatedFlag(field, contact);
}
};
function isBuiltInType(type, tagList) {
return tagList.some(function(tag) {
return tag.type === type;
});
}
var getPhones = function getPhones(contact) {
var selector = '#view-contact-form form div.phone-template:not(.facebook)';
var phones = dom.querySelectorAll(selector);
for (var i = 0; i < phones.length; i++) {
var currentPhone = phones[i];
var index = currentPhone.dataset.index;
var numberField = dom.getElementById('number_' + index);
var value = numberField.value;
if (!value) {
continue;
}
var type = [dom.getElementById('tel_type_' + index).dataset.value || ''];
var carrierSelector = 'carrier_' + index;
var carrier = dom.getElementById(carrierSelector).value || '';
contact.tel = contact.tel || [];
/*jshint -W075 */
contact.tel.push({ value, type, carrier });
}
};
var getEmails = function getEmails(contact) {
var selector = '#view-contact-form form div.email-template:not(.facebook)';
var emails = dom.querySelectorAll(selector);
for (var i = 0; i < emails.length; i++) {
var currentEmail = emails[i];
var index = currentEmail.dataset.index;
var emailField = dom.getElementById('email_' + index);
var value = emailField.value;
value = value && value.trim();
if (!value) {
continue;
}
var type = [
dom.getElementById('email_type_' + index).dataset.value || ''];
contact.email = contact.email || [];
contact.email.push({ value, type });
}
};
var getDates = function getDate(contact) {
var selector = '#view-contact-form form div.date-template';
var dates = dom.querySelectorAll(selector);
var bdayVal = null, anniversaryVal = null;
for (var i = 0; i < dates.length; i++) {
var currentDate = dates[i];
if (dates[i].classList.contains(FB_CLASS)) {
continue;
}
var arrayIndex = currentDate.dataset.index;
var dateField = dom.getElementById('date_' + arrayIndex);
var dateValue = dateField.valueAsDate;
selector = 'date_type_' + arrayIndex;
var type = dom.getElementById(selector).dataset.value || '';
if (!dateValue || !type) {
continue;
}
// Date value is referred to current TZ but it is not needed to normalize
// as that will be done only when the date is presented to the user
// by calculating the corresponding offset
switch (type) {
case 'birthday':
bdayVal = dateValue;
break;
case 'anniversary':
anniversaryVal = dateValue;
break;
}
}
contact.bday = bdayVal;
contact.anniversary = anniversaryVal;
};
var getAddresses = function getAddresses(contact) {
var selector =
'#view-contact-form form div.address-template:not(.facebook)';
var addresses = dom.querySelectorAll(selector);
for (var i = 0; i < addresses.length; i++) {
var currentAddress = addresses[i];
var arrayIndex = currentAddress.dataset.index;
var addressField = dom.getElementById('streetAddress_' + arrayIndex);
var addressValue = addressField.value || '';
addressValue = addressValue.trim();
selector = 'address_type_' + arrayIndex;
var typeField = dom.getElementById(selector).dataset.value || '';
typeField = typeField.trim();
selector = 'locality_' + arrayIndex;
var locality = dom.getElementById(selector).value || '';
locality = locality.trim();
selector = 'postalCode_' + arrayIndex;
var postalCode = dom.getElementById(selector).value || '';
postalCode = postalCode.trim();
selector = 'countryName_' + arrayIndex;
var countryName = dom.getElementById(selector).value || '';
countryName = countryName.trim();
// Sanity check for pameters, check all params but the typeField
if (addressValue === '' && locality === '' &&
postalCode === '' && countryName === '') {
continue;
}
contact.adr = contact.adr || [];
contact.adr.push({
streetAddress: addressValue,
postalCode: postalCode,
locality: locality,
countryName: countryName,
type: [typeField]
});
}
};
var getNotes = function getNotes(contact) {
var selector = '#view-contact-form form div.note-template';
var notes = dom.querySelectorAll(selector);
for (var i = 0; i < notes.length; i++) {
var currentNote = notes[i];
var arrayIndex = currentNote.dataset.index;
var noteField = dom.getElementById('note_' + arrayIndex);
var noteValue = noteField.value;
noteValue = noteValue && noteValue.trim();
if (!noteValue) {
continue;
}
contact.note = contact.note || [];
contact.note.push(noteValue);
}
};
var resetForm = function resetForm() {
currentPhoto = null;
deletedTelNumber = false;
thumbAction.querySelector('p').classList.remove('hide');
var removeIcon = thumbAction.querySelector('button.' + IMG_DELETE_CLASS);
if (removeIcon) {
thumbAction.removeChild(removeIcon);
}
saveButton.removeAttribute('disabled');
addNewDateButton.disabled = false;
resetRemoved();
currentContactId.value = '';
currentContact = {};
givenName.value = '';
familyName.value = '';
company.value = '';
thumb.style.backgroundImage = '';
var phones = dom.querySelector('#contacts-form-phones');
var emails = dom.querySelector('#contacts-form-emails');
var addresses = dom.querySelector('#contacts-form-addresses');
var dates = dom.querySelector('#contacts-form-dates');
var notes = dom.querySelector('#contacts-form-notes');
[phones, emails, addresses, dates, notes].forEach(
utils.dom.removeChildNodes);
counters = {
'tel': 0,
'email': 0,
'adr': 0,
'date': 0,
'note': 0
};
textFieldsCache.clear();
formView.scrollTop = 0;
};
var resetRemoved = function cf_resetRemoved() {
var removedFields = dom.querySelectorAll('.facebook');
for (var i = 0; i < removedFields.length; i++) {
removedFields[i].classList.remove(FB_CLASS);
}
thumbAction.classList.remove('with-photo');
var removeButton = thumbAction.querySelector('button');
if (removeButton) {
thumbAction.removeChild(removeButton);
}
};
var checkDisableButton = function checkDisableButton() {
var saveButton = dom.getElementById('save-button');
if (emptyForm()) {
saveButton.setAttribute('disabled', 'disabled');
} else {
saveButton.removeAttribute('disabled');
}
};
var emptyForm = function emptyForm() {
var textFields = textFieldsCache.get();
for (var i = textFields.length - 1; i >= 0; i--) {
if ((textFields[i].value && textFields[i].value.trim()) ||
(textFields[i].valueAsDate)) {
return false;
}
}
return true;
};
var removeFieldIcon = function removeFieldIcon(selector, type) {
var delButton = document.createElement('button');
delButton.className = IMG_DELETE_CLASS; // + ' fillflow-row-action';
delButton.setAttribute('data-l10n-id', 'removeField');
delButton.setAttribute('data-type', type);
delButton.onclick = function removeElement(event) {
// Workaround until 809452 is fixed.
// What we are avoiding with this condition is removing / restoring
// a field when the event is simulated by a ENTER Keyboard click
if (event.clientX === 0 && event.clientY === 0) {
return false;
}
event.preventDefault();
var container = configs[type].container;
var elem = document.getElementById(selector);
if (type !== 'photo') {
elem.parentNode.removeChild(elem);
if (container.querySelectorAll('[data-field]').length === 0) {
container.classList.add('empty');
}
}
else {
// TODO: Implement the new delete image flow
console.warn('Delete image');
}
if (type === 'tel') {
deletedTelNumber = true;
}
// Update the aria label for acessibility
var delButton = event.target;
delButton.setAttribute('data-l10n-id', 'removeField');
counters[type]--;
// In this version only two dates are allowed
if (type === 'date') {
checkAddDateButton();
}
textFieldsCache.clear();
checkDisableButton();
};
return delButton;
};
var addRemoveIconToPhoto = function cf_addRemIconPhoto() {
// Ensure the removed and FB class names are conveniently reseted
thumbAction.classList.remove(FB_CLASS);
var out = thumbAction.querySelector('button.' + IMG_DELETE_CLASS);
if (!out) {
out = removeFieldIcon(thumbAction.id, 'photo');
thumbAction.appendChild(out);
}
else {
// Ensure it is visible
out.classList.remove('hide');
}
thumbAction.classList.add('with-photo');
return out;
};
function canRemovePhoto() {
var out = getCurrentPhoto() !== null;
if (fb.isFbContact(currentContact)) {
out = Array.isArray(deviceContact.photo) && deviceContact.photo[0];
}
return out;
}
function photoAction() {
if (canRemovePhoto()) {
removeOrUpdatePhoto();
}
else {
pickImage();
}
}
function removeOrUpdatePhoto() {
LazyLoader.load('/contacts/js/action_menu.js', function() {
var prompt = new ActionMenu('photo-options');
prompt.addToList({id: 'remove-photo'}, removePhoto);
prompt.addToList({id: 'change-photo'}, pickImage);
prompt.show();
});
}
function removePhoto() {
currentPhoto = null;
// If photo is removed, the FB photo of a contact is always restoredf
if (fb.isFbContact(currentContact)) {
// The local contact now does not have a photo
deviceContact.photo = null;
var fbPhoto = ContactPhotoHelper.getFullResolution(fbContact);
utils.dom.updatePhoto(fbPhoto, thumb);
}
else {
thumbAction.classList.remove('with-photo');
utils.dom.updatePhoto(null, thumb);
}
if (!emptyForm()) {
saveButton.removeAttribute('disabled');
}
}
var pickImage = function pickImage() {
var activity = new MozActivity({
name: 'pick',
data: {
type: 'image/jpeg',
maxFileSizeBytes: MAX_PHOTO_SIZE
}
});
activity.onsuccess = function success() {
addRemoveIconToPhoto();
if (!emptyForm()) {
saveButton.removeAttribute('disabled');
}
// XXX
// this.result.blob is valid now, but it won't stay valid
// (see https://bugzilla.mozilla.org/show_bug.cgi?id=806503)
// And it might not be the size we want, anyway, so we make
// our own copy that is at the right size.
resizeBlob(this.result.blob, PHOTO_WIDTH, PHOTO_HEIGHT,
function(resized) {
utils.dom.updatePhoto(resized, thumb);
currentPhoto = resized;
if (fb.isFbContact(currentContact)) {
// We temporarily mark that there is a local photo chosen
deviceContact.photo = [currentPhoto];
}
});
};
activity.onerror = function() {
window.console.error('Error in the activity', activity.error);
};
return false;
};
function resizeBlob(blob, target_width, target_height, 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() {
var image_width = img.width;
var image_height = img.height;
var scalex = image_width / target_width;
var scaley = image_height / target_height;
var scale = Math.min(scalex, scaley);
var w = target_width * scale;
var h = target_height * scale;
var x = (image_width - w) / 2;
var y = (image_height - h) / 2;
var canvas = document.createElement('canvas');
canvas.width = target_width;
canvas.height = target_height;
var context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(img, x, y, w, h, 0, 0, target_width, target_height);
cleanupImg();
canvas.toBlob(function(resized) {
context = null;
canvas.width = canvas.height = 0;
canvas = null;
callback(resized);
} , 'image/jpeg');
};
}
return {
'init': init,
'render': render,
'insertField': insertField,
'saveContact': saveContact,
'onNewFieldClicked': onNewFieldClicked
};
})();