<commit_before>/* 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:';
  var LAST_IMPORT_DATE_KEY = 'last_import_date';

  // 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) {
    // Copy the access_token
    accessToken = access_token;
    photoUrls = {};
    return getContactsGroup(access_token);
  };

  var getContactsGroup = function getContactsGroup(access_token) {
    return performAPIRequest(GROUPS_END_POINT, access_token)
      .then((response) => {
        // Locate the entry witch systemGroup id is 'Contacts'
        var feed = response.querySelector('feed');
        if (feed === null) {
          return Promise.reject('feed is null');
        }

        var sgc = feed.querySelector('systemGroup[id="Contacts"]');
        if (sgc !== null) {
          var id = sgc.parentNode.querySelector('id').textContent;
          return getContactsByGroup(id, access_token);
        } else {
          Promise.reject('No systemGroup with id "Contacts" found');
        }
    });
  };

  // Retrieve all the contacts for the specific groupId
  var getContactsByGroup = function getContactsByGroup(groupId, access_token) {

    var groupUrl = END_POINT + '&group=' + groupId;
    return performAPIRequest(groupUrl, access_token).then((response) => {
      // extract updated
      var updated = new Date(response.querySelector('updated').textContent);
      return {
        'data': nodeListToArray(response),
        'updated': updated
      };
    });
  };

  // Given a Google contacts api url add the authentication and
  // extra headers to perform the correct request
  var performAPIRequest = function performAPIRequest(url, access_token) {
    return Rest.get(url, {
      '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.etag = getEtag(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;
  };

  var getEtag = function getEtag(contact) {
    return contact.getAttribute('gd:etag');
  };

  // 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) {
    var url = buildContactPhotoURL(googleContact, access_token);
    return Rest.get(url, { '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(accessToken) {
    var now = new Date();
    var lastImportDateStr = localStorage.getItem(LAST_IMPORT_DATE_KEY);
    var lastImportDate = lastImportDateStr ? new Date(lastImportDateStr) : null;

    if (!lastImportDate || isNaN(lastImportDate.getTime())) {
      // no valid date, import all
      return this.listAllContacts(accessToken)
      .then((result) => {
        var importer = this.getImporter(result.data, accessToken);
        return importer.start();
      }).then(() => {
        // here we consider the import as success
        localStorage.setItem(LAST_IMPORT_DATE_KEY, now);
      })
      // TODO proper error handling.
      .catch(e => {
        console.error(e)
        return Promise.reject(e);
      });
    } else {
      // do something
      return Promise.resolve();
    }
  };

  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
  };

})();
<commit_msg>Cleanly getting the "My contacts" group.<commit_after>/* 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:';
  var LAST_IMPORT_DATE_KEY = 'last_import_date';

  // 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) {
    // Copy the access_token
    // FIXME why?
    accessToken = access_token;
    photoUrls = {};
    return getContactsGroupId(access_token).then((id) => {
      return getContactsByGroup(id, accessToken);
    });
  };

  var getContactsGroupId = function getContactsGroupId(access_token) {
    return performAPIRequest(GROUPS_END_POINT, access_token)
      .then((response) => {
        // Locate the entry witch systemGroup id is 'Contacts'
        var feed = response.querySelector('feed');
        if (feed === null) {
          return Promise.reject('feed is null');
        }

        var sgc = feed.querySelector('systemGroup[id="Contacts"]');
        if (sgc !== null) {
          // return id
          return sgc.parentNode.querySelector('id').textContent;
        } else {
          Promise.reject('No systemGroup with id "Contacts" found');
        }
    });
  };

  // Retrieve all the contacts for the specific groupId
  var getContactsByGroup = function getContactsByGroup(groupId, access_token) {

    var groupUrl = END_POINT + '&group=' + groupId;
    return performAPIRequest(groupUrl, access_token).then((response) => {
      // extract updated
      var updated = new Date(response.querySelector('updated').textContent);
      return {
        'data': nodeListToArray(response),
        'updated': updated
      };
    });
  };

  // Given a Google contacts api url add the authentication and
  // extra headers to perform the correct request
  var performAPIRequest = function performAPIRequest(url, access_token) {
    return Rest.get(url, {
      '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.etag = getEtag(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;
  };

  var getEtag = function getEtag(contact) {
    return contact.getAttribute('gd:etag');
  };

  // 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) {
    var url = buildContactPhotoURL(googleContact, access_token);
    return Rest.get(url, { '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(accessToken) {
    var now = new Date();
    var lastImportDateStr = localStorage.getItem(LAST_IMPORT_DATE_KEY);
    var lastImportDate = lastImportDateStr ? new Date(lastImportDateStr) : null;

    if (!lastImportDate || isNaN(lastImportDate.getTime())) {
      // no valid date, import all
      return this.listAllContacts(accessToken)
      .then((result) => {
        var importer = this.getImporter(result.data, accessToken);
        return importer.start();
      }).then(() => {
        // here we consider the import as success
        localStorage.setItem(LAST_IMPORT_DATE_KEY, now);
      })
      // TODO proper error handling.
      .catch(e => {
        console.error(e)
        return Promise.reject(e);
      });
    } else {
      // do something
      return Promise.resolve();
    }
  };

  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
  };

})();
<|endoftext|>


<commit_before>'use strict';

module.exports = File;

/**
 * Contains information about a file, such as its path, type, and contents.
 *
 * @param {string} url - The absolute or relative URL of the file
 * @param {*} data - The contents of the file, if available
 * @param {Schema} schema - The JSON Schema that the file is part of
 *
 * @class
 */
function File (url, data, schema) {
  /**
   * The {@link Schema} that this file belongs to.
   *
   * @type {Schema}
   */
  this.schema = schema;

  /**
   * The file's full (absolute) URL, without any fragment (#)
   *
   * NOTE: The {@link File#url} property is always url-encoded, even for local filesystem paths.
   * To get the url-decoded path for user-friendly display purposes or for accessing the local filesystem,
   * use the {@link File#path} instead.
   *
   * @type {string}
   */
  this.url = plugins.encodeURI(url);

  /**
   * This is the same as {@link File#url}, except that it is NOT url-encoded.
   *
   * This is most useful for local filesystem paths, since "file:///foo%20bar.json" will be
   * converted to "/foo bar.json".
   *
   * For HTTP URLs, this property is simply the url-decoded version of {@link File#url},
   * which may still be useful for display or logging purposes, since it will be more human-readable.
   *
   * @type {string}
   */
  this.path = plugins.decodeURI(url);

  /**
   * The file's MIME type (e.g. "application/json", "text/html", etc.), if known.
   *
   * @type {?string}
   */
  this.mimeType = undefined;

  /**
   * The file's data. This can be any data type, including a string, object, array, binary, etc.
   *
   * @type {*}
   */
  this.data = undefined;
}

/**
 * Returns a human-friendly representation of the File object.
 *
 * @returns {string}
 */
File.prototype.toString = function toString () {
  return this.path;
};
<commit_msg>added a `File.toJSON()` method<commit_after>'use strict';

module.exports = File;

/**
 * Contains information about a file, such as its path, type, and contents.
 *
 * @param {string} url - The absolute or relative URL of the file
 * @param {Schema} schema - The JSON Schema that the file is part of
 *
 * @class
 */
function File (url, schema) {
  /**
   * The {@link Schema} that this file belongs to.
   *
   * @type {Schema}
   */
  this.schema = schema;

  /**
   * The file's full (absolute) URL, without any fragment (#)
   *
   * NOTE: The {@link File#url} property is always url-encoded, even for local filesystem paths.
   * To get the url-decoded path for user-friendly display purposes or for accessing the local filesystem,
   * use the {@link File#path} instead.
   *
   * @type {string}
   */
  this.url = plugins.encodeURI(url);

  /**
   * This is the same as {@link File#url}, except that it is NOT url-encoded.
   *
   * This is most useful for local filesystem paths, since "file:///foo%20bar.json" will be
   * converted to "/foo bar.json".
   *
   * For HTTP URLs, this property is simply the url-decoded version of {@link File#url},
   * which may still be useful for display or logging purposes, since it will be more human-readable.
   *
   * @type {string}
   */
  this.path = plugins.decodeURI(url);

  /**
   * The file's MIME type (e.g. "application/json", "text/html", etc.), if known.
   *
   * @type {?string}
   */
  this.mimeType = undefined;

  /**
   * The file's data. This can be any data type, including a string, object, array, binary, etc.
   *
   * @type {*}
   */
  this.data = undefined;
}

/**
 * Returns a human-friendly representation of the File object.
 *
 * @returns {string}
 */
File.prototype.toString = function toString () {
  return this.path;
};

/**
 * Allows the File object to be safely serialized as JSON by removing circular references.
 *
 * @returns {object}
 */
File.prototype.toJSON = function toJSON () {
  var keys = Object.keys(this);
  var json = {};

  for (var i = 0; i < keys.length; i++) {
    var key = Object.keys[i];
    if (key !== 'schema') {
      json[key] = this[key];
    }
  }

  return json;
};
<|endoftext|>


<commit_before>/*!
 * API for writing final JS
 */

/**
 * Returns a string for the beginning of concatenation with __RESULT__
 * @return {string}
 */
DirObj.prototype.$ = function () {
	if (this.domComment) {
		return '__COMMENT_RESULT__ +=';
	}

	switch (this.renderMode) {
		case 'stringBuffer':
			return '__RESULT__.push(';

		case 'dom':
			return '__APPEND__(__RESULT__[__RESULT__.length - 1],';

		default:
			return '__RESULT__ +=';
	}
};

/**
 * Returns a string for the ending of concatenation with __RESULT__
 * @return {string}
 */
DirObj.prototype.$$ = function () {
	if (this.domComment) {
		return '';
	}

	switch (this.renderMode) {
		case 'stringBuffer':
			return ')';

		case 'dom':
			return ')';

		default:
			return '';
	}
};

/**
 * Appends a string to __RESULT__
 *
 * @param {?string=} [opt_str] - the source string
 * @return {string}
 */
DirObj.prototype.wrap = function (opt_str) {
	return this.$() + (opt_str || '') + this.$$() + ';';
};

/**
 * Returns a string of node declaration
 * (for renderMode == dom)
 *
 * @param {?boolean=} [opt_inline=false] - if is true, then the node considered as inline
 * @return {string}
 */
DirObj.prototype.returnPushNodeDecl = function (opt_inline) {
	return /* cbws */`
		${this.wrap('__NODE__')}
		${opt_inline ? '' : '__RESULT__.push(__NODE__);'}
		__NODE__ = null;
	`;
};

/**
 * Returns a string of a template content
 * @return {string}
 */
DirObj.prototype.returnResult = function () {
	switch (this.renderMode) {
		case 'stringBuffer':
			return '__RESULT__.join(\'\')';

		case 'dom':
			return '__RESULT__[0]';

		default:
			return '__RESULT__';
	}
};

/**
 * Returns a string of template declaration
 * @return {string}
 */
DirObj.prototype.declResult = function () {
	switch (this.renderMode) {
		case 'stringBuffer':
			return 'new Snakeskin.StringBuffer()';

		case 'dom':
			return '[document.createDocumentFragment()]';

		default:
			return '\'\'';
	}
};

/**
 * Replaces CDATA blocks in a string
 * and returns a new string
 *
 * @param {string} str - the source string
 * @return {string}
 */
DirObj.prototype.replaceCData = function (str) {
	const
		s = ADV_LEFT_BLOCK + LEFT_BLOCK,
		e = RIGHT_BLOCK;

	return str
		.replace(new RegExp(`${s}cdata${e}([\\s\\S]*?)${s}(?:\\/cdata|end cdata)${e}`, 'g'), (sstr, data) => {
			this.cDataContent.push(data);
			return String(
					// The count of added lines
					`${s}__appendLine__ ${(data.match(new RegExp(nextLineRgxp.source, 'g')) || '').length}${e}` +

					// Label to replace CDATA
					`__CDATA__${this.cDataContent.length - 1}_`
			);
		});
};

/**
 * Declares the end of templates declaration
 *
 * @param {?string} cacheKey - a cache key
 * @param {(Date|string)} label - the declaration label
 * @return {!DirObj}
 */
DirObj.prototype.end = function (cacheKey, label) {
	label = label || '';
	switch (this.renderMode) {
		case 'stringBuffer':
			this.res = this.res.replace(/__RESULT__\.push\(''\);/g, '');
			break;

		case 'dom':
			this.res = this.res.replace(/__APPEND__\(__RESULT__\[__RESULT__\.length - 1],''\);/g, '');
			break;

		default:
			this.res = this.res.replace(/__RESULT__ \+= '';/g, '');
			break;
	}

	let includes = '';
	if (this.module.key.length) {
		includes = JSON.stringify(this.module.key);
	}

	this.res = this.pasteDangerBlocks(this.res)
		.replace(
			/__CDATA__(\d+)_/g,
			(sstr, pos) => escapeNextLine(
					this.cDataContent[pos].replace(new RegExp(nextLineRgxp.source, 'g'), this.lineSeparator)
				).replace(/'/g, ''')
		);

	const
		versionDecl = `Snakeskin v${Snakeskin.VERSION.join('.')}`,
		keyDecl = `key <${cacheKey}>`,
		labelDecl = `label <${label.valueOf()}>`,
		includesDecl = `includes <${includes}>`,
		generatedAtDecl = `generated at <${new Date().valueOf()}>`,
		resDecl = `${this.lineSeparator}   ${this.res}`;

	this.res = `/* ${versionDecl}, ${keyDecl}, ${labelDecl}, ${includesDecl}, ${generatedAtDecl}.${resDecl}`;
	this.res += /* cbws */`
			}

			if (!__IS_NODE__ && !__HAS_EXPORTS__) {
				__INIT__();
			}

		}).call(this);
	`;

	return this;
};

/**
 * Returns true, if is possible to write in the JS string
 * @return {boolean}
 */
DirObj.prototype.isSimpleOutput = function () {
	if (getDirName(this.name) !== 'end' && this.strong) {
		this.error(`the directive "${this.structure.name}" can not be used with a "${this.strong}"`);
		return false;
	}

	return !this.parentTplName && !this.protoStart && !this.outerLink && (!this.proto || !this.proto.parentTplName);
};

/**
 * Returns true, if a directive is ready to test
 * @return {boolean}
 */
DirObj.prototype.isReady = function () {
	return !this.protoStart && (!this.proto || !this.proto.parentTplName);
};

/**
 * Returns true, if
 *     !proto && !outerLink &&
 *     (
 *         parentTplName && !hasParentBlock ||
 *         !parentTplName
 *     )
 *
 * @return {boolean}
 */
DirObj.prototype.isAdvTest = function () {
	const res = (
		!this.proto && !this.outerLink &&
		(
			(this.parentTplName && !this.hasParentBlock({
				'block': true,
				'proto': true
			})) ||
			!this.parentTplName
		)
	);

	return Boolean(res);
};

/**
 * Adds a string to the JS string
 *
 * @param {string=} str - the source string
 * @param {?boolean=} [opt_interface=false] - if is true, then the current operation is an interface
 * @param {(boolean|number)=} [opt_jsDoc] - the last position of JSdoc or false
 * @return {boolean}
 */
DirObj.prototype.save = function (str, opt_interface, opt_jsDoc) {
	if (str === undefined) {
		return false;
	}

	if (!this.tplName || write[this.tplName] !== false || opt_interface) {
		if (opt_jsDoc) {
			const pos = Number(opt_jsDoc);
			this.res = this.res.substring(0, pos) + str + this.res.substring(pos);

		} else {
			this.res += str;
		}

		return true;
	}

	return false;
};

/**
 * Adds a string to the JS string
 * (with this.isSimpleOutput())
 *
 * @param {string=} str - the source string
 * @return {boolean}
 */
DirObj.prototype.append = function (str) {
	if (!this.isSimpleOutput()) {
		return false;
	}

	return this.save(str);
};

/**
 * Executes a string
 * (with this.isSimpleOutput())
 *
 * @param {function(this:DirObj)} callback - a callback function
 * @return {boolean}
 */
DirObj.prototype.mod = function (callback) {
	if (!this.isSimpleOutput()) {
		return false;
	}

	if (!this.tplName || write[this.tplName] !== false) {
		callback.call(this);
		return true;
	}

	return false;
};
<commit_msg>Refactoring<commit_after>/*!
 * API for writing final JS
 */

/**
 * Returns a string for the beginning of concatenation with __RESULT__
 * @return {string}
 */
DirObj.prototype.$ = function () {
	if (this.domComment) {
		return '__COMMENT_RESULT__ +=';
	}

	switch (this.renderMode) {
		case 'stringBuffer':
			return '__RESULT__.push(';

		case 'dom':
			return '__APPEND__(__RESULT__[__RESULT__.length - 1],';

		default:
			return '__RESULT__ +=';
	}
};

/**
 * Returns a string for the ending of concatenation with __RESULT__
 * @return {string}
 */
DirObj.prototype.$$ = function () {
	if (this.domComment) {
		return '';
	}

	switch (this.renderMode) {
		case 'stringBuffer':
			return ')';

		case 'dom':
			return ')';

		default:
			return '';
	}
};

/**
 * Appends a string to __RESULT__
 *
 * @param {?string=} [opt_str] - the source string
 * @return {string}
 */
DirObj.prototype.wrap = function (opt_str) {
	return this.$() + (opt_str || '') + this.$$() + ';';
};

/**
 * Returns a string of node declaration
 * (for renderMode == dom)
 *
 * @param {?boolean=} [opt_inline=false] - if is true, then the node considered as inline
 * @return {string}
 */
DirObj.prototype.returnPushNodeDecl = function (opt_inline) {
	return /* cbws */`
		${this.wrap('__NODE__')}
		${opt_inline ? '' : '__RESULT__.push(__NODE__);'}
		__NODE__ = null;
	`;
};

/**
 * Returns a string of a template content
 * @return {string}
 */
DirObj.prototype.returnResult = function () {
	switch (this.renderMode) {
		case 'stringBuffer':
			return '__RESULT__.join(\'\')';

		case 'dom':
			return '__RESULT__[0]';

		default:
			return '__RESULT__';
	}
};

/**
 * Returns a string of template declaration
 * @return {string}
 */
DirObj.prototype.declResult = function () {
	switch (this.renderMode) {
		case 'stringBuffer':
			return 'new Snakeskin.StringBuffer()';

		case 'dom':
			return '[document.createDocumentFragment()]';

		default:
			return '\'\'';
	}
};

/**
 * Replaces CDATA blocks in a string
 * and returns a new string
 *
 * @param {string} str - the source string
 * @return {string}
 */
DirObj.prototype.replaceCData = function (str) {
	const
		s = ADV_LEFT_BLOCK + LEFT_BLOCK,
		e = RIGHT_BLOCK;

	return str
		.replace(new RegExp(`${s}cdata${e}([\\s\\S]*?)${s}(?:\\/cdata|end cdata)${e}`, 'g'), (sstr, data) => {
			this.cDataContent.push(data);
			return String(
					// The count of added lines
					`${s}__appendLine__ ${(data.match(new RegExp(nextLineRgxp.source, 'g')) || '').length}${e}` +

					// Label to replace CDATA
					`__CDATA__${this.cDataContent.length - 1}_`
			);
		});
};

/**
 * Declares the end of templates declaration
 *
 * @param {?string} cacheKey - a cache key
 * @param {(Date|string)} label - the declaration label
 * @return {!DirObj}
 */
DirObj.prototype.end = function (cacheKey, label) {
	label = label || '';
	switch (this.renderMode) {
		case 'stringBuffer':
			this.res = this.res.replace(/__RESULT__\.push\(''\);/g, '');
			break;

		case 'dom':
			this.res = this.res.replace(/__APPEND__\(__RESULT__\[__RESULT__\.length - 1],''\);/g, '');
			break;

		default:
			this.res = this.res.replace(/__RESULT__ \+= '';/g, '');
			break;
	}

	let includes = '';
	if (this.module.key.length) {
		includes = JSON.stringify(this.module.key);
	}

	this.res = this.pasteDangerBlocks(this.res)
		.replace(
			/__CDATA__(\d+)_/g,
			(sstr, pos) => escapeNextLine(
					this.cDataContent[pos].replace(new RegExp(nextLineRgxp.source, 'g'), this.lineSeparator)
				).replace(/'/g, ''')
		);

	const
		versionDecl = `Snakeskin v${Snakeskin.VERSION.join('.')}`,
		keyDecl = `key <${cacheKey}>`,
		labelDecl = `label <${label.valueOf()}>`,
		includesDecl = `includes <${includes}>`,
		generatedAtDecl = `generated at <${new Date().valueOf()}>`,
		resDecl = `${this.lineSeparator}   ${this.res}`;

	this.res = `/* ${versionDecl}, ${keyDecl}, ${labelDecl}, ${includesDecl}, ${generatedAtDecl}.${resDecl}`;
	this.res += /* cbws */`
			}

			if (!__IS_NODE__ && !__HAS_EXPORTS__) {
				__INIT__();
			}

		}).call(this);
	`;

	return this;
};

/**
 * Returns true, if is possible to write in the JS string
 * @return {boolean}
 */
DirObj.prototype.isSimpleOutput = function () {
	if (getDirName(this.name) !== 'end' && this.strong) {
		this.error(`the directive "${this.structure.name}" can not be used with a "${this.strong}"`);
		return false;
	}

	return !this.parentTplName && !this.protoStart && !this.outerLink && (!this.proto || !this.proto.parentTplName);
};

/**
 * Returns true, if a directive is ready to test
 * @return {boolean}
 */
DirObj.prototype.isReady = function () {
	return !this.protoStart && (!this.proto || !this.proto.parentTplName);
};

/**
 * Returns true, if
 *     !proto && !outerLink &&
 *     (
 *         parentTplName && !hasParentBlock ||
 *         !parentTplName
 *     )
 *
 * @return {boolean}
 */
DirObj.prototype.isAdvTest = function () {
	const res = (
		!this.proto && !this.outerLink &&
		(
			(this.parentTplName && !this.hasParentBlock({
				'block': true,
				'proto': true
			})) ||
			!this.parentTplName
		)
	);

	return Boolean(res);
};

/**
 * Adds a string to the JS string
 *
 * @param {string=} str - the source string
 * @param {?boolean=} [opt_interface=false] - if is true, then the current operation is an interface
 * @param {(boolean|number)=} [opt_jsDoc] - the last position of JSdoc or false
 * @return {boolean}
 */
DirObj.prototype.save = function (str, opt_interface, opt_jsDoc) {
	if (str === undefined) {
		return false;
	}

	if (!this.tplName || write[this.tplName] !== false || opt_interface) {
		if (opt_jsDoc) {
			const pos = Number(opt_jsDoc);
			this.res = this.res.substring(0, pos) + str + this.res.substring(pos);

		} else {
			this.res += str;
		}

		return true;
	}

	return false;
};

/**
 * Adds a string to the JS string
 * (with this.isSimpleOutput())
 *
 * @param {string=} str - the source string
 * @param {?boolean=} [opt_interface=false] - if is true, then the current operation is an interface
 * @param {(boolean|number)=} [opt_jsDoc] - the last position of JSdoc or false
 * @return {boolean}
 */
DirObj.prototype.append = function (str, opt_interface, opt_jsDoc) {
	if (!this.isSimpleOutput()) {
		return false;
	}

	return this.save(str, opt_interface, opt_jsDoc);
};

/**
 * Calls a callback function, if is possible to write to the JS string
 *
 * @param {function(this:DirObj)} callback - a callback function
 * @return {boolean}
 */
DirObj.prototype.mod = function (callback) {
	if (!this.isSimpleOutput()) {
		return false;
	}

	if (!this.tplName || write[this.tplName] !== false) {
		callback.call(this);
		return true;
	}

	return false;
};
<|endoftext|>


<commit_before>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks may share information by attaching to global.phet.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );
var child_process = require( 'child_process' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var pullAll = require( '../../../chipper/js/grunt/pullAll' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var cloneDependencies = require( '../../../chipper/js/grunt/cloneDependencies' );
var bumpVersion = require( '../../../chipper/js/grunt/bumpVersion' );
var stringReport = require( '../../../chipper/js/grunt/stringReport' );
var generateLicenseText = require( '../../../chipper/js/grunt/generateLicenseText' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );

/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  var FALLBACK_LOCAL = 'en';

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Delete arch references from the minified file, but only if it is not an arch build.
  var archRequired = pkg.preload && _.find( pkg.preload, function( repo ) { return repo === '../together/js/arch.js'; } ) !== undefined;
  if ( !archRequired ) {
    globalDefs.arch = false;
  }

  // Project configuration.
  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: [ 'js/**/*.js' ],

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: [ _.map( pkg.phetLibs, function( repo ) { return '../' + repo + '/js/**/*.js'; } ), '!../kite/js/parser/svgPath.js' ],

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  var clean = function() {
    if ( fs.existsSync( 'build' ) ) {
      grunt.log.writeln( 'Cleaning build directory' );
      grunt.file.delete( 'build' );
    }
    grunt.file.mkdir( 'build' );
  };

  // Default task ('grunt')
  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  // Other tasks ('grunt taskName')
  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );
  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );
  grunt.registerTask( 'clean', 'Erases the build/ directory and all its contents, and recreates the build/ directory', clean );
  grunt.registerTask( 'generate-license-text',
    'Generates the license text that will be written to the HTML file',
    function() {
      assert( pkg.preload, 'preload required in package.json' );
      generateLicenseText( grunt, pkg.preload );
    } );
  grunt.registerTask( 'build-no-lint',
    'identical to "build", but does not run "lint-all"',
    [ 'clean', 'generate-license-text', 'before-requirejs-build', 'requirejs:build', 'after-requirejs-build' ] );
  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    '--all-locales true:\n\tto build HTML for all locales in strings/\n' +
    '--locales $project:\n\tuse locales inferred from another project\'s strings/ directory\n' +
    '--locale fr:\n\tto build just the French locale\n' +
    '[no options]:\n\tto build just the English locale',
    [ 'lint-all', 'build-no-lint' ] );

  grunt.registerTask( 'string-report',
    'After doing a build, reports on which strings are missing for each locale that was built.',
    function() {
      stringReport( grunt, pkg.name, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'checkout-shas', 'Check out shas for a project, as specified in dependencies.json', function() {
    checkoutShas( grunt, pkg.name, false );
  } );

  grunt.registerTask( 'checkout-master', 'Check out master branch for all dependencies, as specified in dependencies.json', function() {
    checkoutShas( grunt, pkg.name, true );
  } );

  grunt.registerTask( 'pull-all', 'Pull all repo above this directory', function() {
    pullAll( grunt, child_process, assert, pkg.name );  //TODO this looks wrong, why passing in child_process and assert?
  } );

  grunt.registerTask( 'before-requirejs-build', '(internal use only) Do things before the requirejs:build task', function() {
    assert( pkg.phetLibs, 'phetLibs missing from package.json' );
    assert( pkg.preload, 'preload missing from package.json' );
    beforeRequirejsBuild( grunt, pkg.name, pkg.version, pkg.phetLibs, pkg.preload, FALLBACK_LOCAL );
  } );

  grunt.registerTask( 'after-requirejs-build', '(internal use only) Do things after the requirejs:build task', function() {
    afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
  } );

  grunt.registerTask( 'create-sim', 'Create a sim based on the simula-rasa template.  Example usage: grunt create-sim --name=cannon-blaster --author="Jane Smith (Smith Inc.)"', function() {
    createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'overwrite' ) );
  } );

  grunt.registerTask( 'generate-published-README', 'Generates README.md file for a published simulation.', function() {
    assert( pkg.phetLibs, 'phetLibs missing from package.json' );
    assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
    generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, true /* published */ );
  } );

  grunt.registerTask( 'generate-unpublished-README', 'Generates README.md file for an unpublished simulation.', function() {
    assert( pkg.phetLibs, 'phetLibs missing from package.json' );
    assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
    generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, false /* published */ );
  } );

  grunt.registerTask( 'clone-dependencies', 'Clones all dependencies of a project, as listed in package.json phetLibs entry', function() {
    assert( pkg.phetLibs, 'phetLibs missing from package.json' );
    cloneDependencies( grunt, pkg.name, pkg.phetLibs );
  } );

  grunt.registerTask( 'bump-version',
    'This task updates the last value in the version by one. For example from 0.0.0-dev.12 to 0.0.0-dev.13.' +
    'This updates the package.json and js/version.js files, and commits + pushes to git.' +
    'BEWARE: Do not run this task unless your git is clean, otherwise it will commit other work on your repo as well.',
    function() {
      bumpVersion( grunt, pkg.version );
    } );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<commit_msg>#108 reorganize Gruntfile, group tasks into "primary" and "utility"<commit_after>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );
var child_process = require( 'child_process' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var pullAll = require( '../../../chipper/js/grunt/pullAll' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var cloneDependencies = require( '../../../chipper/js/grunt/cloneDependencies' );
var bumpVersion = require( '../../../chipper/js/grunt/bumpVersion' );
var stringReport = require( '../../../chipper/js/grunt/stringReport' );
var generateLicenseText = require( '../../../chipper/js/grunt/generateLicenseText' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  var FALLBACK_LOCAL = 'en';

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Delete arch references from the minified file, but only if it is not an arch build.
  var archRequired = pkg.preload && _.find( pkg.preload, function( repo ) { return repo === '../together/js/arch.js'; } ) !== undefined;
  if ( !archRequired ) {
    globalDefs.arch = false;
  }

  // Project configuration.
  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: [ 'js/**/*.js' ],

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: [ _.map( pkg.phetLibs, function( repo ) { return '../' + repo + '/js/**/*.js'; } ), '!../kite/js/parser/svgPath.js' ],

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')
  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    '--all-locales true:\n\tto build HTML for all locales in strings/\n' +
    '--locales $project:\n\tuse locales inferred from another project\'s strings/ directory\n' +
    '--locale fr:\n\tto build just the French locale\n' +
    '[no options]:\n\tto build just the English locale',
    [ 'lint-all', 'build-no-lint' ] );

  grunt.registerTask( 'build-no-lint',
    'identical to "build", but does not run "lint-all"',
    [ 'clean', 'generate-license-text', 'before-requirejs-build', 'requirejs:build', 'after-requirejs-build' ] );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.log.writeln( 'Cleaning build directory' );
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'generate-license-text',
    'Generates the license text that will be written to the HTML file',
    function() {
      assert( pkg.preload, 'preload required in package.json' );
      generateLicenseText( grunt, pkg.preload );
    } );

  grunt.registerTask( 'before-requirejs-build',
    '(internal use only) Do things before the requirejs:build task',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.preload, 'preload missing from package.json' );
      beforeRequirejsBuild( grunt, pkg.name, pkg.version, pkg.phetLibs, pkg.preload, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, false );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Create a sim based on the simula-rasa template.  Example usage: grunt create-sim --name=cannon-blaster --author="Jane Smith (Smith Inc.)"',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'overwrite' ) );
    } );

  grunt.registerTask( 'generate-published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'generate-unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'bump-version',
    'This task updates the last value in the version by one. For example from 0.0.0-dev.12 to 0.0.0-dev.13.' +
    'This updates the package.json and js/version.js files, and commits + pushes to git.' +
    'BEWARE: Do not run this task unless your git is clean, otherwise it will commit other work on your repo as well.',
    function() {
      bumpVersion( grunt, pkg.version );
    } );

  grunt.registerTask( 'pull-all',
    'Pull all repo above this directory',
    function() {
      pullAll( grunt, child_process, assert, pkg.name );  //TODO this looks wrong, why passing in child_process and assert?
    } );

  grunt.registerTask( 'string-report',
    'After doing a build, reports on which strings are missing for each locale that was built.',
    function() {
      stringReport( grunt, pkg.name, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'clone-dependencies',
    'Clones all dependencies of a project, as listed in package.json phetLibs entry',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      cloneDependencies( grunt, pkg.name, pkg.phetLibs );
    } );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<|endoftext|>


<commit_before>/* The MIT License (MIT)
 *
 * Copyright (c) 2014 Xcraft <mathieu@schroetersa.ch>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

/**
 * Argument constructor.
 */
function Argument (handler, options, desc) {
  if (!options.hasOwnProperty ('scope')) {
    options.scope = 'global';
  }
  this._parent  = null;
  this._name    = null;
  this._options = options;
  this._desc    = desc;
  this._handler = handler;
  return this;
}

Argument.prototype._setName = function (name) {
  this._name = name;
};

Argument.prototype._setParent = function (parent) {
  this._parent = parent;
};

Argument.prototype.isBuiltIn = function () {
  return this._options.builtIn ? true : false;
};

Argument.prototype.isScoped = function (scope) {
  switch (this._options.scope) {
  case '*':
  case scope: {
    return true;
  }
  default: {
    return false;
  }
  }
};

Argument.prototype.isWizard = function () {
  return this._options.wizard ? true : false;
};

Argument.prototype.hasRequired = function () {
  if (this._options.hasOwnProperty ('params') &&
      this._options.params &&
      this._options.params.hasOwnProperty ('required') &&
      this._options.params.required.length) {
    return this._options.params.required;
  }
  return null;
};

Argument.prototype.scope = function () {
  return this._options.scope;
};

Argument.prototype.params = function () {
  if (this._options.hasOwnProperty ('params') && this._options.params) {
    var cmd = '';
    if (this._options.params.hasOwnProperty ('required')) {
      cmd += ' <' + this._options.params.required + '>';
    }
    if (this._options.params.hasOwnProperty ('optional')) {
      cmd += ' [' + this._options.params.optional + ']';
    }
    return cmd;
  }
  return '';
};

Argument.prototype.help = function (onlyDesc) {
  var help = '';
  if (!onlyDesc) {
    help += ' ' + this._name;
    help += this.params ();
    help += new Array (this._parent._helpWidth () - help.length).join (' ');
  }

  help += this._desc;
  return help;
};

Argument.prototype.call = function () {
  return this._handler.apply (this, arguments);
};

Argument.prototype.type = function () {
  return 'argument';
};

exports = module.exports = Argument;
<commit_msg>The help description can be un function too.<commit_after>/* The MIT License (MIT)
 *
 * Copyright (c) 2014 Xcraft <mathieu@schroetersa.ch>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

/**
 * Argument constructor.
 */
function Argument (handler, options, desc) {
  if (!options.hasOwnProperty ('scope')) {
    options.scope = 'global';
  }
  this._parent  = null;
  this._name    = null;
  this._options = options;
  this._desc    = desc;
  this._handler = handler;
  return this;
}

Argument.prototype._setName = function (name) {
  this._name = name;
};

Argument.prototype._setParent = function (parent) {
  this._parent = parent;
};

Argument.prototype.isBuiltIn = function () {
  return this._options.builtIn ? true : false;
};

Argument.prototype.isScoped = function (scope) {
  switch (this._options.scope) {
  case '*':
  case scope: {
    return true;
  }
  default: {
    return false;
  }
  }
};

Argument.prototype.isWizard = function () {
  return this._options.wizard ? true : false;
};

Argument.prototype.hasRequired = function () {
  if (this._options.hasOwnProperty ('params') &&
      this._options.params &&
      this._options.params.hasOwnProperty ('required') &&
      this._options.params.required.length) {
    return this._options.params.required;
  }
  return null;
};

Argument.prototype.scope = function () {
  return this._options.scope;
};

Argument.prototype.params = function () {
  if (this._options.hasOwnProperty ('params') && this._options.params) {
    var cmd = '';
    if (this._options.params.hasOwnProperty ('required')) {
      cmd += ' <' + this._options.params.required + '>';
    }
    if (this._options.params.hasOwnProperty ('optional')) {
      cmd += ' [' + this._options.params.optional + ']';
    }
    return cmd;
  }
  return '';
};

Argument.prototype.help = function (onlyDesc) {
  var help = '';
  if (!onlyDesc) {
    help += ' ' + this._name;
    help += this.params ();
    help += new Array (this._parent._helpWidth () - help.length).join (' ');
  }

  help += typeof this._desc === 'function' ? this._desc () : this._desc;
  return help;
};

Argument.prototype.call = function () {
  return this._handler.apply (this, arguments);
};

Argument.prototype.type = function () {
  return 'argument';
};

exports = module.exports = Argument;
<|endoftext|>


<commit_before>function arraySum(array) {
  var sum = 0;
  Array.prototype.forEach.call(array, (elem) => {
    if (typeof elem == "number" && !Number.isNaN(elem) && Number.isFinite(elem)) {
      sum += elem;
    }
  });
  return sum;
}

module.exports = arraySum;
<commit_msg>use Array.reduce for brevity<commit_after>function arraySum(array) {
  var finalSum = 0;
  finalSum = array.reduce(function(sum, elem) {
    if (typeof elem == "number" && !Number.isNaN(elem) && Number.isFinite(elem)) {
      return sum + elem;
    }
    return sum;
  });
  return finalSum;
}

module.exports = arraySum;
<|endoftext|>


<commit_before>iD.modes.Browse = function() {
    var mode = {
        button: 'browse',
        id: 'browse',
        title: 'Move',
        description: 'Pan and zoom the map',
        key: 'b'
    };

    var behaviors;

    mode.enter = function() {
        var surface = mode.map.surface;

        behaviors = [
            iD.behavior.Hover(),
            iD.behavior.DragNode(mode),
            iD.behavior.DragMidpoint(mode)];

        behaviors.forEach(function(behavior) {
            behavior(surface);
        });

        surface.on('click.browse', function () {
            var datum = d3.select(d3.event.target).datum();
            if (datum instanceof iD.Entity) {
                mode.controller.enter(iD.modes.Select(datum));
            }
        });
    };

    mode.exit = function() {
        var surface = mode.map.surface;

        behaviors.forEach(function(behavior) {
            behavior.off(surface);
        });

        surface.on('click.browse', null);
    };

    return mode;
};
<commit_msg>Mode name and shortcut key should match<commit_after>iD.modes.Browse = function() {
    var mode = {
        button: 'browse',
        id: 'browse',
        title: 'Browse',
        description: 'Pan and zoom the map',
        key: 'b'
    };

    var behaviors;

    mode.enter = function() {
        var surface = mode.map.surface;

        behaviors = [
            iD.behavior.Hover(),
            iD.behavior.DragNode(mode),
            iD.behavior.DragMidpoint(mode)];

        behaviors.forEach(function(behavior) {
            behavior(surface);
        });

        surface.on('click.browse', function () {
            var datum = d3.select(d3.event.target).datum();
            if (datum instanceof iD.Entity) {
                mode.controller.enter(iD.modes.Select(datum));
            }
        });
    };

    mode.exit = function() {
        var surface = mode.map.surface;

        behaviors.forEach(function(behavior) {
            behavior.off(surface);
        });

        surface.on('click.browse', null);
    };

    return mode;
};
<|endoftext|>


<commit_before>iD.ui.Inspector = function(context, entity) {
    var tagEditor,
        id = entity.id,
        newFeature = false;

    function changeTags(tags) {
        var entity = context.entity(id);
        if (entity && !_.isEqual(entity.tags, tags)) {
            context.perform(
                iD.actions.ChangeTags(entity.id, tags),
                t('operations.change_tags.annotation'));
        }
    }

    function browse() {
        context.enter(iD.modes.Browse(context));
    }

    function update() {
        var entity = context.entity(id);
        if (entity) {
            tagEditor.tags(entity.tags);
        }
    }

    function inspector(selection) {

        var reselect = selection.html();

        selection
            .html('')
            .style('display', 'block')
            .style('right', '-500px')
            .style('opacity', 1)
            .transition()
            .duration(reselect ? 0 : 200)
            .style('right', '0px');

        var panewrap = selection
            .append('div')
            .classed('panewrap', true);

        var presetLayer = panewrap
            .append('div')
            .classed('pane grid-pane', true);

        var tagLayer = panewrap
            .append('div')
            .classed('pane tag-pane', true);

        var presetGrid = iD.ui.PresetGrid(context, entity)
            .newFeature(newFeature)
            .on('close', browse)
            .on('choose', function(preset) {
                var right = panewrap.style('right').indexOf('%') > 0 ? '0%' : '0px';
                panewrap
                    .transition()
                    .style('right', right);

                tagLayer.call(tagEditor, preset);
            });

        tagEditor = iD.ui.TagEditor(context, entity)
            .tags(entity.tags)
            .on('changeTags', changeTags)
            .on('close', browse)
            .on('choose', function(preset) {
                var right = panewrap.style('right').indexOf('%') > 0 ?
                    '-100%' :
                    '-' + selection.style('width');
                panewrap
                    .transition()
                    .style('right', right);

                presetLayer.call(presetGrid, preset);
            });

        var initial = entity.isNew() && _.without(Object.keys(entity.tags), 'area').length === 0;

        if (initial) {
            panewrap.style('right', '-100%');
            presetLayer.call(presetGrid);
        } else {
            panewrap.style('right', '-0%');
            tagLayer.call(tagEditor);
        }

        if (d3.event) {
            // Pan the map if the clicked feature intersects with the position
            // of the inspector
            var inspectorSize = selection.size(),
                mapSize = context.map().size(),
                offset = 50,
                shiftLeft = d3.event.clientX - mapSize[0] + inspectorSize[0] + offset,
                center = (mapSize[0] / 2) + shiftLeft + offset;

            if (shiftLeft > 0 && inspectorSize[1] > d3.event.clientY) {
                context.map().centerEase(context.projection.invert([center, mapSize[1]/2]));
            }
        }

        context.history()
            .on('change.inspector', update);
    }

    inspector.close = function(selection) {
        selection.transition()
            .style('right', '-500px')
            .each('end', function() {
                d3.select(this)
                    .style('display', 'none')
                    .html('');
            });

        // Firefox incorrectly implements blur, so typeahead elements
        // are not correctly removed. Remove any stragglers manually.
        d3.selectAll('div.typeahead').remove();

        context.history()
            .on('change.inspector', null);
    };

    inspector.newFeature = function(_) {
        if (!arguments.length) return newFeature;
        newFeature = _;
        return inspector;
    };

    return inspector;
};
<commit_msg>Show preset grid for untagged features (fixes #1258)<commit_after>iD.ui.Inspector = function(context, entity) {
    var tagEditor,
        id = entity.id,
        newFeature = false;

    function changeTags(tags) {
        var entity = context.entity(id);
        if (entity && !_.isEqual(entity.tags, tags)) {
            context.perform(
                iD.actions.ChangeTags(entity.id, tags),
                t('operations.change_tags.annotation'));
        }
    }

    function browse() {
        context.enter(iD.modes.Browse(context));
    }

    function update() {
        var entity = context.entity(id);
        if (entity) {
            tagEditor.tags(entity.tags);
        }
    }

    function inspector(selection) {

        var reselect = selection.html();

        selection
            .html('')
            .style('display', 'block')
            .style('right', '-500px')
            .style('opacity', 1)
            .transition()
            .duration(reselect ? 0 : 200)
            .style('right', '0px');

        var panewrap = selection
            .append('div')
            .classed('panewrap', true);

        var presetLayer = panewrap
            .append('div')
            .classed('pane grid-pane', true);

        var tagLayer = panewrap
            .append('div')
            .classed('pane tag-pane', true);

        var presetGrid = iD.ui.PresetGrid(context, entity)
            .newFeature(newFeature)
            .on('close', browse)
            .on('choose', function(preset) {
                var right = panewrap.style('right').indexOf('%') > 0 ? '0%' : '0px';
                panewrap
                    .transition()
                    .style('right', right);

                tagLayer.call(tagEditor, preset);
            });

        tagEditor = iD.ui.TagEditor(context, entity)
            .tags(entity.tags)
            .on('changeTags', changeTags)
            .on('close', browse)
            .on('choose', function(preset) {
                var right = panewrap.style('right').indexOf('%') > 0 ?
                    '-100%' :
                    '-' + selection.style('width');
                panewrap
                    .transition()
                    .style('right', right);

                presetLayer.call(presetGrid, preset);
            });

        var tagless = _.without(Object.keys(entity.tags), 'area').length === 0;

        if (tagless) {
            panewrap.style('right', '-100%');
            presetLayer.call(presetGrid);
        } else {
            panewrap.style('right', '-0%');
            tagLayer.call(tagEditor);
        }

        if (d3.event) {
            // Pan the map if the clicked feature intersects with the position
            // of the inspector
            var inspectorSize = selection.size(),
                mapSize = context.map().size(),
                offset = 50,
                shiftLeft = d3.event.clientX - mapSize[0] + inspectorSize[0] + offset,
                center = (mapSize[0] / 2) + shiftLeft + offset;

            if (shiftLeft > 0 && inspectorSize[1] > d3.event.clientY) {
                context.map().centerEase(context.projection.invert([center, mapSize[1]/2]));
            }
        }

        context.history()
            .on('change.inspector', update);
    }

    inspector.close = function(selection) {
        selection.transition()
            .style('right', '-500px')
            .each('end', function() {
                d3.select(this)
                    .style('display', 'none')
                    .html('');
            });

        // Firefox incorrectly implements blur, so typeahead elements
        // are not correctly removed. Remove any stragglers manually.
        d3.selectAll('div.typeahead').remove();

        context.history()
            .on('change.inspector', null);
    };

    inspector.newFeature = function(_) {
        if (!arguments.length) return newFeature;
        newFeature = _;
        return inspector;
    };

    return inspector;
};
<|endoftext|>


<commit_before>/* MIT (c) Juho Vepsalainen */
(function ($) {
    function ellipsis($elem, options) {
        var a = check('li', $elem, options);
        var b = check('dt', $elem, options);

        if(!(a || b)) {
            checkText($elem, options);
        }
    }

    function check(name, $elem, options) {
        var $elems = $(name, $elem);

        if($elems.length > options.visible) {
            var $slice = $elem.children().slice($($elems[options.visible]).index()).hide();

            $more(name, options.more, function() {
                if(options.showCb) {
                    options.showCb($slice);
                }
                else {
                    $slice.show();
                }
            }).appendTo($elem);
        }

        if($elems.length) {
            return true;
        }
    }

    function checkText($elem, options) {
        var origText = $elem.text();
        var split = origText.split(' ');

        if(split.length > options.visible) {
            var text = split.slice(0, options.visible).join(' ');
            $elem.text(text);

            $more('span', options.more, function() {
                if(options.showCb) {
                    options.showCb($elem, origText);
                }
                else {
                    $elem.text(origText);
                }
            }).appendTo($elem);

            return true;
        }
    }

    function $more(name, text, showCb) {
        var $m = $('<' + name + ' class="more">' + text +'</' + name + '>');

        $m.bind('click', function() {
            showCb();
            $m.remove();
        });

        return $m;
    }

    var defaults = {
        visible: 3,
        more: '…',
        showCb: null
    };
    $.fn.ellipsis = function(options) {
        return this.each(function () {
            var $elem = $(this);
            var opts = $.extend({}, defaults, options);

            ellipsis($elem, opts);
        });
    };
    $.fn.ellipsis.options = defaults;
})(jQuery);
<commit_msg>Add atFront option<commit_after>/* MIT (c) Juho Vepsalainen */
(function ($) {
    function ellipsis($elem, options) {
        var a = check('li', $elem, options);
        var b = check('dt', $elem, options);

        if(!(a || b)) {
            checkText($elem, options);
        }
    }

    function check(name, $elem, options) {
        var $elems = $(name, $elem);

        if($elems.length > options.visible) {
            var $slice = $elem.children().slice($($elems[options.visible]).index()).hide();

            $more(name, options.more, function() {
                if(options.showCb) {
                    options.showCb($slice);
                }
                else {
                    $slice.show();
                }
            }).appendTo($elem);
        }

        if($elems.length) {
            return true;
        }
    }

    function checkText($elem, options) {
        var origText = $elem.text();
        var split = origText.split(' ');

        if(split.length > options.visible) {
            var text = split.slice(0, options.visible).join(' ');
            $elem.text(text);

            var $m = $more('span', options.more, function() {
                if(options.showCb) {
                    options.showCb($elem, origText);
                }
                else {
                    $elem.text(origText);
                }
            });

            if(options.atFront) $m.prependTo($elem);
            else $m.appendTo($elem);

            return true;
        }
    }

    function $more(name, text, showCb) {
        var $m = $('<' + name + ' class="more">' + text +'</' + name + '>');

        $m.bind('click', function() {
            showCb();
            $m.remove();
        });

        return $m;
    }

    var defaults = {
        visible: 3,
        more: '…',
        showCb: null,
        atFront: false
    };
    $.fn.ellipsis = function(options) {
        return this.each(function () {
            var $elem = $(this);
            var opts = $.extend({}, defaults, options);

            ellipsis($elem, opts);
        });
    };
    $.fn.ellipsis.options = defaults;
})(jQuery);
<|endoftext|>