<commit_before>// MAP DATA RENDER ////////////////////////////////////////////////
// class to handle rendering into leaflet the JSON data from the servers

window.Render = function() {
  // below this many portals displayed, we reorder the SVG at the end of the render pass to put portals above fields/links
  this.LOW_PORTAL_COUNT = 350;

}

// start a render pass. called as we start to make the batch of data requests to the servers
window.Render.prototype.startRenderPass = function() {
  this.isRendering = true;

  this.deletedGuid = {};  // object - represents the set of all deleted game entity GUIDs seen in a render pass

  this.seenPortalsGuid = {};
  this.seenLinksGuid = {};
  this.seenFieldsGuid = {};
}

window.Render.prototype.clearPortalsBelowLevel = function(level) {
  var count = 0;
  for (var guid in window.portals) {
    var p = portals[guid];
    if (parseInt(p.options.level) < level && guid !== selectedPortal) {
      this.deletePortalEntity(guid);
      count++;
    }
  }
  console.log('Render: deleted '+count+' portals by level');
}

window.Render.prototype.clearEntitiesOutsideBounds = function(bounds) {
  var pcount=0, lcount=0, fcount=0;

  for (var guid in window.portals) {
    var p = portals[guid];
    if (!bounds.contains (p.getLatLng()) && guid !== selectedPortal) {
      this.deletePortalEntity(guid);
      pcount++;
    }
  }

  for (var guid in window.links) {
    var l = links[guid];
    if (!bounds.intersects (l.getBounds())) {
      this.deleteLinkEntity(guid);
      lcount++;
    }
  }

  for (var guid in window.fields) {
    var f = fields[guid];
    if (!bounds.intersects (f.getBounds())) {
      this.deleteFieldEntity(guid);
      fcount++;
    }
  }
  console.log('Render: deleted '+pcount+' portals, '+lcount+' links, '+fcount+' fields by bounds check');
}

// process deleted entity list and entity data
window.Render.prototype.processTileData = function(tiledata) {
  this.processDeletedGameEntityGuids(tiledata.deletedGameEntityGuids||[]);
  this.processGameEntities(tiledata.gameEntities||[]);
}

window.Render.prototype.processDeletedGameEntityGuids = function(deleted) {
  for(var i in deleted) {
    var guid = deleted[i];

    if ( !(guid in this.deletedGuid) ) {
      this.deletedGuid[guid] = true;  // flag this guid as having being processed

      // the original code this is based on checked to see if the guid was for a field - and if so, removed it from the linkedFields of the relevant portals
      // given that the server will also return updated portals - and the linkedFields data is not actively used in IITC, this seems pointless.

      this.deleteEntity(guid);

    }
  }

}

window.Render.prototype.processGameEntities = function(entities) {
  for (var i in entities) {
    var ent = entities[i];

    // don't create entities in the 'deleted' list
    if (!(ent[0] in this.deletedGuid)) {
      this.createEntity(ent);
    }
  }

  // now reconstruct links 'optimised' out of the data from the portal link data

}

// end a render pass. does any cleaning up required, postponed processing of data, etc. called when the render
// is considered complete
window.Render.prototype.endRenderPass = function() {

  // check to see if there's eny entities we haven't seen. if so, delete them
  for (var guid in window.portals) {
    if (!(guid in this.seenPortalsGuid)) {
      this.deletePortalEntity(guid);
    }
  }
  for (var guid in window.links) {
    if (!(guid in this.seenLinksGuid)) {
      this.deleteLinkEntity(guid);
    }
  }
  for (var guid in window.fields) {
    if (!(guid in this.seenFieldsGuid)) {
      this.deleteFieldEntity(guid);
    }
  }

  // reorder portals to be after links/fields, but only if the number is low
  if (Object.keys(window.portals).length <= this.LOW_PORTAL_COUNT) {
    for (var i in window.portalsLayers) {
      var layer = window.portalsLayers[i];
      if (window.map.hasLayer(layer)) {
        layer.eachLayer (function(p) {
          p.bringToFront();
        });
      }
    }

  }

  this.isRendering = false;
}

window.Render.prototype.deleteEntity = function(guid) {
  this.deletePortalEntity(guid);
  this.deleteLinkEntity(guid);
  this.deleteFieldEntity(guid);
}

window.Render.prototype.deletePortalEntity = function(guid) {
  if (guid in window.portals) {
    var p = window.portals[guid];
    for(var i in portalsLayers) {
      portalsLayers[i].removeLayer(p);
    }
    delete window.portals[guid];
  }
}

window.Render.prototype.deleteLinkEntity = function(guid) {
  if (guid in window.links) {
    var l = window.links[guid];
    linksLayer.removeLayer(l);
    delete window.links[guid];
  }
}

window.Render.prototype.deleteFieldEntity = function(guid) {
  if (guid in window.fields) {
    var f = window.fields[guid];
    fieldsLayer.removeLayer(f);
    delete window.fields[guid];
  }
}

window.Render.prototype.createEntity = function(ent) {

  // ent[0] == guid
  // ent[1] == mtime
  // ent[2] == data

  // logic on detecting entity type based on the stock site javascript.
  if ("portalV2" in ent[2]) {
    this.createPortalEntity(ent);
  } else if ("capturedRegion" in ent[2]) {
    this.createFieldEntity(ent);
  } else if ("edge" in ent[2]) {
    this.createLinkEntity(ent);
  } else {
    console.warn("Unknown entity found: "+JSON.stringify(ent));
  }

}

window.Render.prototype.createPortalEntity = function(ent) {
  this.seenPortalsGuid[ent[0]] = true;  // flag we've seen it

  // check if entity already exists
  if (ent[0] in window.portals) {
    // yes. now check to see if the entity data we have is newer than that in place
    var p = window.portals[ent[0]];

    if (p.options.timestamp >= ent[1]) return; // this data is identical or older - abort processing

    // the data we have is newer. many data changes require re-rendering of the portal
    // (e.g. level changed, so size is different, or stats changed so highlighter is different)
    // so to keep things simple we'll always re-create the entity in this case

    this.deletePortalEntity(ent[0]);
  }

  var portalLevel = getPortalLevel(ent[2]);
  var team = getTeam(ent[2]);

  var latlng = L.latLng(ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6);

//TODO: move marker creation, style setting, etc into a separate class
//(as it's called from elsewhere - e.g. selecting/deselecting portals)

//ALSO: change API for highlighters - make them return the updated style rather than directly calling setStyle on the portal marker
//(can this be done in a backwardly-compatable way??)

  var dataOptions = {
    level: portalLevel,
    team: team,
    ent: ent,  // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .details instead
    guid: ent[0],
    timestamp: ent[1],
    details: ent[2]
  };

  var marker = createMarker(latlng, dataOptions);

  marker.on('click', function() { window.renderPortalDetails(ent[0]); });
  marker.on('dblclick', function() { window.renderPortalDetails(ent[0]); window.map.setView(latlng, 17); });

  window.runHooks('portalAdded', {portal: marker});

  window.portals[ent[0]] = marker;

  // check for URL links to portal, and select it if this is the one
  if (urlPortalLL && urlPortalLL[0] == marker.getLatLng().lat && urlPortalLL[1] == marker.getLatLng().lng) {
    // URL-passed portal found via pll parameter - set the guid-based parameter
    console.log('urlPortalLL '+urlPortalLL[0]+','+urlPortalLL[1]+' matches portal GUID '+ent[0]);

    urlPortal = ent[0];
    urlPortalLL = undefined;  // clear the URL parameter so it's not matched again
  }
  if (urlPortal == ent[0]) {
    // URL-passed portal found via guid parameter - set it as the selected portal
    console.log('urlPortal GUID '+urlPortal+' found - selecting...');
    selectedPortal = ent[0];
    urlPortal = undefined;  // clear the URL parameter so it's not matched again
  }

  // (re-)select the portal, to refresh the sidebar on any changes
  if (ent[0] == selectedPortal) {
    console.log('portal guid '+ent[0]+' is the selected portal - re-rendering portal details');
    renderPortalDetails (selectedPortal);
  }

  //TODO? postpone adding to the map layer
  portalsLayers[parseInt(portalLevel)].addLayer(marker);

}

window.Render.prototype.createFieldEntity = function(ent) {
  this.seenFieldsGuid[ent[0]] = true;  // flag we've seen it

  // check if entity already exists
  if(ent[0] in window.fields) {
    // yes. in theory, we should never get updated data for an existing field. they're created, and they're destroyed - never changed
    // but theory and practice may not be the same thing...
    var f = window.fields[ent[0]];

    if (f.options.timestamp >= ent[1]) return; // this data is identical (or order) than that rendered - abort processing

    // the data we have is newer - two options
    // 1. just update the data, assume the field render appearance is unmodified
    // 2. delete the entity, then re-create with the new data
    this.deleteFieldEntity(ent[0]); // option 2, for now
  }

  var team = getTeam(ent[2]);
  var reg = ent[2].capturedRegion;
  var latlngs = [
    L.latLng(reg.vertexA.location.latE6/1E6, reg.vertexA.location.lngE6/1E6),
    L.latLng(reg.vertexB.location.latE6/1E6, reg.vertexB.location.lngE6/1E6),
    L.latLng(reg.vertexC.location.latE6/1E6, reg.vertexC.location.lngE6/1E6)
  ];

  var poly = L.geodesicPolygon(latlngs, {
    fillColor: COLORS[team],
    fillOpacity: 0.25,
    stroke: false,
    clickable: false,
    guid: ent[0],
    timestamp: ent[1],
    details: ent[2],
    // LEGACY FIELDS: these duplicate data available via .details, as IITC previously stored it in data and vertices
    data: ent[2],
    vertices: ent[2].capturedRegion
  });

  window.fields[ent[0]] = poly;

  // TODO? postpone adding to the layer??
  fieldsLayer.addLayer(poly);
}

window.Render.prototype.createLinkEntity = function(ent) {
  this.seenLinksGuid[ent[0]] = true;  // flag we've seen it

  // check if entity already exists
  if (ent[0] in window.links) {
    // yes. now, as sometimes links are 'faked', they have incomplete data. if the data we have is better, replace the data
    var l = window.links[ent[0]];

    // the faked data will have older timestamps than real data (currently, faked set to zero)
    if (l.options.timestamp >= ent[1]) return; // this data is older or identical to the rendered data - abort processing

    // the data is newer/better - two options
    // 1. just update the data. assume the link render appearance is unmodified
    // 2. delete the entity, then re-create it with the new data
    this.deleteLinkEntity(ent[0]); // option 2 - for now
  }

  var team = getTeam(ent[2]);
  var edge = ent[2].edge;
  var latlngs = [
    L.latLng(edge.originPortalLocation.latE6/1E6, edge.originPortalLocation.lngE6/1E6),
    L.latLng(edge.destinationPortalLocation.latE6/1E6, edge.destinationPortalLocation.lngE6/1E6)
  ];
  var poly = L.geodesicPolyline(latlngs, {
    color: COLORS[team],
    opacity: 1,
    weight: 2,
    clickable: false,
    guid: ent[0],
    timestamp: ent[1],
    details: ent[2],
    // LEGACY FIELDS: these duplicate data available via .details, as IITC previously stored it in data and vertices
    data: ent[2]
  });

  window.links[ent[0]] = poly;

  // TODO? postpone adding to the layer??
  linksLayer.addLayer(poly);
}
<commit_msg>attempt to re-create missing links from the portal data may not work, as this code was work-in-progress when niantic broke the json protocol<commit_after>// MAP DATA RENDER ////////////////////////////////////////////////
// class to handle rendering into leaflet the JSON data from the servers

window.Render = function() {
  // below this many portals displayed, we reorder the SVG at the end of the render pass to put portals above fields/links
  this.LOW_PORTAL_COUNT = 350;

}

// start a render pass. called as we start to make the batch of data requests to the servers
window.Render.prototype.startRenderPass = function() {
  this.isRendering = true;

  this.deletedGuid = {};  // object - represents the set of all deleted game entity GUIDs seen in a render pass

  this.seenPortalsGuid = {};
  this.seenLinksGuid = {};
  this.seenFieldsGuid = {};
}

window.Render.prototype.clearPortalsBelowLevel = function(level) {
  var count = 0;
  for (var guid in window.portals) {
    var p = portals[guid];
    if (parseInt(p.options.level) < level && guid !== selectedPortal) {
      this.deletePortalEntity(guid);
      count++;
    }
  }
  console.log('Render: deleted '+count+' portals by level');
}

window.Render.prototype.clearEntitiesOutsideBounds = function(bounds) {
  var pcount=0, lcount=0, fcount=0;

  for (var guid in window.portals) {
    var p = portals[guid];
    if (!bounds.contains (p.getLatLng()) && guid !== selectedPortal) {
      this.deletePortalEntity(guid);
      pcount++;
    }
  }

  for (var guid in window.links) {
    var l = links[guid];
    if (!bounds.intersects (l.getBounds())) {
      this.deleteLinkEntity(guid);
      lcount++;
    }
  }

  for (var guid in window.fields) {
    var f = fields[guid];
    if (!bounds.intersects (f.getBounds())) {
      this.deleteFieldEntity(guid);
      fcount++;
    }
  }
  console.log('Render: deleted '+pcount+' portals, '+lcount+' links, '+fcount+' fields by bounds check');
}

// process deleted entity list and entity data
window.Render.prototype.processTileData = function(tiledata) {
  this.processDeletedGameEntityGuids(tiledata.deletedGameEntityGuids||[]);
  this.processGameEntities(tiledata.gameEntities||[]);
}

window.Render.prototype.processDeletedGameEntityGuids = function(deleted) {
  for(var i in deleted) {
    var guid = deleted[i];

    if ( !(guid in this.deletedGuid) ) {
      this.deletedGuid[guid] = true;  // flag this guid as having being processed

      // the original code this is based on checked to see if the guid was for a field - and if so, removed it from the linkedFields of the relevant portals
      // given that the server will also return updated portals - and the linkedFields data is not actively used in IITC, this seems pointless.

      this.deleteEntity(guid);

    }
  }

}

window.Render.prototype.processGameEntities = function(entities) {
  var portalGuids = [];

  for (var i in entities) {
    var ent = entities[i];

    // don't create entities in the 'deleted' list
    if (!(ent[0] in this.deletedGuid)) {
      this.createEntity(ent);
      if ('portalV2' in ent[2]) portalGuids.push(ent[0]);
    }
  }

  // now reconstruct links 'optimised' out of the data from the portal link data
  this.createLinksFromPortalData(portalGuids);
}

// end a render pass. does any cleaning up required, postponed processing of data, etc. called when the render
// is considered complete
window.Render.prototype.endRenderPass = function() {

  // check to see if there's eny entities we haven't seen. if so, delete them
  for (var guid in window.portals) {
    if (!(guid in this.seenPortalsGuid)) {
      this.deletePortalEntity(guid);
    }
  }
  for (var guid in window.links) {
    if (!(guid in this.seenLinksGuid)) {
      this.deleteLinkEntity(guid);
    }
  }
  for (var guid in window.fields) {
    if (!(guid in this.seenFieldsGuid)) {
      this.deleteFieldEntity(guid);
    }
  }

  // reorder portals to be after links/fields, but only if the number is low
  if (Object.keys(window.portals).length <= this.LOW_PORTAL_COUNT) {
    for (var i in window.portalsLayers) {
      var layer = window.portalsLayers[i];
      if (window.map.hasLayer(layer)) {
        layer.eachLayer (function(p) {
          p.bringToFront();
        });
      }
    }

  }

  this.isRendering = false;
}

window.Render.prototype.deleteEntity = function(guid) {
  this.deletePortalEntity(guid);
  this.deleteLinkEntity(guid);
  this.deleteFieldEntity(guid);
}

window.Render.prototype.deletePortalEntity = function(guid) {
  if (guid in window.portals) {
    var p = window.portals[guid];
    for(var i in portalsLayers) {
      portalsLayers[i].removeLayer(p);
    }
    delete window.portals[guid];
  }
}

window.Render.prototype.deleteLinkEntity = function(guid) {
  if (guid in window.links) {
    var l = window.links[guid];
    linksLayer.removeLayer(l);
    delete window.links[guid];
  }
}

window.Render.prototype.deleteFieldEntity = function(guid) {
  if (guid in window.fields) {
    var f = window.fields[guid];
    fieldsLayer.removeLayer(f);
    delete window.fields[guid];
  }
}

window.Render.prototype.createEntity = function(ent) {

  // ent[0] == guid
  // ent[1] == mtime
  // ent[2] == data

  // logic on detecting entity type based on the stock site javascript.
  if ("portalV2" in ent[2]) {
    this.createPortalEntity(ent);
  } else if ("capturedRegion" in ent[2]) {
    this.createFieldEntity(ent);
  } else if ("edge" in ent[2]) {
    this.createLinkEntity(ent);
  } else {
    console.warn("Unknown entity found: "+JSON.stringify(ent));
  }

}

window.Render.prototype.createPortalEntity = function(ent) {
  this.seenPortalsGuid[ent[0]] = true;  // flag we've seen it

  // check if entity already exists
  if (ent[0] in window.portals) {
    // yes. now check to see if the entity data we have is newer than that in place
    var p = window.portals[ent[0]];

    if (p.options.timestamp >= ent[1]) return; // this data is identical or older - abort processing

    // the data we have is newer. many data changes require re-rendering of the portal
    // (e.g. level changed, so size is different, or stats changed so highlighter is different)
    // so to keep things simple we'll always re-create the entity in this case

    this.deletePortalEntity(ent[0]);
  }

  var portalLevel = getPortalLevel(ent[2]);
  var team = getTeam(ent[2]);

  var latlng = L.latLng(ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6);

//TODO: move marker creation, style setting, etc into a separate class
//(as it's called from elsewhere - e.g. selecting/deselecting portals)

//ALSO: change API for highlighters - make them return the updated style rather than directly calling setStyle on the portal marker
//(can this be done in a backwardly-compatable way??)

  var dataOptions = {
    level: portalLevel,
    team: team,
    ent: ent,  // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .details instead
    guid: ent[0],
    timestamp: ent[1],
    details: ent[2]
  };

  var marker = createMarker(latlng, dataOptions);

  marker.on('click', function() { window.renderPortalDetails(ent[0]); });
  marker.on('dblclick', function() { window.renderPortalDetails(ent[0]); window.map.setView(latlng, 17); });

  window.runHooks('portalAdded', {portal: marker});

  window.portals[ent[0]] = marker;

  // check for URL links to portal, and select it if this is the one
  if (urlPortalLL && urlPortalLL[0] == marker.getLatLng().lat && urlPortalLL[1] == marker.getLatLng().lng) {
    // URL-passed portal found via pll parameter - set the guid-based parameter
    console.log('urlPortalLL '+urlPortalLL[0]+','+urlPortalLL[1]+' matches portal GUID '+ent[0]);

    urlPortal = ent[0];
    urlPortalLL = undefined;  // clear the URL parameter so it's not matched again
  }
  if (urlPortal == ent[0]) {
    // URL-passed portal found via guid parameter - set it as the selected portal
    console.log('urlPortal GUID '+urlPortal+' found - selecting...');
    selectedPortal = ent[0];
    urlPortal = undefined;  // clear the URL parameter so it's not matched again
  }

  // (re-)select the portal, to refresh the sidebar on any changes
  if (ent[0] == selectedPortal) {
    console.log('portal guid '+ent[0]+' is the selected portal - re-rendering portal details');
    renderPortalDetails (selectedPortal);
  }

  //TODO? postpone adding to the map layer
  portalsLayers[parseInt(portalLevel)].addLayer(marker);

}

window.Render.prototype.createFieldEntity = function(ent) {
  this.seenFieldsGuid[ent[0]] = true;  // flag we've seen it

  // check if entity already exists
  if(ent[0] in window.fields) {
    // yes. in theory, we should never get updated data for an existing field. they're created, and they're destroyed - never changed
    // but theory and practice may not be the same thing...
    var f = window.fields[ent[0]];

    if (f.options.timestamp >= ent[1]) return; // this data is identical (or order) than that rendered - abort processing

    // the data we have is newer - two options
    // 1. just update the data, assume the field render appearance is unmodified
    // 2. delete the entity, then re-create with the new data
    this.deleteFieldEntity(ent[0]); // option 2, for now
  }

  var team = getTeam(ent[2]);
  var reg = ent[2].capturedRegion;
  var latlngs = [
    L.latLng(reg.vertexA.location.latE6/1E6, reg.vertexA.location.lngE6/1E6),
    L.latLng(reg.vertexB.location.latE6/1E6, reg.vertexB.location.lngE6/1E6),
    L.latLng(reg.vertexC.location.latE6/1E6, reg.vertexC.location.lngE6/1E6)
  ];

  var poly = L.geodesicPolygon(latlngs, {
    fillColor: COLORS[team],
    fillOpacity: 0.25,
    stroke: false,
    clickable: false,
    guid: ent[0],
    timestamp: ent[1],
    details: ent[2],
    // LEGACY FIELDS: these duplicate data available via .details, as IITC previously stored it in data and vertices
    data: ent[2],
    vertices: ent[2].capturedRegion
  });

  window.fields[ent[0]] = poly;

  // TODO? postpone adding to the layer??
  fieldsLayer.addLayer(poly);
}

window.Render.prototype.createLinkEntity = function(ent) {
  this.seenLinksGuid[ent[0]] = true;  // flag we've seen it

  // check if entity already exists
  if (ent[0] in window.links) {
    // yes. now, as sometimes links are 'faked', they have incomplete data. if the data we have is better, replace the data
    var l = window.links[ent[0]];

    // the faked data will have older timestamps than real data (currently, faked set to zero)
    if (l.options.timestamp >= ent[1]) return; // this data is older or identical to the rendered data - abort processing

    // the data is newer/better - two options
    // 1. just update the data. assume the link render appearance is unmodified
    // 2. delete the entity, then re-create it with the new data
    this.deleteLinkEntity(ent[0]); // option 2 - for now
  }

  var team = getTeam(ent[2]);
  var edge = ent[2].edge;
  var latlngs = [
    L.latLng(edge.originPortalLocation.latE6/1E6, edge.originPortalLocation.lngE6/1E6),
    L.latLng(edge.destinationPortalLocation.latE6/1E6, edge.destinationPortalLocation.lngE6/1E6)
  ];
  var poly = L.geodesicPolyline(latlngs, {
    color: COLORS[team],
    opacity: 1,
    weight: 2,
    clickable: false,
    guid: ent[0],
    timestamp: ent[1],
    details: ent[2],
    // LEGACY FIELDS: these duplicate data available via .details, as IITC previously stored it in data and vertices
    data: ent[2]
  });

  window.links[ent[0]] = poly;

  // TODO? postpone adding to the layer??
  linksLayer.addLayer(poly);
}

window.Render.prototype.createLinksFromPortalData = function(portalGuids) {

  for (var portalGuidIndex in portalGuids) {
    var portalGuid = portalGuids[portalGuidIndex];
    var sourcePortal = portals[portalGuid];

    for (var sourceLinkIndex in sourcePortal.options.details.portalV2.linkedEdges||[]) {
      var sourcePortalLinkInfo = sourcePortal.options.details.portalV2.linkedEdges[sourceLinkIndex];

      // portals often contain details for edges that don't exist. so only consider faking an edge if this
      // is the origin portal, the link doesn't already exist...
      if (sourcePortalLinkInfo.isOrigin && !(sourcePortalLinkInfo.edgeGuid in links)) {

        // ... and the other porta has matching link information.
        if (portalGuids.indexOf(sourcePortalLinkInfo.otherPortalGuid) != -1 &&
            sourcePortalLinkInfo.otherPortalGuid in portals) {

          var targetPortal = portals[sourcePortalLinkInfo.otherPortalGuid];

          for (var targetLinkIndex in targetPortal.options.details.portalV2.linkedEdges||[]) {
            var targetPortalLinkInfo = targetPortal.options.details.portalV2.linkedEdges[targetLinkIndex];

            if (targetPortalLinkInfo.edgeGuid == sourcePortalLinkInfo.edgeGuid) {
              // yes - edge in both portals. create it

              var fakeEnt = [
                sourcePortalLinkInfo.edgeGuid,
                0,  // mtime for entity data - unknown when faking it, so zero will be the oldest possible
                {
                  controllingTeam: sourcePortal.options.details.controllingTeam,
                  edge: {
                    originPortalGuid: portalGuid,
                    originPortalLocation: sourcePortal.options.details.locationE6,
                    destinationPortalGuid: sourcePortalLinkInfo.otherPortalGuid,
                    destinationPortalLocation: targetPortal.options.details.locationE6
                  }
                }
              ];

              this.createLinkEntity(fakeEnt);

            }

          }

        }

      }

    }
  }
}
<|endoftext|>


<commit_before>import Vue from 'vue'
import messageAPI from '@/services/api/messages'
import conversationsAPI from '@/services/api/conversations'
import { createMetaModule, withMeta, metaStatusesWithId } from '@/store/helpers'

export function isUnread (message, conversation) {
  if (!message || !conversation) return
  if (!conversation.seenUpTo) return true
  return message.id > conversation.seenUpTo
}

function initialState () {
  return {
    entries: {},
    messages: {}, // { <conversation-id> : [<message>,...] }
    cursors: {}, // { <conversation-id> : [<cursor>, ...]}
    activeConversationId: null,
  }
}

export default {
  namespaced: true,
  modules: { meta: createMetaModule() },
  state: initialState(),
  getters: {
    enrichMessage: (state, getters, rootState, rootGetters) => message => {
      if (!message) return
      return {
        ...message,
        author: rootGetters['users/get'](message.author),
        isUnread: isUnread(message, getters.activeConversation),
      }
    },
    activeMessages: (state, getters) => {
      return (state.messages[state.activeConversationId] || []).map(getters.enrichMessage)
    },
    activeConversation: state => {
      if (!state.activeConversationId) return
      return state.entries[state.activeConversationId]
    },
    active: (state, getters) => {
      const id = state.activeConversationId
      if (!id) return

      const canLoadMore = typeof state.cursors[state.activeConversationId] === 'string'
      return {
        ...getters.activeConversation,
        messages: getters.activeMessages,
        canLoadMore,
        ...metaStatusesWithId(getters, ['send', 'fetch', 'fetchMore'], id),
      }
    },
  },
  actions: {
    ...withMeta({
      async send ({ commit, state, dispatch }, { id, messageData }) {
        const message = await messageAPI.create({
          content: messageData,
          conversation: id,
        })
        commit('updateMessages', {
          messages: [message],
          conversationId: message.conversation,
        })
      },
      async fetch ({ commit }, conversationId) {
        const data = await messageAPI.list(conversationId)
        commit('updateMessages', {
          messages: data.results,
          conversationId,
          cursor: data.next,
        })
      },

      async fetchMore ({ state, commit }, conversationId) {
        const currentCursor = state.cursors[conversationId]
        const data = await messageAPI.listMore(currentCursor)
        commit('updateMessages', {
          messages: data.results,
          conversationId,
          cursor: data.next,
        })
      },

      async fetchConversation ({ commit }, conversationId) {
        commit('setConversation', { conversation: await conversationsAPI.get(conversationId) })
      },

      async mark ({ dispatch }, { id, seenUpTo }) {
        await conversationsAPI.mark(id, { seenUpTo })
      },
    }),

    async sendInActiveConversation ({ state, dispatch }, messageData) {
      dispatch('send', { id: state.activeConversationId, messageData })
    },
    async markAllReadInActiveConversation ({ state, dispatch, getters }) {
      const newestMessage = getters.activeMessages[0]
      dispatch('mark', { id: state.activeConversationId, seenUpTo: newestMessage.id })
    },

    async fetchMoreForActiveConversation ({ state, dispatch }) {
      const conversationId = state.activeConversationId
      const currentCursor = state.cursors[conversationId]
      if (!currentCursor) {
        return
      }
      await dispatch('fetchMore', conversationId)
    },

    async setActive ({ commit, dispatch }, conversation) {
      commit('setActive', { conversationId: conversation.id })
      commit('setConversation', { conversation })
      await dispatch('fetch', conversation.id)
    },

    clearActive ({ commit }) {
      commit('clearActive')
    },

    receiveMessage ({ commit, state, getters }, message) {
      commit('updateMessages', {
        messages: [message],
        conversationId: message.conversation,
      })
    },

    updateConversation ({ state, commit }, conversation) {
      const existing = state.entries[conversation.id]
      if (existing && existing.updatedAt <= conversation.updatedAt) {
        commit('setConversation', { conversation })
      }
    },
  },
  mutations: {
    setActive (state, { conversationId }) {
      state.activeConversationId = conversationId
    },
    clearActive (state) {
      state.activeConversationId = null
    },
    updateMessages (state, { conversationId, messages, cursor }) {
      if (!state.messages[conversationId]) {
        Vue.set(state.messages, conversationId, messages)
        return
      }
      // simple insertion sort for new messages
      // assumes that existing messages are sorted AND incoming messages are sorted
      let i = 0
      for (let message of messages) {
        const stateMessages = state.messages[conversationId]
        while (i < stateMessages.length && stateMessages[i].createdAt > message.createdAt) i++
        if (i >= stateMessages.length || stateMessages[i].id !== message.id) {
          stateMessages.splice(i, 0, message)
        }
      }
      if (cursor !== undefined) {
        Vue.set(state.cursors, conversationId, cursor)
      }
    },
    setConversation (state, { conversation }) {
      Vue.set(state.entries, conversation.id, conversation)
    },
  },
}
<commit_msg>fix message loading<commit_after>import Vue from 'vue'
import messageAPI from '@/services/api/messages'
import conversationsAPI from '@/services/api/conversations'
import { createMetaModule, withMeta, metaStatusesWithId } from '@/store/helpers'

export function isUnread (message, conversation) {
  if (!message || !conversation) return
  if (!conversation.seenUpTo) return true
  return message.id > conversation.seenUpTo
}

function initialState () {
  return {
    entries: {},
    messages: {}, // { <conversation-id> : [<message>,...] }
    cursors: {}, // { <conversation-id> : [<cursor>, ...]}
    activeConversationId: null,
  }
}

export default {
  namespaced: true,
  modules: { meta: createMetaModule() },
  state: initialState(),
  getters: {
    enrichMessage: (state, getters, rootState, rootGetters) => message => {
      if (!message) return
      return {
        ...message,
        author: rootGetters['users/get'](message.author),
        isUnread: isUnread(message, getters.activeConversation),
      }
    },
    activeMessages: (state, getters) => {
      return (state.messages[state.activeConversationId] || []).map(getters.enrichMessage)
    },
    activeConversation: state => {
      if (!state.activeConversationId) return
      return state.entries[state.activeConversationId]
    },
    active: (state, getters) => {
      const id = state.activeConversationId
      if (!id) return

      const canLoadMore = typeof state.cursors[state.activeConversationId] === 'string'
      return {
        ...getters.activeConversation,
        messages: getters.activeMessages,
        canLoadMore,
        ...metaStatusesWithId(getters, ['send', 'fetch', 'fetchMore'], id),
      }
    },
  },
  actions: {
    ...withMeta({
      async send ({ commit, state, dispatch }, { id, messageData }) {
        const message = await messageAPI.create({
          content: messageData,
          conversation: id,
        })
        dispatch('receiveMessage', message)
      },
      async fetch ({ commit }, conversationId) {
        const data = await messageAPI.list(conversationId)
        commit('updateMessages', {
          messages: data.results,
          conversationId,
        })
        commit('setCursor', {
          conversationId,
          cursor: data.next,
        })
      },

      async fetchMore ({ state, commit }, conversationId) {
        const currentCursor = state.cursors[conversationId]
        const data = await messageAPI.listMore(currentCursor)
        commit('updateMessages', {
          messages: data.results,
          conversationId,
        })
        commit('setCursor', {
          conversationId,
          cursor: data.next,
        })
      },

      async fetchConversation ({ commit }, conversationId) {
        commit('setConversation', { conversation: await conversationsAPI.get(conversationId) })
      },

      async mark ({ dispatch }, { id, seenUpTo }) {
        await conversationsAPI.mark(id, { seenUpTo })
      },
    }),

    async sendInActiveConversation ({ state, dispatch }, messageData) {
      dispatch('send', { id: state.activeConversationId, messageData })
    },
    async markAllReadInActiveConversation ({ state, dispatch, getters }) {
      const newestMessage = getters.activeMessages[0]
      dispatch('mark', { id: state.activeConversationId, seenUpTo: newestMessage.id })
    },

    async fetchMoreForActiveConversation ({ state, dispatch }) {
      const conversationId = state.activeConversationId
      const currentCursor = state.cursors[conversationId]
      if (!currentCursor) {
        return
      }
      await dispatch('fetchMore', conversationId)
    },

    async setActive ({ commit, dispatch }, conversation) {
      commit('setActive', { conversationId: conversation.id })
      commit('setConversation', { conversation })
      await dispatch('fetch', conversation.id)
    },

    clearActive ({ commit }) {
      commit('clearActive')
    },

    receiveMessage ({ commit, state, getters }, message) {
      commit('updateMessages', {
        messages: [message],
        conversationId: message.conversation,
      })
    },

    updateConversation ({ state, commit }, conversation) {
      const existing = state.entries[conversation.id]
      if (existing && existing.updatedAt <= conversation.updatedAt) {
        commit('setConversation', { conversation })
      }
    },
  },
  mutations: {
    setActive (state, { conversationId }) {
      state.activeConversationId = conversationId
    },
    clearActive (state) {
      state.activeConversationId = null
    },
    updateMessages (state, { conversationId, messages }) {
      if (!state.messages[conversationId]) {
        Vue.set(state.messages, conversationId, messages)
        return
      }
      // simple insertion sort for new messages
      // assumes that existing messages are sorted AND incoming messages are sorted
      let i = 0
      for (let message of messages) {
        const stateMessages = state.messages[conversationId]
        while (i < stateMessages.length && stateMessages[i].createdAt > message.createdAt) i++
        if (i >= stateMessages.length || stateMessages[i].id !== message.id) {
          stateMessages.splice(i, 0, message)
        }
      }
    },
    setCursor (state, { conversationId, cursor }) {
      Vue.set(state.cursors, conversationId, cursor)
    },
    setConversation (state, { conversation }) {
      Vue.set(state.entries, conversation.id, conversation)
    },
  },
}
<|endoftext|>


<commit_before>/* jshint node: true, browser: true */

/**
 * Media Helper rewrite.
 *
 * This is a rewrite of media helper to enforce propogation of events from parent to children.
 * Ensure that teardown of one state is fired before setup of another.
 *
 * Usage: Register what events you would like to subscribe to.
 * These events will also get fired on all the children so if you
 * reregister on a perchild basis they will get fired twice on the
 * item and all of it's children.
 *
 * @codingstandard ftlabs-jsv2
 * @copyright The Financial Times Limited [All rights reserved]
 */

'use strict';

/**
 * Locals
 */

var mm = window.matchMedia;
var async = require('async');

/**
 * Exports
 */

module.exports = function(module) {
	var setImmediateId = 0;
	var setupCallbacks = [];
	var teardownCallbacks = [];

	module.on('initialize', function(options) {
		this._media = {};

		var media = this.media;
		var states = expandStates(this.states || {});
		var state;
		var name;

		for (var query in media) {
			if (media.hasOwnProperty(query)) {
				name = media[query];
				state = states[name] || {};
				this._media[name] = {
					query: query,
					setup: state.setup,
					teardown: state.teardown
				};
			}
		}
	});

	module.on('setup', function() {
		var state;
		var matcher;

		for (var name in this._media) {
			if (this._media.hasOwnProperty(name)) {
				state = this._media[name];
				matcher = state.matcher = mm(state.query);
				matcher.addListener(state.cb = callback(name));

				// Call setup on the current media state.
				if (matcher.matches) setup(name, function () {});
			}
		}
	});

	module.on('before teardown', function() {
		var state;
		var matcher;

		for (var name in this._media) {
			if (this._media.hasOwnProperty(name)) {
				state = this._media[name];
				matcher = state.matcher;
				matcher.removeListener(state.cb);
				if (matcher.matches) teardown(name, { fromDOM: false }, function () {});
			}
		}
	});

	function callback(name) {
		return function(data) {

			var state = module._media[name];

			// Either setup or teardown
			if (data.matches) {
				setupCallbacks.push(name);
			} else {
				teardownCallbacks.push(name);
			}

			if (!setImmediateId) {
				setImmediateId = setImmediate(function() {
					setImmediateId = 0;

					// Run teardown callbacks (if any)
					var tearDownCallbacksLength = teardownCallbacks.length;
					async.map(teardownCallbacks, teardown, function (err, results) {
						teardownCallbacks.slice(tearDownCallbacksLength);

						// Fire an event to allow third
						// parties to hook into this change.
						module.fireStatic('statechange');

						// Run Setup callbacks if any.
						var setupCallbacksLength = setupCallbacks.length;
						async.map(setupCallbacks, setup, function (err, results) {
							setupCallbacks.slice(setupCallbacksLength);
							module.fireStatic('statechangecomplete');
						});
					});
				});
			}

			// Update the matches state
			state.matches = data.matches;
		};
	}

	function setup(name, callback) {
		module.el.classList.add(name);
		run('setup', { on: name }, callback);
	}

	function teardown(name, options, callback) {
		var fromDOM = (!options || options.fromDOM !== false);
		if (fromDOM) {
			module.el.classList.remove(name);
		}
		if (typeof options === "function") callback = options;
		run('teardown', { on: name, fromDOM: fromDOM }, callback);
	}

	function run(method, options, callback) {
		var fn = module._media[options.on][method];
		if (fn) fn.call(module, options, callback);
		else callback();
	}
};

// This allows multiple breakpoints to trigger one function.
function expandStates(states) {
	var expanded = {};
	var contents;

	for (var names in states) {
		if (states.hasOwnProperty(names)) {
			contents = states[names];
			if (~names.indexOf(' ')) {
				names.split(' ').forEach(copy);
			} else {
				expanded[names] = contents;
			}
		}
	}

	function copy(name) {
		expanded[name] = contents;
	}

	return expanded;
}<commit_msg>Fix media changes not being cleared up correctly<commit_after>/* jshint node: true, browser: true */

/**
 * Media Helper rewrite.
 *
 * This is a rewrite of media helper to enforce propogation of events from parent to children.
 * Ensure that teardown of one state is fired before setup of another.
 *
 * Usage: Register what events you would like to subscribe to.
 * These events will also get fired on all the children so if you
 * reregister on a perchild basis they will get fired twice on the
 * item and all of it's children.
 *
 * @codingstandard ftlabs-jsv2
 * @copyright The Financial Times Limited [All rights reserved]
 */

'use strict';

/**
 * Locals
 */

var mm = window.matchMedia;
var async = require('async');

/**
 * Exports
 */

module.exports = function(module) {
	var setImmediateId = 0;
	var setupCallbacks = [];
	var teardownCallbacks = [];

	module.on('initialize', function(options) {
		this._media = {};

		var media = this.media;
		var states = expandStates(this.states || {});
		var state;
		var name;

		for (var query in media) {
			if (media.hasOwnProperty(query)) {
				name = media[query];
				state = states[name] || {};
				this._media[name] = {
					query: query,
					setup: state.setup,
					teardown: state.teardown
				};
			}
		}
	});

	module.on('setup', function() {
		var state;
		var matcher;

		for (var name in this._media) {
			if (this._media.hasOwnProperty(name)) {
				state = this._media[name];
				matcher = state.matcher = mm(state.query);
				matcher.addListener(state.cb = callback(name));

				// Call setup on the current media state.
				if (matcher.matches) setup(name, function () {});
			}
		}
	});

	module.on('before teardown', function() {
		var state;
		var matcher;

		for (var name in this._media) {
			if (this._media.hasOwnProperty(name)) {
				state = this._media[name];
				matcher = state.matcher;
				matcher.removeListener(state.cb);
				if (matcher.matches) teardown(name, { fromDOM: false }, function () {});
			}
		}
	});

	function callback(name) {
		return function(data) {

			var state = module._media[name];

			// NOTE:WP:Don't do anything if the
			// matches state has not changed.
			// This is to work around a strange bug,
			// whereby sometimes the callback fires
			// twice.
			//
			// I'm not sure if this a browser
			// bug, or a bug we have caused.
			if (state.matches === data.matches) return;

			// Either setup or teardown
			if (data.matches) {
				setupCallbacks.push(name);
			} else {
				teardownCallbacks.push(name);
			}

			if (!setImmediateId) {
				setImmediateId = setImmediate(function() {
					setImmediateId = 0;

					// Run teardown callbacks (if any)
					async.each(teardownCallbacks, teardown, function (err, results) {

						// Fire an event to allow third
						// parties to hook into this change.
						module.fireStatic('statechange');

						// Run Setup callbacks if any.
						async.each(setupCallbacks, setup, function (err, results) {
							module.fireStatic('statechangecomplete');
						});
					});
				});
			}

			// Update the matches state
			state.matches = data.matches;
		};
	}

	function setup(name, callback) {
		setupCallbacks.splice(setupCallbacks.indexOf(name), 1);
		module.el.classList.add(name);
		run('setup', { on: name }, callback);
	}

	function teardown(name, options, callback) {
		teardownCallbacks.splice(teardownCallbacks.indexOf(name), 1);
		var fromDOM = (!options || options.fromDOM !== false);
		if (fromDOM) {
			module.el.classList.remove(name);
		}
		if (typeof options === "function") callback = options;
		run('teardown', { on: name, fromDOM: fromDOM }, callback);
	}

	function run(method, options, callback) {
		var fn = module._media[options.on][method];
		if (fn) fn.call(module, options, callback);
		else callback();
	}
};

// This allows multiple breakpoints to trigger one function.
function expandStates(states) {
	var expanded = {};
	var contents;

	for (var names in states) {
		if (states.hasOwnProperty(names)) {
			contents = states[names];
			if (~names.indexOf(' ')) {
				names.split(' ').forEach(copy);
			} else {
				expanded[names] = contents;
			}
		}
	}

	function copy(name) {
		expanded[name] = contents;
	}

	return expanded;
}<|endoftext|>


<commit_before>//Dependencies
import React, { Component , PropTypes} from 'react';
import NotificationList from './notification-list';
import {capitalize} from 'lodash/string';
import {groupBy, map, pluck} from 'lodash/collection';
import {getConfig} from '../config';
import moment from 'moment';
const propTypes = {
    data: PropTypes.array,
    onSingleRead: PropTypes.func.isRequired,
    onGroupRead: PropTypes.func.isRequired
};
function _isYoungerThanA(periodName, date) {
    return moment(date).diff(moment().subtract(1, periodName)) > 0;
}
// function to group date
function groupDate({creationDate: date}) {
    if(_isYoungerThanA('day', date)) {
        return 'today';
    }
    if(_isYoungerThanA('week', date)) {
        return 'week';
    }
    if(_isYoungerThanA('month', date)) {
        return 'month';
    }
    return 'before';
}

function translate(key){
  const {i18n} = getConfig();
  return i18n[key];
}

function formatDate(date) {
    if(_isYoungerThanA('day', date)) {
        return momentDate.fromNow();
    }
    if(_isYoungerThanA('week', date)) {
        return 'Last week';
    }
    if(_isYoungerThanA('month', date)) {
        return 'Last month';
    }
    return 'Before';
}

function sortDateFn(a, b){
  return new Date(b.creationDate) - new Date(a.creationDate);
}

//Maybe i should add a Notification Group component by date which uses a notifciation list component
const NotificationGroup = ({data, onGroupRead, onSingleRead}) => {
    let idx = 0; // maybe use sth else as key
    return (

            {
                map(
                    groupBy(data, groupDate),
                    (group, groupTitle) => {
                        return (

{translate(groupTitle)} <button className='mdl-button mdl-js-button mdl-button--icon' onClick={()=> onGroupRead(pluck(group, 'uuid'))}>done_all</button>

                                <NotificationList data={group.sort(sortDateFn)} onRead={onSingleRead}/>

                        );
                    }
                )
            }

    );
};

NotificationGroup.propTypes = propTypes;

export default NotificationGroup;
<commit_msg>[group] Secure sort.<commit_after>//Dependencies
import React, { Component , PropTypes} from 'react';
import NotificationList from './notification-list';
import {capitalize} from 'lodash/string';
import {groupBy, reduce, pluck} from 'lodash/collection';
import {getConfig} from '../config';
import moment from 'moment';
const propTypes = {
    data: PropTypes.array,
    onSingleRead: PropTypes.func.isRequired,
    onGroupRead: PropTypes.func.isRequired
};
function _isYoungerThanA(periodName, date) {
    return moment(date).diff(moment().subtract(1, periodName)) > 0;
}
// function to group date
function groupDate({creationDate: date}) {
    if(_isYoungerThanA('day', date)) {
        return '0_today';
    }
    if(_isYoungerThanA('week', date)) {
        return '1_lastWeek';
    }
    if(_isYoungerThanA('month', date)) {
        return '2_lastMonth';
    }
    return '3_before';
}

function translate(key){
  const {i18n} = getConfig();
  return i18n[key];
}

function sortDateFn(a, b){
  return new Date(b.creationDate) - new Date(a.creationDate);
}

function sortNameFn(a,b){
  if (a.key  b.key)
    return 1;
  else
    return 0;
}

//Maybe i should add a Notification Group component by date which uses a notifciation list component
const NotificationGroup = ({data, onGroupRead, onSingleRead}) => {
    //    console.log('Grouped infos', groupBy(data, groupDate), reduce(groupBy(data, groupDate));
    const groupedAndSortedNotifs = reduce(
      groupBy(data, groupDate),
      (accumulator, value, key) => {
          accumulator.push({key, value});
          return accumulator;
      },
      []
    ).sort(sortNameFn);
    return (

            {
                groupedAndSortedNotifs.map(
                    ({value: group, key: groupTitle}) => {
                        return (

{translate(groupTitle)} <button className='mdl-button mdl-js-button mdl-button--icon' onClick={()=> onGroupRead(pluck(group, 'uuid'))}>done_all</button>

                                <NotificationList data={group.sort(sortDateFn)} onRead={onSingleRead}/>

                        );
                    }
                )
            }

    );
};

NotificationGroup.propTypes = propTypes;

export default NotificationGroup;
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import styled from 'styled-components'

import H2 from '../../elements/H2/H2'
import Button from '../../elements/Button/Button'
import CenteredDiv from '../../elements/CenteredDiv/CenteredDiv'

import streamingOnTwitch from './streaming-on-twitch.png'
import landingAQuad from './landing-a-quad-1.mp4'

const VideoWrapper = styled.div`
  video {
    right: 0;
    bottom: 0;
    min-width: 100%;
    min-height: 100%;
    width: 100%;
    height: auto;
    z-index: -100;
    max-width: 70ch;
  }
`

export default class Intro extends Component {
  render() {
    return (

{ Hi }

        <CenteredDiv>

            I'm Georgi,{' '}

              web developer

            . You can find me on{' '}
            twitter,{' '}
            github,{' '}
            twitch and{' '}
            linkedin.

I write code, solve problems and sometimes
            stream on twitch.

I also love flying FPV drones, 3D printing,
            contributing to open source, and working on side projects such as{' '}
            fpvtips.com.

          <VideoWrapper>
            <video autoPlay loop id="video-background" muted playsInline>
              <source src={landingAQuad} type="video/mp4" />
            </video>
          </VideoWrapper>

            I also maintain a blog where I write about life,
            goals, software and flying FPV drones.

        </CenteredDiv>

          <Button>Blog</Button>

    )
  }
}
<commit_msg>Fix broken twitch link<commit_after>import React, { Component } from 'react'
import styled from 'styled-components'

import H2 from '../../elements/H2/H2'
import Button from '../../elements/Button/Button'
import CenteredDiv from '../../elements/CenteredDiv/CenteredDiv'

import streamingOnTwitch from './streaming-on-twitch.png'
import landingAQuad from './landing-a-quad-1.mp4'

const VideoWrapper = styled.div`
  video {
    right: 0;
    bottom: 0;
    min-width: 100%;
    min-height: 100%;
    width: 100%;
    height: auto;
    z-index: -100;
    max-width: 70ch;
  }
`

export default class Intro extends Component {
  render() {
    return (

{ Hi }

        <CenteredDiv>

            I'm Georgi,{' '}

              web developer

            . You can find me on{' '}
            twitter,{' '}
            github,{' '}
            twitch and{' '}
            linkedin.

I write code, solve problems and sometimes
            stream on twitch.

I also love flying FPV drones, 3D printing,
            contributing to open source, and working on side projects such as{' '}
            fpvtips.com.

          <VideoWrapper>
            <video autoPlay loop id="video-background" muted playsInline>
              <source src={landingAQuad} type="video/mp4" />
            </video>
          </VideoWrapper>

            I also maintain a blog where I write about life,
            goals, software and flying FPV drones.

        </CenteredDiv>

          <Button>Blog</Button>

    )
  }
}
<|endoftext|>


<commit_before>export default ({type, value}) => {
    if (type !== 'word') {
        return false;
    }
    if (value[0] === '-') {
        if (/[0-9]/.test(value[1])) {
            return false;
        }
        if (value[1] === '-' && value[2] !== '-') {
            return false;
        }
        return true;
    }
    return !/[0-9]/.test(value[0]);
};

export const type = 'node';
<commit_msg>Update isCustomIdent validator.<commit_after>function isInvalid (value) {
    return /[^a-z0-9_-]/ig.test(value);
}

function isCodepoint (value) {
    return /\\u[a-f0-9]{1,6}/ig.test(value) || /\\[a-f0-9]{1,6}/ig.test(value);
}

function isValid (value) {
    return !isInvalid(value) || isCodepoint(value);
}

export default ({type, value}) => {
    if (type !== 'word') {
        return false;
    }
    if (value[0] === '-') {
        if (/[0-9]/.test(value[1])) {
            return false;
        }
        if (value[1] === '-' && value[2] !== '-') {
            return false;
        }
        return isValid(value);
    }
    return !/[0-9]/.test(value[0]) && isValid(value);
};

export const type = 'node';
<|endoftext|>


<commit_before>// for Google Publisher Tag ads
//
// required props:
//    - adId: eg, '/6355419/Travel/Europe/France/Paris'
//    - height: a css size in pixels
//    - width: ditto
//
// optional props:
//    - placement: only useful if the same `adId` is used more than once on a
//      page, use this to distinguish between them: call one "1" and the other
//      "2", or call them "top-left" and "bottom-right", etc
//    - refresh: number of seconds between refreshes
//    - tracing: verbose messages in console.log via Ember.Logger.log

import Ember from 'ember';
import {task, timeout} from 'ember-concurrency';

const {
    assert, get, set, getProperties, Component,
    String: { htmlSafe },
    Logger: { log }
} = Ember;

export default Component.extend({
    classNames: ['google-publisher-tag'],
    attributeBindings: ['style'],

    placement: 0,
    refresh: 0,
    refreshCount: 0,
    tracing: false,

    didReceiveAttrs() {
        this._super(...arguments);

        let {adId, width, height, placement} = getProperties(this, 'adId', 'width', 'height', 'placement');

        // width and height are also passed into .defineSlot(), which will
        // silently fail unless they are actually numbers
        assert(`google-publisher-tag "${adId} (${placement})": width and height must be numbers`, typeof width === 'number' && typeof height === 'number');

        let style = `width:${width}px; height:${height}px;`;
        set(this, 'style', htmlSafe(style));
    },
    didInsertElement() {
        this._super(...arguments);

        let googletag = window.googletag;
        let {adId, width, height, elementId} = getProperties(this, 'adId', 'width', 'height', 'elementId');
        if (adId && width && height && elementId) {
            googletag.cmd.push( () => {
                this.trace(`defining slot ${adId} for div ${elementId} at size ${width}x${height}`);
                let slot = googletag.defineSlot(adId, [width, height], elementId)
                    .addService(googletag.pubads());
                this.addTargeting(slot);
                googletag.enableServices();
                googletag.display(elementId);
                set(this, 'slot', slot);
                this.waitForRefresh();
            });
        }
    },

    addTargeting(slot) { // jshint ignore:line
        // override this in child components, if needed, but an example is:
        // slot.setTargeting('placement', get(this, 'placement'));
        // slot.setTargeting('refresh_count', get(this, 'refreshCount'));
        // slot.setTargeting('planet', 'Earth');
    },
    waitForRefresh() {
        let duration = get(this, 'refresh');
        if (duration > 0) {
            this.trace(`will refresh in ${duration} seconds`);
            get(this, 'doRefresh').perform(duration);
        }
    },
    trace() {
        if (get(this, 'tracing')) {
            let {adId, placement} = getProperties(this, 'adId', 'placement');
            log(`${adId} (${placement}): `, ...arguments);
        }
    },

    doRefresh: task(function * (duration) {
        yield timeout(duration * 1000);
        let googletag = window.googletag;
        googletag.cmd.push( () => {
            this.trace('refreshing now');
            this.incrementProperty('refreshCount');
            let slot = get(this, 'slot');
            this.addTargeting(slot);
            googletag.pubads().refresh([slot]);
            this.waitForRefresh();
        });
    }).restartable()
});
<commit_msg>use ad-queue for GPT component (breaks refresh, for now)<commit_after>// for Google Publisher Tag ads
//
// required props:
//    - adId: eg, '/6355419/Travel/Europe/France/Paris'
//    - height: a css size in pixels
//    - width: ditto
//
// optional props:
//    - placement: only useful if the same `adId` is used more than once on a
//      page, use this to distinguish between them: call one "1" and the other
//      "2", or call them "top-left" and "bottom-right", etc
//    - refresh: number of seconds between refreshes
//    - tracing: verbose messages in console.log via Ember.Logger.log

import Ember from 'ember';
import {task, timeout} from 'ember-concurrency';

const {
    assert, get, set, getProperties, Component,
    String: { htmlSafe },
    Logger: { log },
    inject: { service }
} = Ember;

export default Component.extend({
    classNames: ['google-publisher-tag'],
    attributeBindings: ['style'],

    placement: 0,
    refresh: 0,
    refreshCount: 0,
    tracing: false,
    adQueue: service(),

    didReceiveAttrs() {
        this._super(...arguments);

        let {adId, width, height, placement} = getProperties(this, 'adId', 'width', 'height', 'placement');

        // width and height are also passed into .defineSlot(), which will
        // silently fail unless they are actually numbers
        assert(`google-publisher-tag "${adId} (${placement})": width and height must be numbers`, typeof width === 'number' && typeof height === 'number');

        let style = `width:${width}px; height:${height}px;`;
        set(this, 'style', htmlSafe(style));
    },
    didInsertElement() {
        this._super(...arguments);

        get(this, 'adQueue').push(this);
    },

    addTargeting(slot) { // jshint ignore:line
        // override this in child components, if needed, but an example is:
        // slot.setTargeting('placement', get(this, 'placement'));
        // slot.setTargeting('refresh_count', get(this, 'refreshCount'));
        // slot.setTargeting('planet', 'Earth');
    },
    waitForRefresh() {
        let duration = get(this, 'refresh');
        if (duration > 0) {
            this.trace(`will refresh in ${duration} seconds`);
            get(this, 'doRefresh').perform(duration);
        }
    },
    trace() {
        if (get(this, 'tracing')) {
            let {adId, placement} = getProperties(this, 'adId', 'placement');
            log(`${adId} (${placement}): `, ...arguments);
        }
    },

    doRefresh: task(function * (duration) {
        this.incrementProperty('refreshCount');

        yield timeout(duration * 1000);

        let googletag = window.googletag;
        googletag.cmd.push( () => {
            let slot = get(this, 'slot');
            this.trace('refreshing now');
            this.addTargeting(slot);
            googletag.pubads().refresh([slot]);
            this.waitForRefresh();
        });
    }).restartable()
});
<|endoftext|>


<commit_before>const EventEmitter = require('events').EventEmitter;
const Collection = require('../util/Collection');

/**
 * Collects messages based on a specified filter, then emits them.
 * @extends {EventEmitter}
 */
class MessageCollector extends EventEmitter {
  /**
   * A function that takes a Message object and a MessageCollector and returns a boolean.
   * ```js
   * function(message, collector) {
   *  if (message.content.includes('discord')) {
   *    return true; // passed the filter test
   *  }
   *  return false; // failed the filter test
   * }
   * ```
   * @typedef {function} CollectorFilterFunction
   */

  /**
   * An object containing options used to configure a MessageCollector. All properties are optional.
   * @typedef {Object} CollectorOptions
   * @property {number} [time] Duration for the collector in milliseconds
   * @property {number} [max] Maximum number of messages to handle
   */

  /**
   * @param {Channel} channel The channel to collect messages in
   * @param {CollectorFilterFunction} filter The filter function
   * @param {CollectorOptions} [options] Options for the collector
   */
  constructor(channel, filter, options = {}) {
    super();

    /**
     * The channel this collector is operating on
     * @type {Channel}
     */
    this.channel = channel;

    /**
     * A function used to filter messages that the collector collects.
     * @type {CollectorFilterFunction}
     */
    this.filter = filter;

    /**
     * Options for the collecor.
     * @type {CollectorOptions}
     */
    this.options = options;

    /**
     * Whether this collector has stopped collecting Messages.
     * @type {boolean}
     */
    this.ended = false;

    /**
     * A collection of collected messages, mapped by message ID.
     * @type {Collection<string, Message>}
     */
    this.collected = new Collection();

    this.listener = message => this.verify(message);
    this.channel.client.on('message', this.listener);
    if (options.time) this.channel.client.setTimeout(() => this.stop('time'), options.time);
  }

  /**
   * Verifies a message against the filter and options
   * @private
   * @param {Message} message The message
   * @returns {boolean}
   */
  verify(message) {
    if (this.channel ? this.channel.id !== message.channel.id : false) return false;
    if (this.filter(message, this)) {
      this.collected.set(message.id, message);
      /**
       * Emitted whenever the Collector receives a Message that passes the filter test.
       * @param {Message} message The received message
       * @param {MessageCollector} collector The collector the message passed through
       * @event MessageCollector#message
       */
      this.emit('message', message, this);
      if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
      return true;
    }
    return false;
  }

  /**
   * Returns a promise that resolves when a valid message is sent. Rejects
   * with collected messages if the Collector ends before receiving a message.
   * @type {Promise<Message>}
   * @readonly
   */
  get next() {
    return new Promise((resolve, reject) => {
      if (this.ended) {
        reject(this.collected);
        return;
      }

      const cleanup = () => {
        this.removeListener('message', onMessage);
        this.removeListener('end', onEnd);
      };

      const onMessage = (...args) => {
        cleanup();
        resolve(...args);
      };

      const onEnd = (...args) => {
        cleanup();
        reject(...args);
      };

      this.once('message', onMessage);
      this.once('end', onEnd);
    });
  }

  /**
   * Stops the collector and emits `end`.
   * @param {string} [reason='user'] An optional reason for stopping the collector
   */
  stop(reason = 'user') {
    if (this.ended) return;
    this.ended = true;
    this.channel.client.removeListener('message', this.listener);
    /**
     * Emitted when the Collector stops collecting.
     * @param {Collection<string, Message>} collection A collection of messages collected
     * during the lifetime of the Collector, mapped by the ID of the Messages.
     * @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time
     * limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it
     * ended because it reached its message limit, it will be `limit`.
     * @event MessageCollector#end
     */
    this.emit('end', this.collected, reason);
  }
}

module.exports = MessageCollector;
<commit_msg>Add maxMatches CollectorOption<commit_after>const EventEmitter = require('events').EventEmitter;
const Collection = require('../util/Collection');

/**
 * Collects messages based on a specified filter, then emits them.
 * @extends {EventEmitter}
 */
class MessageCollector extends EventEmitter {
  /**
   * A function that takes a Message object and a MessageCollector and returns a boolean.
   * ```js
   * function(message, collector) {
   *  if (message.content.includes('discord')) {
   *    return true; // passed the filter test
   *  }
   *  return false; // failed the filter test
   * }
   * ```
   * @typedef {function} CollectorFilterFunction
   */

  /**
   * An object containing options used to configure a MessageCollector. All properties are optional.
   * @typedef {Object} CollectorOptions
   * @property {number} [time] Duration for the collector in milliseconds
   * @property {number} [max] Maximum number of messages to handle
   * @property {number} [maxMatches] Maximum number of successfully filtered messages to obtain
   */

  /**
   * @param {Channel} channel The channel to collect messages in
   * @param {CollectorFilterFunction} filter The filter function
   * @param {CollectorOptions} [options] Options for the collector
   */
  constructor(channel, filter, options = {}) {
    super();

    /**
     * The channel this collector is operating on
     * @type {Channel}
     */
    this.channel = channel;

    /**
     * A function used to filter messages that the collector collects.
     * @type {CollectorFilterFunction}
     */
    this.filter = filter;

    /**
     * Options for the collecor.
     * @type {CollectorOptions}
     */
    this.options = options;

    /**
     * Whether this collector has stopped collecting Messages.
     * @type {boolean}
     */
    this.ended = false;

    /**
     * A collection of collected messages, mapped by message ID.
     * @type {Collection<string, Message>}
     */
    this.collected = new Collection();

    this.listener = message => this.verify(message);
    this.channel.client.on('message', this.listener);
    if (options.time) this.channel.client.setTimeout(() => this.stop('time'), options.time);
  }

  /**
   * Verifies a message against the filter and options
   * @private
   * @param {Message} message The message
   * @returns {boolean}
   */
  verify(message) {
    if (this.channel ? this.channel.id !== message.channel.id : false) return false;
    if (this.filter(message, this)) {
      this.collected.set(message.id, message);
      /**
       * Emitted whenever the Collector receives a Message that passes the filter test.
       * @param {Message} message The received message
       * @param {MessageCollector} collector The collector the message passed through
       * @event MessageCollector#message
       */
      this.emit('message', message, this);
      if (this.collected.size >= this.options.maxMatches) this.stop('matchesLimit');
      else if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
      return true;
    }
    return false;
  }

  /**
   * Returns a promise that resolves when a valid message is sent. Rejects
   * with collected messages if the Collector ends before receiving a message.
   * @type {Promise<Message>}
   * @readonly
   */
  get next() {
    return new Promise((resolve, reject) => {
      if (this.ended) {
        reject(this.collected);
        return;
      }

      const cleanup = () => {
        this.removeListener('message', onMessage);
        this.removeListener('end', onEnd);
      };

      const onMessage = (...args) => {
        cleanup();
        resolve(...args);
      };

      const onEnd = (...args) => {
        cleanup();
        reject(...args);
      };

      this.once('message', onMessage);
      this.once('end', onEnd);
    });
  }

  /**
   * Stops the collector and emits `end`.
   * @param {string} [reason='user'] An optional reason for stopping the collector
   */
  stop(reason = 'user') {
    if (this.ended) return;
    this.ended = true;
    this.channel.client.removeListener('message', this.listener);
    /**
     * Emitted when the Collector stops collecting.
     * @param {Collection<string, Message>} collection A collection of messages collected
     * during the lifetime of the Collector, mapped by the ID of the Messages.
     * @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time
     * limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it
     * ended because it reached its message limit, it will be `limit`.
     * @event MessageCollector#end
     */
    this.emit('end', this.collected, reason);
  }
}

module.exports = MessageCollector;
<|endoftext|>


<commit_before>import React from 'react'
import { Heading } from 'rebass'
import styles from './style.css'
import { EVCalculator } from 'components/EVCalculator'

export const AppSkeleton = () => (

    <Heading level={1}>
      📸 Exposure Value
    </Heading>

    <EVCalculator />

)

export default AppSkeleton
<commit_msg>adjust header font size<commit_after>import React from 'react'
import { Heading } from 'rebass'
import styles from './style.css'
import { EVCalculator } from 'components/EVCalculator'

export const AppSkeleton = () => (

    <Heading level={1} style={{ fontSize: 18 }}>
      📸 Exposure Value
    </Heading>

    <EVCalculator />

)

export default AppSkeleton
<|endoftext|>


<commit_before>import Ember from "ember";

export default Ember.Checkbox;
<commit_msg>Fixed checkbox to correctly bind checked attribute with value<commit_after>import Ember from "ember";

export default Ember.Checkbox.extend({
  checked: Ember.computed.alias("value")
});
<|endoftext|>


<commit_before>"use strict";
/*jshint node: true*/
/*eslint-env node*/
/*global configuration*/

module.exports = {
    options: {
        configFile: ".eslintrc",
        rulePaths: [".eslintrules"]
    },
    target: configuration.jsLintedFiles
};
<commit_msg>Changed configuration<commit_after>"use strict";
/*jshint node: true*/
/*eslint-env node*/
/*global configuration*/

module.exports = {
    options: {
        rulePaths: [".eslintrules"]
    },
    target: configuration.jsLintedFiles
};
<|endoftext|>


<commit_before>import Component from '@ember/component';
import layout from '../../templates/components/nypr-ui/brand-header';
export default Component.extend({
  layout,
  tagName: '',
  init() {
    let menuItems = [
      {
        name: 'WNYC',
        url: 'https://www.wnyc.org',
        svg: 'wnyc-logo',
      },
      {
        name: 'Gothamist',
        url: 'https://www.gothamist.com',
        svg: 'gothamist-logo',
      },
      {
        name: 'WNYC Studios',
        url: 'https://www.wnycstudios.org',
        svg: 'wnyc-studios-logo',
      },
      {
        name: 'NJPR',
        url: 'http://www.njpublicradio.org',
        svg: 'njpr-logo',
      },
      {
        name: 'WQXR',
        url: 'https://www.wqxr.org',
        svg: 'wqxr-logo',
      },
      {
        name: 'New Sounds',
        url: 'https://www.newsounds.org',
        svg: 'new-sounds-logo',
      },
      {
        name: 'The Greene Space',
        url: 'https://www.thegreenespace.org',
        svg: 'greene-space-logo',
      },
    ];
    this.set('menuItems', menuItems);

    this._super(...arguments);
  },
});
<commit_msg>Add utm params to brand header links<commit_after>import Component from '@ember/component';
import layout from '../../templates/components/nypr-ui/brand-header';
import { getOwner } from '@ember/application';

export default Component.extend({
  layout,
  tagName: '',
  init() {
    let config = getOwner(this).resolveRegistration('config:environment');

    let menuItems = [
      {
        name: 'WNYC',
        url: `https://www.wnyc.org?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'wnyc-logo',
      },
      {
        name: 'Gothamist',
        url: `https://www.gothamist.com?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'gothamist-logo',
      },
      {
        name: 'WNYC Studios',
        url: `https://www.wnycstudios.org?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'wnyc-studios-logo',
      },
      {
        name: 'NJPR',
        url: `http://www.njpublicradio.org?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'njpr-logo',
      },
      {
        name: 'WQXR',
        url: `https://www.wqxr.org?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'wqxr-logo',
      },
      {
        name: 'New Sounds',
        url: `https://www.newsounds.org?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'new-sounds-logo',
      },
      {
        name: 'The Greene Space',
        url: `https://www.thegreenespace.org?utm_medium=partnersite&utm_campaign=brandheader&utm_source=${config.siteSlug}`,
        svg: 'greene-space-logo',
      },
    ];
    this.set('menuItems', menuItems);

    this._super(...arguments);
  }
});
<|endoftext|>


<commit_before>import React, { PropTypes, Component } from 'react';
import classnames from 'classnames';
import DialPad from '../DialPad';
import TextInput from '../TextInput';
import styles from './styles.scss';

function DialerPanel({
  callButtonDisabled,
  className,
  keepToNumber,
  onCall,
  toNumber,
}) {
  return (

        <TextInput
          className={styles.dialInput}
          value={toNumber}
          onChange={(event) => {
            keepToNumber(event.currentTarget.value);
          } }
          />

        <DialPad
          className={styles.dialPad}
          onButtonOutput={(key) => {
            keepToNumber(toNumber + key);
          } }
          />

            <svg className={styles.btnSvg} viewBox="0 0 500 500">
              <g
                className={classnames(
                  styles.btnSvgGroup,
                  callButtonDisabled && styles.disabled,
                )}
                onClick={onCall}
                >
                <circle
                  className={styles.circle}
                  cx="250"
                  cy="250"
                  r="200"
                  />
                <text
                  className={styles.btnValue}
                  x="250"
                  y="330"
                  dangerouslySetInnerHTML={{
                    __html: '®',
                  }}
                  />
              </g>
            </svg>

  );
}
DialerPanel.propTypes = {
  className: PropTypes.string,
  onCall: PropTypes.func.isRequired,
  callButtonDisabled: PropTypes.bool,
  toNumber: PropTypes.string,
  keepToNumber: PropTypes.func,
};

export default DialerPanel;
<commit_msg>[RCINT-3270]	Disable Call Button if user clicks too frequently or app somehow is throttling (#62)<commit_after>import React, { PropTypes, Component } from 'react';
import classnames from 'classnames';
import DialPad from '../DialPad';
import TextInput from '../TextInput';
import styles from './styles.scss';

function DialerPanel({
  callButtonDisabled,
  className,
  keepToNumber,
  onCall,
  toNumber,
}) {
  const onCallFunc = () => {
    !callButtonDisabled && onCall();
  };
  return (

        <TextInput
          className={styles.dialInput}
          value={toNumber}
          onChange={(event) => {
            keepToNumber(event.currentTarget.value);
          } }
          />

        <DialPad
          className={styles.dialPad}
          onButtonOutput={(key) => {
            keepToNumber(toNumber + key);
          } }
          />

            <svg className={styles.btnSvg} viewBox="0 0 500 500">
              <g
                className={classnames(
                  styles.btnSvgGroup,
                  callButtonDisabled && styles.disabled,
                )}
                onClick={onCallFunc}
                >
                <circle
                  className={styles.circle}
                  cx="250"
                  cy="250"
                  r="200"
                  />
                <text
                  className={styles.btnValue}
                  x="250"
                  y="330"
                  dangerouslySetInnerHTML={{
                    __html: '®',
                  }}
                  />
              </g>
            </svg>

  );
}
DialerPanel.propTypes = {
  className: PropTypes.string,
  onCall: PropTypes.func.isRequired,
  callButtonDisabled: PropTypes.bool,
  toNumber: PropTypes.string,
  keepToNumber: PropTypes.func,
};

export default DialerPanel;
<|endoftext|>


<commit_before>import { SECTION_EACH, SECTION_IF, SECTION_UNLESS, SECTION_WITH } from 'config/types';
import { isArray, isObject } from 'utils/is';
import Fragment from '../Fragment';
import RepeatedFragment from '../RepeatedFragment';
import Mustache from './shared/Mustache';

let emptyFragment;

function getType ( value, hasIndexRef ) {
	if ( hasIndexRef || isArray( value ) ) return SECTION_EACH;
	if ( isObject( value ) || typeof value === 'function' ) return SECTION_WITH;
	return SECTION_IF;
}

export default class Section extends Mustache {
	constructor ( options ) {
		super( options );

		this.sectionType = options.template.n || null;
		this.fragment = null;
	}

	bind () {
		super.bind();

		// if we managed to bind, we need to create children
		if ( this.model ) {
			const value = this.model.value;
			let fragment;

			if ( !this.sectionType ) this.sectionType = getType( value, this.template.i );

			if ( !value ) {
				if ( this.sectionType === SECTION_UNLESS ) {
					this.fragment = new Fragment({
						owner: this,
						template: this.template.f
					});

					this.fragment.bind();
				}

				// otherwise, create no children
				return;
			}

			if ( this.sectionType === SECTION_UNLESS ) return;

			if ( this.sectionType === SECTION_IF ) {
				fragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind();
			}

			else if ( this.sectionType === SECTION_WITH ) {
				fragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind( this.model );
			}

			else {
				fragment = new RepeatedFragment({
					owner: this,
					template: this.template.f,
					indexRef: this.template.i
				}).bind( this.model );
			}

			this.fragment = fragment;
		}
	}

	find ( selector ) {
		if ( this.fragment ) {
			return this.fragment.find( selector );
		}
	}

	findAll ( selector, queryResult ) {
		if ( this.fragment ) {
			this.fragment.findAll( selector, queryResult );
		}
	}

	findComponent ( name ) {
		if ( this.fragment ) {
			return this.fragment.findComponent( name );
		}
	}

	findAllComponents ( name, queryResult ) {
		if ( this.fragment ) {
			this.fragment.findAllComponents( name, queryResult );
		}
	}

	findNextNode () {
		return this.parentFragment.findNextNode( this );
	}

	firstNode () {
		return this.fragment.firstNode();
	}

	render () {
		this.rendered = true;

		return this.fragment ?
			this.fragment.render() :
			( emptyFragment || ( emptyFragment = document.createDocumentFragment() ) );
	}

	shuffle ( newIndices ) {
		if ( this.fragment && this.sectionType === SECTION_EACH ) {
			this.fragment.shuffle( newIndices );
		}
	}

	toString ( escape ) {
		return this.fragment ? this.fragment.toString( escape ) : '';
	}

	unrender ( shouldDestroy ) {
		if ( this.rendered && this.fragment ) this.fragment.unrender( shouldDestroy );
		this.rendered = false;
	}

	// TODO DRY this out - lot of repeated stuff between this and bind()
	update () {
		if ( !this.dirty ) return;
		if ( !this.model ) return; // TODO can this happen?

		const value = this.model.value;

		if ( this.sectionType === null ) this.sectionType = getType( value, this.template.i );

		let newFragment;

		if ( this.sectionType === SECTION_EACH ) {
			if ( this.fragment ) {
				this.fragment.update();
			} else {
				// TODO can this happen?
				newFragment = new RepeatedFragment({
					owner: this,
					template: this.template.f,
					indexRef: this.template.i
				}).bind( this.model );
			}
		}

		else if ( this.sectionType === SECTION_WITH ) {
			// TODO remove fragments if value is null or empty object
			if ( this.fragment ) {
				this.fragment.update();
			} else {
				this.fragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind( this.model );
			}
		}

		else {
			const shouldRender = this.sectionType === SECTION_UNLESS ? !value : !!value;

			if ( this.fragment ) {
				if ( shouldRender ) {
					this.fragment.update();
				} else {
					this.fragment.unbind().unrender( true );
					this.fragment = null;
				}
			} else if ( shouldRender ) {
				newFragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind( null );
			}
		}

		if ( newFragment ) {
			if ( this.rendered ) {
				const parentNode = this.parentFragment.findParentNode();
				const anchor = this.parentFragment.findNextNode( this );

				parentNode.insertBefore( newFragment.render(), anchor );
			}

			this.fragment = newFragment;
		}

		this.dirty = false;
	}
}
<commit_msg>dont assign section type prematurely. also, typo<commit_after>import { SECTION_EACH, SECTION_IF, SECTION_UNLESS, SECTION_WITH } from 'config/types';
import { isArray, isObject } from 'utils/is';
import Fragment from '../Fragment';
import RepeatedFragment from '../RepeatedFragment';
import Mustache from './shared/Mustache';

let emptyFragment;

function getType ( value, hasIndexRef ) {
	if ( hasIndexRef || isArray( value ) ) return SECTION_EACH;
	if ( isObject( value ) || typeof value === 'function' ) return SECTION_WITH;
	if ( value === undefined ) return null;
	return SECTION_IF;
}

export default class Section extends Mustache {
	constructor ( options ) {
		super( options );

		this.sectionType = options.template.n || null;
		this.fragment = null;
	}

	bind () {
		super.bind();

		// if we managed to bind, we need to create children
		if ( this.model ) {
			const value = this.model.value;
			let fragment;

			if ( !this.sectionType ) this.sectionType = getType( value, this.template.i );

			if ( !value ) {
				if ( this.sectionType === SECTION_UNLESS ) {
					this.fragment = new Fragment({
						owner: this,
						template: this.template.f
					});

					this.fragment.bind();
				}

				// otherwise, create no children
				return;
			}

			if ( this.sectionType === SECTION_UNLESS ) return;

			if ( this.sectionType === SECTION_IF ) {
				fragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind();
			}

			else if ( this.sectionType === SECTION_WITH ) {
				fragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind( this.model );
			}

			else {
				fragment = new RepeatedFragment({
					owner: this,
					template: this.template.f,
					indexRef: this.template.i
				}).bind( this.model );
			}

			this.fragment = fragment;
		}
	}

	find ( selector ) {
		if ( this.fragment ) {
			return this.fragment.find( selector );
		}
	}

	findAll ( selector, queryResult ) {
		if ( this.fragment ) {
			this.fragment.findAll( selector, queryResult );
		}
	}

	findComponent ( name ) {
		if ( this.fragment ) {
			return this.fragment.findComponent( name );
		}
	}

	findAllComponents ( name, queryResult ) {
		if ( this.fragment ) {
			this.fragment.findAllComponents( name, queryResult );
		}
	}

	findNextNode () {
		return this.parentFragment.findNextNode( this );
	}

	firstNode () {
		return this.fragment.firstNode();
	}

	render () {
		this.rendered = true;

		return this.fragment ?
			this.fragment.render() :
			( emptyFragment || ( emptyFragment = document.createDocumentFragment() ) );
	}

	shuffle ( newIndices ) {
		if ( this.fragment && this.sectionType === SECTION_EACH ) {
			this.fragment.shuffle( newIndices );
		}
	}

	toString ( escape ) {
		return this.fragment ? this.fragment.toString( escape ) : '';
	}

	unrender ( shouldDestroy ) {
		if ( this.rendered && this.fragment ) this.fragment.unrender( shouldDestroy );
		this.rendered = false;
	}

	// TODO DRY this out - lot of repeated stuff between this and bind()
	update () {
		if ( !this.dirty ) return;
		if ( !this.model ) return; // TODO can this happen?

		const value = this.model.value;

		if ( this.sectionType === null ) this.sectionType = getType( value, this.template.i );

		let newFragment;

		if ( this.sectionType === SECTION_EACH ) {
			if ( this.fragment ) {
				this.fragment.update();
			} else {
				// TODO can this happen?
				newFragment = new RepeatedFragment({
					owner: this,
					template: this.template.f,
					indexRef: this.template.i
				}).bind( this.model );
			}
		}

		else if ( this.sectionType === SECTION_WITH ) {
			// TODO remove fragments if value is null or empty object
			if ( this.fragment ) {
				this.fragment.update();
			} else {
				newFragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind( this.model );
			}
		}

		else {
			const shouldRender = this.sectionType === SECTION_UNLESS ? !value : !!value;

			if ( this.fragment ) {
				if ( shouldRender ) {
					this.fragment.update();
				} else {
					this.fragment.unbind().unrender( true );
					this.fragment = null;
				}
			} else if ( shouldRender ) {
				newFragment = new Fragment({
					owner: this,
					template: this.template.f
				}).bind( null );
			}
		}

		if ( newFragment ) {
			if ( this.rendered ) {
				const parentNode = this.parentFragment.findParentNode();
				const anchor = this.parentFragment.findNextNode( this );

				parentNode.insertBefore( newFragment.render(), anchor );
			}

			this.fragment = newFragment;
		}

		this.dirty = false;
	}
}
<|endoftext|>


<commit_before>$.jCanvas.defaults.fromCenter = false;
var $comicCanvas = $('#comic-canvas'); // <canvas id="comic-canvas" height="590" width="767">
var $savedComicCanvas = $('#saved-comic-canvas'); //<canvas id="saved-comic-canvas" width="767" height="129">
var hasBackground = false;
var selectedIndex;

// EVENT HANDLERS
$(document).on('dragstart', function(e) {
	var target = e.target;
	if (target.className != 'draggable') {return};
	e.dataTransfer = e.originalEvent.dataTransfer;
	e.dataTransfer.setData('text/plain', target.src);
	if (target.src.includes("backgrounds")) {return};
	var img = new Image();
	img.src = target.src;
	e.dataTransfer.setDragImage(img, img.width/2, img.height/2);
});

$comicCanvas.on('dragover', function(e) {
	e.preventDefault();
	e.dataTransfer = e.originalEvent.dataTransfer;
	e.dataTransfer.dropEffect = 'move';
});

$comicCanvas.on('drop', function(e) {
	e.preventDefault();
	e.dataTransfer = e.originalEvent.dataTransfer;
	var src = e.dataTransfer.getData('text');

	// Find image size
	var img = new Image();
	img.src = src;

	// Use image size to draw on canvas right where you drop it.
	var x = e.pageX - $comicCanvas.offset().left - img.width/2;
	var y = e.pageY - $comicCanvas.offset().top - img.height/2;
	addLayer(src, x, y);
});

$('#images').on('click', 'img', function() {
	if(hasBackground){
		var x = ($comicCanvas.getLayers().length - 1) * 100;
	}else{
		var x = $comicCanvas.getLayers().length * 100;
	}
	addLayer(this.src, x, 0);
});

$comicCanvas.on('click', function(e) {
	deSelectLayers();
});

$('#delete-button').on('click', function(e) {
	deleteSelectedLayer();
});

// HELPER FUNCTIONS

function addLayer(src, x, y) {
	var index, isBackground;
	if (src.includes('backgrounds')) {
		isBackground = true;
		// Backgrounds are centered in canvas...
		x = 0;
		y = 0;
		// And replace the previous background if already drawn
		if (hasBackground) {
			$comicCanvas.removeLayer(0);
			index = 0;
		}
		hasBackground = true;
	} else {
		isBackground = false;
		index = 1 + $comicCanvas.getLayers().length;
	};

	$comicCanvas.addLayer({
		type: 'image',
		source: src,
		x: x, y: y,
		draggable: false,
		index: index,
		sel: true,
		isBackground: isBackground,
		// When selected, change to .6 opaque
		click: function(layer){
			selectLayer(layer);
		}
	});

	// Get layer we just added and select it
	var length = $comicCanvas.getLayers().length;
	var layer = $comicCanvas.getLayers()[length - 1];
	selectLayer(layer);

	$comicCanvas.drawLayers();
};

function selectLayer(layer) {
	// Backgrounds aren't selectable
	if (layer.isBackground) {return};
	deSelectLayers(); // What is the usage of this line?
	// layer.sel = true; // No longer needed. After testing, safe to remove.
	layer.opacity = 0.6;
	// selectedIndex = layer.index; // No longer needed. After testing, remove this line.
	$comicCanvas.setLayer(layer, { draggable: true })
	// Uses built in JCanvas functionality to track the selected images
	.addLayerToGroup(layer, 'selected')
	// JCanvas alows chaining for funcitons with the same canvas root (same as $comicCanvas.drawLayers() when used after above lines)
	.drawLayers();
	console.log($comicCanvas.getLayerGroup('selected'));
}

function deSelectLayers() {
	var layers = $comicCanvas.getLayers();
	for (var i=0; i < layers.length; i++) {
		var layer = layers[i];
		layer.sel = false;
		layer.draggable = false;
		layer.opacity = 1;
		selectedIndex = null;
		$comicCanvas.drawLayers();
	}
}

function deleteSelectedLayer() {
	// var layer = $comicCanvas.getLayer(selectedIndex);
	// $comicCanvas.removeLayer(layer)
	// console.log($comicCanvas.getLayerGroup('selected'));
	$comicCanvas.removeLayerGroup('selected')
	.drawLayers();
}

<commit_msg>Refactor new images to stack in a group. Create group deletion feature.<commit_after>$.jCanvas.defaults.fromCenter = false;
var $comicCanvas = $('#comic-canvas'); // <canvas id="comic-canvas" height="590" width="767">
var $savedComicCanvas = $('#saved-comic-canvas'); //<canvas id="saved-comic-canvas" width="767" height="129">
var hasBackground = false;
// var selectedIndex;

// EVENT HANDLERS
$(document).on('dragstart', function(e) {
	var target = e.target;
	if (target.className != 'draggable') {return};
	e.dataTransfer = e.originalEvent.dataTransfer;
	e.dataTransfer.setData('text/plain', target.src);
	if (target.src.includes("backgrounds")) {return};
	var img = new Image();
	img.src = target.src;
	e.dataTransfer.setDragImage(img, img.width/2, img.height/2);
});

$comicCanvas.on('dragover', function(e) {
	e.preventDefault();
	e.dataTransfer = e.originalEvent.dataTransfer;
	e.dataTransfer.dropEffect = 'move';
});

$comicCanvas.on('drop', function(e) {
	e.preventDefault();
	e.dataTransfer = e.originalEvent.dataTransfer;
	var src = e.dataTransfer.getData('text');

	// Find image size
	var img = new Image();
	img.src = src;

	// Use image size to draw on canvas right where you drop it.
	var x = e.pageX - $comicCanvas.offset().left - img.width/2;
	var y = e.pageY - $comicCanvas.offset().top - img.height/2;
	addLayer(src, x, y);
});

$('#images').on('click', 'img', function() {
	if(hasBackground){
		var x = ($comicCanvas.getLayers().length - 1) * 100;
	}else{
		var x = $comicCanvas.getLayers().length * 100; //TODO Change this line to bound the x, y and keep the image on screen by default
	}
	addLayer(this.src, x, 0);
});

$comicCanvas.on('click', function(e) {
	deSelectLayers();
});

$('#delete-button').on('click', function(e) {
	deleteSelectedLayer();
});

// HELPER FUNCTIONS

function addLayer(src, x, y) {
	var index, isBackground;
	console.log(src.includes('backgrounds'));
	if (src.includes('backgrounds')) {
		isBackground = true;
		// Backgrounds are centered in canvas...
		x = 0;
		y = 0;
		// And replace the previous background if already drawn
		if (hasBackground) {
			$comicCanvas.removeLayer(0);
			index = 0;
		}
		hasBackground = true;
	} else {
		isBackground = false;
		index = 1 + $comicCanvas.getLayers().length;
	};

	$comicCanvas.addLayer({
		type: 'image',
		source: src,
		x: x, y: y,
		draggable: false,
		index: index,
		// sel: true,
		isBackground: isBackground,
		// When selected, change to .6 opaque
		click: function(layer){
			selectLayer(layer);
		}
	});

	// TODO: Refactor to prevent issues with unwanted selection -- see 'Issues' in Git Hub Repo.
	// Get layer we just added and select it
	var length = $comicCanvas.getLayers().length;
	var layer = $comicCanvas.getLayers()[length - 1];
	selectLayer(layer);

	$comicCanvas.drawLayers();
};

function selectLayer(layer) {
	// Backgrounds aren't selectable
	// console.log(layer.isBackground);
	if (layer.isBackground) {return};
	// deSelectLayers();
	// layer.sel = true; // No longer needed. After testing, safe to remove.
	layer.opacity = 0.6;
	// selectedIndex = layer.index; // No longer needed. After testing, remove this line.
	$comicCanvas.setLayer(layer, { draggable: true })
	// Uses built in JCanvas functionality to track the selected images
	.addLayerToGroup(layer, 'selected')
	// JCanvas alows chaining for funcitons with the same canvas root (same as $comicCanvas.drawLayers() when used after above lines)
	.drawLayers();
	// console.log($comicCanvas.getLayerGroup('selected'));
}

function deSelectLayers() {
	var layers = $comicCanvas.getLayers();
	$comicCanvas.setLayerGroup('selected', {
		opacity: 1,
		draggable: false
	})
	for (var i=0; i < layers.length; i++) {
		var layer = layers[i];
		// layer.sel = false;
		// layer.draggable = false;
		// layer.opacity = 1;
		// selectedIndex = null;
		$comicCanvas.removeLayerFromGroup(layer, 'selected')
		.drawLayers();
	}
}

function deleteSelectedLayer() {
	//Removes all layers from the 'selected' group and redraws layers
	$comicCanvas.removeLayerGroup('selected')
	.drawLayers();
}

<|endoftext|>


<commit_before>import Ember from 'ember';
import { Query } from 'ember-flexberry-data';

const { Builder, FilterOperator } = Query;

/*
 * Returns promise to load model specified by modelName and projectionName in the context of store and stagePk.
 */
function getPromise(store, stagePk, modelName, projectionName) {
  let stagePkPredicate = new Query.SimplePredicate('stage.id', FilterOperator.Eq, stagePk);
  let q = new Builder(store, modelName)
    .selectByProjection(projectionName)
    .where(stagePkPredicate)
    .build();

  return store.query(modelName, q);
}

/**
  Returns promise to preload stage metadata specified by stagePk in the context of store.
  @param {DS.Store} store
  @param {String} stagePk
  @return {Ember.RSVP.Promise} promise
 */
export default function fdPreloadStageMetadata(store, stagePk) {

  return new Ember.RSVP.Promise(function(resolve, reject) {
    let promises = [];

    const projectionName = 'FdPreloadMetadata';

    // stage promise
    let modelName = 'fd-dev-stage';
    let q = new Builder(store, modelName)
      .selectByProjection(projectionName)
      .byId(stagePk)
      .build();
    promises.push(store.query(modelName, q));

    // classes promise
    promises.push(getPromise(store, stagePk, 'fd-dev-class', projectionName));

    // associations promise
    promises.push(getPromise(store, stagePk, 'fd-dev-association', projectionName));

    // agregations promise
    promises.push(getPromise(store, stagePk, 'fd-dev-aggregation', projectionName));

    // inheritances promise
    promises.push(getPromise(store, stagePk, 'fd-dev-inheritance', projectionName));

    // resolve, reject
    Ember.RSVP.all(promises).then(resolve, reject);
  });
}
<commit_msg>Delete locks of current user<commit_after>import Ember from 'ember';
import { Query } from 'ember-flexberry-data';

const { Builder, FilterOperator } = Query;

/*
 * Returns promise to load model specified by modelName and projectionName in the context of store and stagePk.
 */
function getPromise(store, stagePk, modelName, projectionName) {
  let stagePkPredicate = new Query.SimplePredicate('stage.id', FilterOperator.Eq, stagePk);
  let q = new Builder(store, modelName)
    .selectByProjection(projectionName)
    .where(stagePkPredicate)
    .build();

  return store.query(modelName, q);
}

/*
 * Returns true if record is loaded to store and related to specified stage.
 */
function tryPeekRecord(store, modelName, recordId, stagePk) {
  let record = store.peekRecord(modelName, recordId);
  return record !== null && record.get('stage.id') === stagePk;
}

/**
  Returns promise to preload stage metadata specified by stagePk in the context of store.
  @param {DS.Store} store
  @param {String} stagePk
  @return {Ember.RSVP.Promise} promise
 */
export default function fdPreloadStageMetadata(store, stagePk) {

  return new Ember.RSVP.Promise(function(resolve, reject) {
    let promises = [];

    const projectionName = 'FdPreloadMetadata';

    // stage promise
    let modelName = 'fd-dev-stage';
    let q = new Builder(store, modelName)
      .selectByProjection(projectionName)
      .byId(stagePk)
      .build();
    promises.push(store.query(modelName, q));

    // classes promise
    promises.push(getPromise(store, stagePk, 'fd-dev-class', projectionName));

    // associations promise
    promises.push(getPromise(store, stagePk, 'fd-dev-association', projectionName));

    // agregations promise
    promises.push(getPromise(store, stagePk, 'fd-dev-aggregation', projectionName));

    // inheritances promise
    promises.push(getPromise(store, stagePk, 'fd-dev-inheritance', projectionName));

    // resolve, reject
    Ember.RSVP.all(promises).then(() => {
      let userService = Ember.getOwner(store).lookup('service:user');
      let userName = userService.getCurrentUserName();
      let lockModelName = 'new-platform-flexberry-services-lock';

      let lockPredicate = new Query.SimplePredicate('userName', FilterOperator.Eq, userName);
      let q = new Builder(store, lockModelName)
        .selectByProjection('LockL')
        .where(lockPredicate)
        .build();

      // delete locks for objects in current stage for current user.
      store.query(lockModelName, q).then((lockObjects) => {
        let deleteLocksPromises = [];
        lockObjects.forEach((item) => {
          let needToDeleteLock = tryPeekRecord(store, 'fd-dev-class', item.get('id'), stagePk) ||
            tryPeekRecord(store, 'fd-dev-association', item.get('id'), stagePk) ||
            tryPeekRecord(store, 'fd-dev-aggregation', item.get('id'), stagePk) ||
            tryPeekRecord(store, 'fd-dev-inheritance', item.get('id'), stagePk);

          if (needToDeleteLock) {
            deleteLocksPromises.push(item.destroyRecord());
          }
        });

        Ember.RSVP.all(deleteLocksPromises).then(resolve, reject);
      }, reject);
    }, reject);
  });
}
<|endoftext|>