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