<commit_before>define(['angular'], function(angular) {
  'use strict';

  angular.module('stream', [])
    .factory('stream', function($log){
      // TODO: channels should be API urls, which would let us simplify the logic
      // on the backend
      var Stream = function(url) {
        var self = this;

        this.url = url;
        this.es = null;

        this.channels = {
          // channelName: [scopeA, scopeB]
        };
        this.subscribers = {
          // eventName: [(scopeA, callbackA), (scopeB, callbackB)]
        };

        var arrPop = function(arr, el) {
          var idx = arr.indexOf(el);
          if (idx !== 1) {
            return arr.splice(idx, 1);
          }
          return;
        };

        return {
          addScopedSubscriber: function($scope, event, callback){
            // TODO: we should only parse the JSON once
            var listener = function(e) {
              var data = $.parseJSON(e.data);
              // $log.debug('[Stream] Got event for ' + event);
              callback(data);
            };

            if (self.subscribers[event] === undefined) {
              self.subscribers[event] = [[$scope, listener]];
            } else {
              self.subscribers[event].push([$scope, listener]);
            }

            $scope.$on('$destroy', function(){
              arrPop(self.subscribers[event], [$scope, listener]);
              if (self.subscribers[event].length === 0) {
                arrPop(self.subscribers, event);
              }

              if (self.es) {
                self.es.removeEventListener(event, listener, false);
              }
            });

            if (self.es) {
              self.es.addEventListener(event, listener, false);
            }
          },
          addScopedChannels: function($scope, channels) {
            var reconnect = this.reconnect;
            var changed = false;

            $.each(channels, function(_, channel){
              if (self.channels[channel] === undefined) {
                self.channels[channel] = [$scope];
                changed = true;
              } else {
                self.channels[channel].push($scope);
              }
            });

            $scope.$on('$destroy', function(){
              var changed = false;

              $.each(channels, function(_, channel){
                arrPop(self.channels[channel], $scope);
                if (self.channels[channel].length === 0) {
                  arrPop(self.channels, channel);
                  changed = true;
                }
              });

              if (!changed) {
                return;
              }

              reconnect();
            });

            if (changed) {
              reconnect();
            }
          },
          reconnect: function() {
            if (self.es) {
              $log.info('[Stream] Closing connection to ' + self.url);
              self.es.close();
            }
            $log.info('[Stream] Initiating connection to ' + self.url);

            var querystring = '?_=' + new Date().getTime();
            $.each(self.channels, function(channel, _){
              querystring += '&c=' + encodeURIComponent(channel);
            });

            self.es = new EventSource(url + querystring);
            self.es.onopen = function(e) {
              $log.info('[Stream] Connection opened to ' + self.url);
            };

            $.each(self.subscribers, function(event, item){
              self.es.addEventListener(event, item[1], false);
            });
          }
        };
      };

      return new Stream('/api/0/stream/');
    });
});
<commit_msg>Correct empty subscribers object<commit_after>define(['angular'], function(angular) {
  'use strict';

  angular.module('stream', [])
    .factory('stream', function($log){
      // TODO: channels should be API urls, which would let us simplify the logic
      // on the backend
      var Stream = function(url) {
        var self = this;

        this.url = url;
        this.es = null;

        this.channels = {
          // channelName: [scopeA, scopeB]
        };
        this.subscribers = {
          // eventName: [(scopeA, callbackA), (scopeB, callbackB)]
        };

        var arrPop = function(arr, el) {
          var idx = arr.indexOf(el);
          if (idx !== 1) {
            return arr.splice(idx, 1);
          }
          return;
        };

        return {
          addScopedSubscriber: function($scope, event, callback){
            // TODO: we should only parse the JSON once
            var listener = function(e) {
              var data = $.parseJSON(e.data);
              // $log.debug('[Stream] Got event for ' + event);
              callback(data);
            };

            if (self.subscribers[event] === undefined) {
              self.subscribers[event] = [[$scope, listener]];
            } else {
              self.subscribers[event].push([$scope, listener]);
            }

            $scope.$on('$destroy', function(){
              arrPop(self.subscribers[event], [$scope, listener]);
              if (self.subscribers[event].length === 0) {
                delete self.subscribers[event];
              }

              if (self.es) {
                self.es.removeEventListener(event, listener, false);
              }
            });

            if (self.es) {
              self.es.addEventListener(event, listener, false);
            }
          },
          addScopedChannels: function($scope, channels) {
            var reconnect = this.reconnect;
            var changed = false;

            $.each(channels, function(_, channel){
              if (self.channels[channel] === undefined) {
                self.channels[channel] = [$scope];
                changed = true;
              } else {
                self.channels[channel].push($scope);
              }
            });

            $scope.$on('$destroy', function(){
              var changed = false;

              $.each(channels, function(_, channel){
                arrPop(self.channels[channel], $scope);
                if (self.channels[channel].length === 0) {
                  arrPop(self.channels, channel);
                  changed = true;
                }
              });

              if (!changed) {
                return;
              }

              reconnect();
            });

            if (changed) {
              reconnect();
            }
          },
          reconnect: function() {
            if (self.es) {
              $log.info('[Stream] Closing connection to ' + self.url);
              self.es.close();
            }
            $log.info('[Stream] Initiating connection to ' + self.url);

            var querystring = '?_=' + new Date().getTime();
            $.each(self.channels, function(channel, _){
              querystring += '&c=' + encodeURIComponent(channel);
            });

            self.es = new EventSource(url + querystring);
            self.es.onopen = function(e) {
              $log.info('[Stream] Connection opened to ' + self.url);
            };

            $.each(self.subscribers, function(event, item){
              self.es.addEventListener(event, item[1], false);
            });
          }
        };
      };

      return new Stream('/api/0/stream/');
    });
});
<|endoftext|>


<commit_before>var db = require('../db');
var async = require('async');
var utility = require('../utility');
var generateJob = utility.generateJob;
var getData = utility.getData;
var queries = require('../queries');
var config = require('../config');
var retrieverArr = config.RETRIEVER_HOST.split(",");
var count = 0;
var args = process.argv.slice(2);
var start_id = Number(args[0]) || 0;
db.select('account_id').from('players').where('account_id', '>', start_id).orderByRaw(start_id ? 'account_id asc' : 'random()').asCallback(function(err, players) {
    if (err) {
        process.exit(1);
    }
    async.eachSeries(players, function(p, cb) {
        var job = {
            data: generateJob("mmr", {
                account_id: p.account_id,
                url: retrieverArr.map(function(r) {
                    return "http://" + r + "?key=" + config.RETRIEVER_SECRET + "&account_id=" + p.account_id;
                })[p.account_id % retrieverArr.length]
            })
        };
        getData({
            url: job.data.url,
            noRetry: true
        }, function(err, data) {
            if (err) {
                console.error(err);
            }
            count += 1;
            console.log(count, p.account_id);
            if (data.solo_competitive_rank || data.competitive_rank) {
                console.log(data);
                data.account_id = job.data.payload.account_id;
                data.match_id = job.data.payload.match_id;
                data.time = new Date();
                queries.insertPlayerRating(db, data, cb);
            }
            else {
                cb();
            }
        });
    }, function(err) {
        console.log(err);
        process.exit(Number(err));
    });
});
<commit_msg>Update allMmr.js<commit_after>var db = require('../db');
var async = require('async');
var utility = require('../utility');
var generateJob = utility.generateJob;
var getData = utility.getData;
var queries = require('../queries');
var config = require('../config');
var retrieverArr = config.RETRIEVER_HOST.split(",");
var count = 0;
var args = process.argv.slice(2);
var start_id = Number(args[0]) || 0;
db.select('account_id').from('players').where('account_id', '>', start_id).orderByRaw(start_id ? 'account_id asc' : 'random()').asCallback(function(err, players) {
    if (err) {
        process.exit(1);
    }
    async.eachSeries(players, function(p, cb) {
        var job = {
            data: generateJob("mmr", {
                account_id: p.account_id,
                url: retrieverArr.map(function(r) {
                    return "http://" + r + "?key=" + config.RETRIEVER_SECRET + "&account_id=" + p.account_id;
                })[p.account_id % retrieverArr.length]
            })
        };
        getData({
            url: job.data.url,
            noRetry: true
        }, function(err, data) {
            if (err) {
                console.error(err);
            }
            count += 1;
            console.log(count, p.account_id);
            if (data && (data.solo_competitive_rank || data.competitive_rank)) {
                console.log(data);
                data.account_id = job.data.payload.account_id;
                data.match_id = job.data.payload.match_id;
                data.time = new Date();
                queries.insertPlayerRating(db, data, cb);
            }
            else {
                cb();
            }
        });
    }, function(err) {
        console.log(err);
        process.exit(Number(err));
    });
});
<|endoftext|>


<commit_before>// load all files to run v86 in browser, uncompiled

(function()
{
    "use strict";

    var CORE_FILES =
        "const.js config.js log.js lib.js coverage.js cpu.js debug.js " +
        "io.js main.js ide.js pci.js floppy.js " +
        "memory.js dma.js pit.js vga.js ps2.js pic.js rtc.js uart.js acpi.js apic.js ioapic.js hpet.js " +
        "ne2k.js state.js virtio.js bus.js elf.js";

    var BROWSER_FILES = "main.js screen.js keyboard.js mouse.js serial.js lib.js network.js starter.js worker_bus.js print_stats.js";
    var LIB_FILES = "";

    // jor1k stuff
    LIB_FILES += " jor1k.js 9p.js filesystem.js marshall.js utf8.js";

    var BUILD_FILES = "capstone-x86.min.js libwabt.js";

    var to_load = [];

    load_scripts(CORE_FILES, "src/");
    load_scripts(BROWSER_FILES, "src/browser/");
    load_scripts(LIB_FILES, "lib/");
    load_scripts(BUILD_FILES, "build/");

    function load_scripts(resp, path)
    {
        var files = resp.split(" ");

        for(var i = 0; i < files.length; i++)
        {
            if(!files[i])
            {
                continue;
            }

            to_load.push(path + files[i]);
        }
    }

    load_next();

    function load_next()
    {
        var s = to_load.shift();

        if(!s)
        {
            return;
        }

        var script = document.createElement("script");
        script.src = s;
        script.onload = load_next;
        document.head.appendChild(script);
    }
})();
<commit_msg>loader.js: Try to carry on even if some scripts fail to load<commit_after>// load all files to run v86 in browser, uncompiled

(function()
{
    "use strict";

    var CORE_FILES =
        "const.js config.js log.js lib.js coverage.js cpu.js debug.js " +
        "io.js main.js ide.js pci.js floppy.js " +
        "memory.js dma.js pit.js vga.js ps2.js pic.js rtc.js uart.js acpi.js apic.js ioapic.js hpet.js " +
        "ne2k.js state.js virtio.js bus.js elf.js";

    var BROWSER_FILES = "main.js screen.js keyboard.js mouse.js serial.js lib.js network.js starter.js worker_bus.js print_stats.js";
    var LIB_FILES = "";

    // jor1k stuff
    LIB_FILES += " jor1k.js 9p.js filesystem.js marshall.js utf8.js";

    var BUILD_FILES = "capstone-x86.min.js libwabt.js";

    var to_load = [];

    load_scripts(CORE_FILES, "src/");
    load_scripts(BROWSER_FILES, "src/browser/");
    load_scripts(LIB_FILES, "lib/");
    load_scripts(BUILD_FILES, "build/");

    function load_scripts(resp, path)
    {
        var files = resp.split(" ");

        for(var i = 0; i < files.length; i++)
        {
            if(!files[i])
            {
                continue;
            }

            to_load.push(path + files[i]);
        }
    }

    load_next();

    function load_next()
    {
        let s = to_load.shift();

        if(!s)
        {
            return;
        }

        var script = document.createElement("script");
        script.src = s;
        script.onload = load_next;
        script.onerror = function() {
            dbg_log("Warning: trying to ignore script " + s + ", which failed to load");

            load_next();
        };
        document.head.appendChild(script);
    }
})();
<|endoftext|>


<commit_before>window.Helpers = {
  authorize: function() {
    var client_id = this.getQueryParam('app_client_id');

    // Use Exportify application client_id if none given
    if (client_id == '') {
      client_id = "9950ac751e34487dbbe027c4fd7f8e99"
    }

    window.location = "https://accounts.spotify.com/authorize" +
      "?client_id=" + client_id +
      "&redirect_uri=" + encodeURIComponent([location.protocol, '//', location.host, location.pathname].join('')) +
      "&scope=playlist-read-private%20playlist-read-collaborative" +
      "&response_type=token";
  },

  // http://stackoverflow.com/a/901144/4167042
  getQueryParam: function(name) {
    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
        results = regex.exec(location.search);
    return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
  },

  apiCall: function(url, access_token) {
    return $.ajax({
      url: url,
      headers: {
        'Authorization': 'Bearer ' + access_token
      }
    }).fail(function (jqXHR, textStatus) {
      if (jqXHR.status == 401) {
        // Return to home page after auth token expiry
        window.location = window.location.href.split('#')[0]
      } else if (jqXHR.status == 429) {
        // API Rate-limiting encountered
        window.location = window.location.href.split('#')[0] + '?rate_limit_message=true'
      } else {
        // Otherwise report the error so user can raise an issue
        alert(jqXHR.responseText);
      }
    })
  }
}

var PlaylistTable = React.createClass({
  getInitialState: function() {
    return {
      playlists: [],
      playlistCount: 0,
      nextURL: null,
      prevURL: null
    };
  },

  loadPlaylists: function(url) {
    var userId = '';

    window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) {
      userId = response.id;

      return window.Helpers.apiCall(
        typeof url !== 'undefined' ? url : "https://api.spotify.com/v1/users/" + userId + "/playlists",
        this.props.access_token
      )
    }.bind(this)).done(function(response) {
      if (this.isMounted()) {
        this.setState({
          playlists: response.items,
          playlistCount: response.total,
          nextURL: response.next,
          prevURL: response.previous
        });

        $('#playlists').fadeIn();
        $('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId)
      }
    }.bind(this))
  },

  exportPlaylists: function() {
    PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
  },

  componentDidMount: function() {
    this.loadPlaylists(this.props.url);
  },

  render: function() {
    if (this.state.playlists.length > 0) {
      return (

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

                Name
                Owner
                Tracks
                Public?
                Collaborative?
                <button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}> Export All</button>

              {this.state.playlists.map(function(playlist, i) {
                return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>;
              }.bind(this))}

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

      );
    } else {
      return

    }
  }
});

var PlaylistRow = React.createClass({
  exportPlaylist: function() {
    PlaylistExporter.export(this.props.access_token, this.props.playlist);
  },

  renderTickCross: function(condition) {
    if (condition) {
      return
    } else {
      return
    }
  },

  render: function() {
    playlist = this.props.playlist
    if(playlist.uri==null) return (

        {this.renderIcon(playlist)}
        {playlist.name}
        This playlist is not supported
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}

    );
    return (

        {playlist.name}
        {playlist.owner.display_name}
        {playlist.tracks.total}
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}
        <button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}> Export</button>

    );
  }
});

var Paginator = React.createClass({
  nextClick: function(e) {
    e.preventDefault()

    if (this.props.nextURL != null) {
      this.props.loadPlaylists(this.props.nextURL)
    }
  },

  prevClick: function(e) {
    e.preventDefault()

    if (this.props.prevURL != null) {
      this.props.loadPlaylists(this.props.prevURL)
    }
  },

  render: function() {
    if (this.props.nextURL != null || this.props.prevURL != null) {
      return (

                «

                »

      )
    } else {
      return

    }
  }
});

// Handles exporting all playlist data as a zip file
var PlaylistsExporter = {
  export: function(access_token, playlistCount) {
    var playlistFileNames = [];

    window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
      var requests = [];
      var limit = 20;

      for (var offset = 0; offset < playlistCount; offset = offset + limit) {
        var url = "https://api.spotify.com/v1/users/" + response.id + "/playlists";
        requests.push(
          window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token)
        )
      }

      $.when.apply($, requests).then(function() {
        var playlists = [];
        var playlistExports = [];

        // Handle either single or multiple responses
        if (typeof arguments[0].href == 'undefined') {
          $(arguments).each(function(i, response) { $.merge(playlists, response[0].items) })
        } else {
          playlists = arguments[0].items
        }

        $(playlists).each(function(i, playlist) {
          playlistFileNames.push(PlaylistExporter.fileName(playlist));
          playlistExports.push(PlaylistExporter.csvData(access_token, playlist));
        });

        return $.when.apply($, playlistExports);
      }).then(function() {
        var zip = new JSZip();
        var responses = [];

        $(arguments).each(function(i, response) {
          zip.file(playlistFileNames[i], response)
        });

        var content = zip.generate({ type: "blob" });
        saveAs(content, "spotify_playlists.zip");
      });
    });
  }
}

// Handles exporting a single playlist as a CSV file
var PlaylistExporter = {
  export: function(access_token, playlist) {
    this.csvData(access_token, playlist).then(function(data) {
      var blob = new Blob([ data ], { type: "text/csv;charset=utf-8" });
      saveAs(blob, this.fileName(playlist), true);
    }.bind(this))
  },

  csvData: function(access_token, playlist) {
    var requests = [];
    var limit = 100;

    for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) {
      requests.push(
        window.Helpers.apiCall(playlist.tracks.href + '?offset=' + offset + '&limit=' + limit, access_token)
      )
    }

    return $.when.apply($, requests).then(function() {
      var responses = [];

      // Handle either single or multiple responses
      if (typeof arguments[0] != 'undefined') {
        if (typeof arguments[0].href == 'undefined') {
          responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] });
        } else {
          responses = [arguments[0]];
        }
      }

      var tracks = responses.map(function(response) {
        return response.items.map(function(item) {
          return [
            item.track.uri,
            item.track.name,
            item.track.artists.map(function (artist) { return String(artist.name).replace(/,/g, "\\,"); }).join(', '),
            item.track.album.name,
            item.track.disc_number,
            item.track.track_number,
            item.track.duration_ms,
            item.added_by == null ? '' : item.added_by.uri,
            item.added_at
          ];
        });
      });

      // Flatten the array of pages
      tracks = $.map(tracks, function(n) { return n })

      tracks.unshift([
        "Spotify URI",
        "Track Name",
        "Artist Name",
        "Album Name",
        "Disc Number",
        "Track Number",
        "Track Duration (ms)",
        "Added By",
        "Added At"
      ]);

      csvContent = '';
      tracks.forEach(function (row, index){
        dataString = row.map(function (cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(",");
        csvContent += index < tracks.length ? dataString+ "\n" : dataString;
      });

      return csvContent;
    });
  },

  fileName: function(playlist) {
    return playlist.name.replace(/[\x00-\x1F\x7F/\\<>:;"|=,.?*[\] ]+/g, "_").toLowerCase() + ".csv";
  }
}

$(function() {
  var vars = window.location.hash.substring(1).split('&');
  var key = {};
  for (i=0; i<vars.length; i++) {
    var tmp = vars[i].split('=');
    key[tmp[0]] = tmp[1];
  }

  if (window.Helpers.getQueryParam('rate_limit_message') != '') {
    // Show rate limit message
    $('#rateLimitMessage').show();
  } else if (typeof key['access_token'] === 'undefined') {
    $('#loginButton').css('display', 'inline-block')
  } else {
    React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer);
  }
});
<commit_msg>Drop tautology and ensure POSIX compliance with trailing newline<commit_after>window.Helpers = {
  authorize: function() {
    var client_id = this.getQueryParam('app_client_id');

    // Use Exportify application client_id if none given
    if (client_id == '') {
      client_id = "9950ac751e34487dbbe027c4fd7f8e99"
    }

    window.location = "https://accounts.spotify.com/authorize" +
      "?client_id=" + client_id +
      "&redirect_uri=" + encodeURIComponent([location.protocol, '//', location.host, location.pathname].join('')) +
      "&scope=playlist-read-private%20playlist-read-collaborative" +
      "&response_type=token";
  },

  // http://stackoverflow.com/a/901144/4167042
  getQueryParam: function(name) {
    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
        results = regex.exec(location.search);
    return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
  },

  apiCall: function(url, access_token) {
    return $.ajax({
      url: url,
      headers: {
        'Authorization': 'Bearer ' + access_token
      }
    }).fail(function (jqXHR, textStatus) {
      if (jqXHR.status == 401) {
        // Return to home page after auth token expiry
        window.location = window.location.href.split('#')[0]
      } else if (jqXHR.status == 429) {
        // API Rate-limiting encountered
        window.location = window.location.href.split('#')[0] + '?rate_limit_message=true'
      } else {
        // Otherwise report the error so user can raise an issue
        alert(jqXHR.responseText);
      }
    })
  }
}

var PlaylistTable = React.createClass({
  getInitialState: function() {
    return {
      playlists: [],
      playlistCount: 0,
      nextURL: null,
      prevURL: null
    };
  },

  loadPlaylists: function(url) {
    var userId = '';

    window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) {
      userId = response.id;

      return window.Helpers.apiCall(
        typeof url !== 'undefined' ? url : "https://api.spotify.com/v1/users/" + userId + "/playlists",
        this.props.access_token
      )
    }.bind(this)).done(function(response) {
      if (this.isMounted()) {
        this.setState({
          playlists: response.items,
          playlistCount: response.total,
          nextURL: response.next,
          prevURL: response.previous
        });

        $('#playlists').fadeIn();
        $('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId)
      }
    }.bind(this))
  },

  exportPlaylists: function() {
    PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
  },

  componentDidMount: function() {
    this.loadPlaylists(this.props.url);
  },

  render: function() {
    if (this.state.playlists.length > 0) {
      return (

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

                Name
                Owner
                Tracks
                Public?
                Collaborative?
                <button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}> Export All</button>

              {this.state.playlists.map(function(playlist, i) {
                return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>;
              }.bind(this))}

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

      );
    } else {
      return

    }
  }
});

var PlaylistRow = React.createClass({
  exportPlaylist: function() {
    PlaylistExporter.export(this.props.access_token, this.props.playlist);
  },

  renderTickCross: function(condition) {
    if (condition) {
      return
    } else {
      return
    }
  },

  render: function() {
    playlist = this.props.playlist
    if(playlist.uri==null) return (

        {this.renderIcon(playlist)}
        {playlist.name}
        This playlist is not supported
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}

    );
    return (

        {playlist.name}
        {playlist.owner.display_name}
        {playlist.tracks.total}
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}
        <button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}> Export</button>

    );
  }
});

var Paginator = React.createClass({
  nextClick: function(e) {
    e.preventDefault()

    if (this.props.nextURL != null) {
      this.props.loadPlaylists(this.props.nextURL)
    }
  },

  prevClick: function(e) {
    e.preventDefault()

    if (this.props.prevURL != null) {
      this.props.loadPlaylists(this.props.prevURL)
    }
  },

  render: function() {
    if (this.props.nextURL != null || this.props.prevURL != null) {
      return (

                «

                »

      )
    } else {
      return

    }
  }
});

// Handles exporting all playlist data as a zip file
var PlaylistsExporter = {
  export: function(access_token, playlistCount) {
    var playlistFileNames = [];

    window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
      var requests = [];
      var limit = 20;

      for (var offset = 0; offset < playlistCount; offset = offset + limit) {
        var url = "https://api.spotify.com/v1/users/" + response.id + "/playlists";
        requests.push(
          window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token)
        )
      }

      $.when.apply($, requests).then(function() {
        var playlists = [];
        var playlistExports = [];

        // Handle either single or multiple responses
        if (typeof arguments[0].href == 'undefined') {
          $(arguments).each(function(i, response) { $.merge(playlists, response[0].items) })
        } else {
          playlists = arguments[0].items
        }

        $(playlists).each(function(i, playlist) {
          playlistFileNames.push(PlaylistExporter.fileName(playlist));
          playlistExports.push(PlaylistExporter.csvData(access_token, playlist));
        });

        return $.when.apply($, playlistExports);
      }).then(function() {
        var zip = new JSZip();
        var responses = [];

        $(arguments).each(function(i, response) {
          zip.file(playlistFileNames[i], response)
        });

        var content = zip.generate({ type: "blob" });
        saveAs(content, "spotify_playlists.zip");
      });
    });
  }
}

// Handles exporting a single playlist as a CSV file
var PlaylistExporter = {
  export: function(access_token, playlist) {
    this.csvData(access_token, playlist).then(function(data) {
      var blob = new Blob([ data ], { type: "text/csv;charset=utf-8" });
      saveAs(blob, this.fileName(playlist), true);
    }.bind(this))
  },

  csvData: function(access_token, playlist) {
    var requests = [];
    var limit = 100;

    for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) {
      requests.push(
        window.Helpers.apiCall(playlist.tracks.href + '?offset=' + offset + '&limit=' + limit, access_token)
      )
    }

    return $.when.apply($, requests).then(function() {
      var responses = [];

      // Handle either single or multiple responses
      if (typeof arguments[0] != 'undefined') {
        if (typeof arguments[0].href == 'undefined') {
          responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] });
        } else {
          responses = [arguments[0]];
        }
      }

      var tracks = responses.map(function(response) {
        return response.items.map(function(item) {
          return [
            item.track.uri,
            item.track.name,
            item.track.artists.map(function (artist) { return String(artist.name).replace(/,/g, "\\,"); }).join(', '),
            item.track.album.name,
            item.track.disc_number,
            item.track.track_number,
            item.track.duration_ms,
            item.added_by == null ? '' : item.added_by.uri,
            item.added_at
          ];
        });
      });

      // Flatten the array of pages
      tracks = $.map(tracks, function(n) { return n })

      tracks.unshift([
        "Spotify URI",
        "Track Name",
        "Artist Name",
        "Album Name",
        "Disc Number",
        "Track Number",
        "Track Duration (ms)",
        "Added By",
        "Added At"
      ]);

      csvContent = '';
      tracks.forEach(function(row, index){
        dataString = row.map(function (cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(",");
        csvContent += dataString + "\n";
      });

      return csvContent;
    });
  },

  fileName: function(playlist) {
    return playlist.name.replace(/[\x00-\x1F\x7F/\\<>:;"|=,.?*[\] ]+/g, "_").toLowerCase() + ".csv";
  }
}

$(function() {
  var vars = window.location.hash.substring(1).split('&');
  var key = {};
  for (i=0; i<vars.length; i++) {
    var tmp = vars[i].split('=');
    key[tmp[0]] = tmp[1];
  }

  if (window.Helpers.getQueryParam('rate_limit_message') != '') {
    // Show rate limit message
    $('#rateLimitMessage').show();
  } else if (typeof key['access_token'] === 'undefined') {
    $('#loginButton').css('display', 'inline-block')
  } else {
    React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer);
  }
});
<|endoftext|>


<commit_before>/**
 * OpenEyes
 *
 * (C) Moorfields Eye Hospital NHS Foundation Trust, 2008-2011
 * (C) OpenEyes Foundation, 2011-2013
 * This file is part of OpenEyes.
 * OpenEyes is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 * OpenEyes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with OpenEyes in a file titled COPYING. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package OpenEyes
 * @link http://www.openeyes.org.uk
 * @author OpenEyes <info@openeyes.org.uk>
 * @copyright Copyright (c) 2008-2011, Moorfields Eye Hospital NHS Foundation Trust
 * @copyright Copyright (c) 2011-2013, OpenEyes Foundation
 * @license http://www.gnu.org/licenses/gpl-3.0.html The GNU General Public License V3.0
 */
/**
 * Create a global getSVG method that takes an array of charts as an argument
 */
Highcharts.getSVG = function(charts) {
    var svgArr = [],
        top = 60,
        width = 0,
        header = '',
        hasDates = $('#from-date').val() || $('#to-date').val();

    $.each(charts, function(i, chart) {
        var currentWidth = chart.chartWidth;
        chart.setSize(700, chart.chartHeight);
        var svg = chart.getSVG();
        svg = svg.replace('<svg', '<g transform="translate(0,' + top + ')" ');
        svg = svg.replace('</svg>', '</g>');

        top += chart.chartHeight + 50;
        width = Math.max(width, chart.chartWidth);

        svgArr.push(svg);
        chart.setSize(currentWidth, chart.chartHeight);
    });
    width += 50;

    header = '<svg height="60" width="' + width + '" ><text y="20" x="' + (width / 2) + '" text-anchor="middle">'
        + $('.mdl-layout-title').text() + '</text>';

    if(hasDates){
        header += '<text y="40" x="' + (width / 2) + '" text-anchor="middle">';

        if($('#from-date').val()){
            header += 'From: ' +  $('#from-date').val() + ' ';
        } else {
            header += ' From: All time ';
        }

        if($('#to-date').val()){
            header += 'To: ' +  $('#to-date').val();
        } else {
            header += 'To: Present day';
        }

        header += '</text>';
    }
    header += '</svg>';

    return '<svg height="'+ top +'" width="' + width + '" version="1.1" xmlns="http://www.w3.org/2000/svg">' + header + svgArr.join('') + '</svg>';
};

/**
 * Create a global exportCharts method that takes an array of charts as an argument,
 * and exporting options as the second argument
 */
Highcharts.exportCharts = function(charts, options) {
    var form,
    svg = Highcharts.getSVG(charts);

    // merge the options
    options = Highcharts.merge(Highcharts.getOptions().exporting, options);

    // create the form
    form = Highcharts.createElement('form', {
        method: 'post',
        action: '/dashboard/printSvg'
    }, {
        display: 'none'
    }, document.body);

    // add the values
    Highcharts.each(['filename', 'type', 'width', 'svg'], function(name) {
        Highcharts.createElement('input', {
            type: 'hidden',
            name: name,
            value: {
                filename: options.filename || 'chart',
                type: options.type,
                width: options.width,
                svg: svg
            }[name]
        }, null, form);
    });

    var input = document.createElement("input");
    input.type = "hidden";
    input.name = "YII_CSRF_TOKEN";
    input.value = YII_CSRF_TOKEN;
    form.appendChild(input);
    //console.log(svg); return;
    // submit
    form.submit();

    // clean up
    form.parentNode.removeChild(form);
};

Highcharts.setOptions({
    lang: {
        noData: "No data found for this report"
    }
});

$(document).ready(function() {
    OpenEyes.Dash.init('#dash-grid');
    OpenEyes.Dash.addBespokeReport('/report/ajaxReport?report=PcrRisk');
    OpenEyes.Dash.addBespokeReport('/report/ajaxReport?report=CataractComplications');
    OpenEyes.Dash.addBespokeReport('/report/ajaxReport?report=\\OEModule\\OphCiExamination\\components\\VisualOutcome');
    OpenEyes.Dash.addBespokeReport('/report/ajaxReport?report=\\OEModule\\OphCiExamination\\components\\RefractiveOutcome');

    $('#export').on('click', function(){
        Highcharts.exportCharts(OpenEyes.Dash.reports,{
            type: 'application/pdf',
            filename: 'cataract-audit'
        });
    });
});

<commit_msg>Remove change brought back by merge which has been refactored elsewhere.<commit_after>/**
 * OpenEyes
 *
 * (C) Moorfields Eye Hospital NHS Foundation Trust, 2008-2011
 * (C) OpenEyes Foundation, 2011-2013
 * This file is part of OpenEyes.
 * OpenEyes is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 * OpenEyes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with OpenEyes in a file titled COPYING. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package OpenEyes
 * @link http://www.openeyes.org.uk
 * @author OpenEyes <info@openeyes.org.uk>
 * @copyright Copyright (c) 2008-2011, Moorfields Eye Hospital NHS Foundation Trust
 * @copyright Copyright (c) 2011-2013, OpenEyes Foundation
 * @license http://www.gnu.org/licenses/gpl-3.0.html The GNU General Public License V3.0
 */
/**
 * Create a global getSVG method that takes an array of charts as an argument
 */
Highcharts.getSVG = function(charts) {
    var svgArr = [],
        top = 60,
        width = 0,
        header = '',
        hasDates = $('#from-date').val() || $('#to-date').val();

    $.each(charts, function(i, chart) {
        var currentWidth = chart.chartWidth;
        chart.setSize(700, chart.chartHeight);
        var svg = chart.getSVG();
        svg = svg.replace('<svg', '<g transform="translate(0,' + top + ')" ');
        svg = svg.replace('</svg>', '</g>');

        top += chart.chartHeight + 50;
        width = Math.max(width, chart.chartWidth);

        svgArr.push(svg);
        chart.setSize(currentWidth, chart.chartHeight);
    });
    width += 50;

    header = '<svg height="60" width="' + width + '" ><text y="20" x="' + (width / 2) + '" text-anchor="middle">'
        + $('.mdl-layout-title').text() + '</text>';

    if(hasDates){
        header += '<text y="40" x="' + (width / 2) + '" text-anchor="middle">';

        if($('#from-date').val()){
            header += 'From: ' +  $('#from-date').val() + ' ';
        } else {
            header += ' From: All time ';
        }

        if($('#to-date').val()){
            header += 'To: ' +  $('#to-date').val();
        } else {
            header += 'To: Present day';
        }

        header += '</text>';
    }
    header += '</svg>';

    return '<svg height="'+ top +'" width="' + width + '" version="1.1" xmlns="http://www.w3.org/2000/svg">' + header + svgArr.join('') + '</svg>';
};

/**
 * Create a global exportCharts method that takes an array of charts as an argument,
 * and exporting options as the second argument
 */
Highcharts.exportCharts = function(charts, options) {
    var form,
    svg = Highcharts.getSVG(charts);

    // merge the options
    options = Highcharts.merge(Highcharts.getOptions().exporting, options);

    // create the form
    form = Highcharts.createElement('form', {
        method: 'post',
        action: '/dashboard/printSvg'
    }, {
        display: 'none'
    }, document.body);

    // add the values
    Highcharts.each(['filename', 'type', 'width', 'svg'], function(name) {
        Highcharts.createElement('input', {
            type: 'hidden',
            name: name,
            value: {
                filename: options.filename || 'chart',
                type: options.type,
                width: options.width,
                svg: svg
            }[name]
        }, null, form);
    });

    var input = document.createElement("input");
    input.type = "hidden";
    input.name = "YII_CSRF_TOKEN";
    input.value = YII_CSRF_TOKEN;
    form.appendChild(input);
    //console.log(svg); return;
    // submit
    form.submit();

    // clean up
    form.parentNode.removeChild(form);
};

Highcharts.setOptions({
    lang: {
        noData: "No data found for this report"
    }
});

<|endoftext|>


<commit_before>(function () {

	'use strict';

	const IS_V2 = Polymer.flush != null;

	Polymer({

		is: 'cosmoz-grouped-list',

		properties: {

			data: {
				type: Array
			},

			scrollTarget: {
				type: HTMLElement
			},

			selectedItems: {
				type: Array,
				notify: true
			},

			highlightedItems: {
				type: Array,
				value() {
					return [];
				},
				notify: true
			},

			/**
			 * Indicates wether this grouped-list should render groups without items.
			 */
			displayEmptyGroups: {
				type: Boolean,
				value: false

			},

			_flatData: {
				type: Array,
			}

		},
		behaviors: [
			Cosmoz.GroupedListTemplatizeBehaviorImpl
		],

		observers: [
			'_dataChanged(data.*)',
			'_scrollTargetChanged(scrollTarget, isAttached)'
		],

		/**
		 * A map of (group,state).
		 * If source data is grouped, then this map stores group level items, with an associated state object indicating wether
		 * the group is folded and selected.
		 */
		_groupsMap: null,

		/**
		 * A map of (item,state), used to store an object indicating wether an item is selected and expanded.
		 */
		_itemsMap: null,

		/**
		 * Polymer `created` livecycle function.
		 *
		 * @return {void}
		 */
		created() {
			this._templateSelectors = [];
		},

		/**
		 * Polymer `attached` livecycle function.
		 *
		 * @return {void}
		 */
		attached() {
			this._debounceRender();
		},

		/**
		 * Polymer `detached` livecycle function.
		 *
		 * @return {void}
		 */
		detached() {
			this.cancelDebouncer('render');
		},

		_dataChanged(change) {
			if (change.path === 'data' || change.path.slice(-8) === '.splices') {
				this._debounceRender();
			} else if (change.path.slice(-7) !== '.length') {
				if (!this._forwardItemPath(change.path, change.value)) {
					this._debounceRender();
				}
			}
		},

		_debounceRender() {
			this.debounce('render', this._render);
		},

		_render() {
			if (!this.isAttached) {
				return;
			}
			this._flatData = this._prepareData(this.data);
		},

		_forwardItemPath(path, value) {
			const match = path.match(/data(?:\.#?(\d+)\.items)?\.#?(\d+)(\..+)$/i);

			if (!match) {
				return;
			}

			const
				groupIndex = match[1],
				itemIndex =  match[2],
				propertyPath = 'item' + match[3],
				item =  groupIndex ? this.data[groupIndex].items[itemIndex] : itemIndex ? this.data[itemIndex] : null;

			if (item == null) {
				console.warn('Item not found when forwarding path', path);
				return;
			}

			const instance = this._getModelFromItem(item);

			if (!instance) {
				console.warn('Template instance for item not found when forwarding path', path);
				return;
			}

			if (IS_V2) {
				instance._setPendingPropertyOrPath(propertyPath, value, false, true);
			} else {
				instance.notifyPath(propertyPath, value, true);
			}

			if (instance._flushProperties) {
				instance._flushProperties(true);
			}

			return true;
		},

		_scrollTargetChanged(scrollTarget, isAttached)  {
			if (! (scrollTarget === undefined || isAttached === undefined)) {
				if (scrollTarget && isAttached) {
					this.classList.add('has-scroll-target');
					this.$.list.scrollTarget = scrollTarget;
				} else {
					this.$.list.scrollTarget = undefined;
					this.classList.remove('has-scroll-target');
				}
			}
		},

		_prepareData(data = null) {

			this.selectedItems = [];

			if (data === null || data.length === 0 || data[0] === undefined) {
				this._expandedItems = this._foldedGroups = this._groupsMap = null;
				return null;
			}
			this._itemsMap = new WeakMap();

			if (!data[0].items) {
				// no grouping, so render items as a standard list
				this._groupsMap = null;
				return data.slice();
			}
			this._groupsMap = new WeakMap();

			return data.reduce(function (flatData, group) {
				if (!group.items) {
					console.warn('Incorrect data, group does not have items');
					return flatData;
				}

				this._groupsMap.set(group, {
					selected: false,
					folded: false
				});

				if (group.items.length) {
					return flatData.concat(group, group.items);
				} else if (this.displayEmptyGroups) {
					return flatData.concat(group);
				}
				return flatData;
			}.bind(this), []);
		},

		_onTemplateSelectorChanged(e, {item, index = this._flatData.indexOf(item), hidden, selector}) {
			const prevInstance = selector.__instance;
			if (hidden && prevInstance !== null) {
				this._reuseInstance(prevInstance);
				selector.__instance = null;
				return;
			}
			const slotName = this._getSlotByIndex(index) + Date.now(),
				isGroup = this.isGroup(item),
				type = this._getItemType(item, isGroup),
				props = {
					[this.as]: item,
					[this.indexAs]: index,
					selected: isGroup ? this.isGroupSelected(item) : this.isItemSelected(item),
					folded: isGroup ? this.isFolded(item) : undefined,
					expanded: !isGroup ? this.isExpanded(item) : undefined,
					highlighted: this.isItemHighlighted(item),
				},
				instance = this._getInstance(type, props, prevInstance);

			let slot = Polymer.dom(selector).querySelector('slot');

			if (slot == null) {
				slot = Polymer.dom(selector).querySelector('content');
				slot.setAttribute('select', '[slot="' + slotName + '"]');
			} else {
				slot.setAttribute('name', slotName);
			}
			instance.element.setAttribute('slot', slotName);
			Polymer.dom(this).appendChild(instance.root);
			selector.__instance = instance;
		},

		/**
		 * Utility method that returns the element that displays the first visible item in the list.
		 * This method is mainly aimed at `cosmoz-omnitable`.
		 * @returns {HTMLElement|null} The first visible element or null
		 */
		getFirstVisibleItemElement() {
			const {_flatData: flat} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}

			const {firstVisibleIndex} = this.$.list;
			const firstVisibleInstance = this._getInstanceByProperty('index', firstVisibleIndex);
			if (firstVisibleInstance) {
				return firstVisibleInstance.element;
			}
		},

		/**
		 * Returns true if this list has rendered data.
		 * This property returns true if at least one group or item has been rendered by iron-list.
		 * If getFirstVisibleItemElement returns undefined, but hasRenderedData returns true,
		 * this means we are displaying only group templates.
		 */
		get hasRenderedData() {
			const {_flatData: flat, _instances: instances} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}
			return instances.some(instance => instance !== null);
		},

		/**
		 * Utility method to remove an item from the list.
		 * This method simply removes the specified item from the `data` using
		 * Polymer array mutation methods.
		 * Cannot be used to remove a group.
		 * @param {Object} item The item to remove
		 * @returns {Boolean|undefined} true/false or null
		 */
		removeItem(item) {
			if (this._groupsMap) {
				return this.data.some((group, groupIndex) => {
					const index = group.items.indexOf(item);
					if (index >= 0) {
						this.splice('data.' + groupIndex + '.items', index, 1);
						return true;
					}
				}, this);
			}
			const i = this.data.indexOf(item);
			if (i >= 0) {
				this.splice('data', i, 1);
			}
		},

		isGroup(item) {
			return this._groupsMap && this._groupsMap.get(item) !== undefined;
		},

		/**
		 * Returns the group of the specified item
		 * @param {Object} item The item to search for
		 * @returns {Object|null} The group the item is in or null
		 */
		getItemGroup(item) {
			if (!this._groupsMap) {
				return;
			}
			return this.data.find(group => Array.isArray(group.items) && group.items.indexOf(item) > -1);
		},

		isFolded(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState && groupState.folded;
		},

		toggleFold(templateInstance) {
			const item = templateInstance.item,
				group = this.isGroup(item) ? item : this.getItemGroup(item),
				isFolded = this.isFolded(group);

			if (isFolded) {
				this.unfoldGroup(group);
			} else {
				this.foldGroup(group);
			}

			templateInstance.folded = !isFolded;
		},

		unfoldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState && groupState.folded) {
				groupState.folded = false;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice.apply(this, ['_flatData', groupFlatIndex + 1, 0].concat(group.items));
			}

		},

		foldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState && !groupState.folded) {
				groupState.folded = true;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice('_flatData', groupFlatIndex + 1, group.items.length);
			}
		},

		selectItem(item) {
			const model = this._getModelFromItem(item);

			if (!this.isItemSelected(item)) {
				this.push('selectedItems', item);
			}
			if (model) {
				model['selected'] = true;
			}
		},

		highlightItem(item, reverse) {
			const model = this._getModelFromItem(item),
				highlightedIndex = this.highlightedItems.indexOf(item);

			if (highlightedIndex === -1 && !reverse) {
				this.push('highlightedItems', item);
			}

			if (highlightedIndex > -1 && reverse) {
				this.splice('highlightedItems', highlightedIndex, 1);
			}

			if (model) {
				model['highlighted'] = !reverse;
			}
		},

		deselectItem(item) {
			const selectedIndex = this.selectedItems.indexOf(item);
			if (selectedIndex >= 0) {
				this.splice('selectedItems', selectedIndex, 1);
			}
			const model = this._getModelFromItem(item);
			if (model) {
				model['selected'] = false;
			}
			// If the containing group was selected, then deselect it
			// as all items are not selected anymore
			const group = this.getItemGroup(item);
			if (group && this.isGroupSelected(group)) {
				const groupState = this._groupsMap.get(group),
					groupModel = this._getModelFromItem(group);
				if (groupModel) {
					groupModel['selected'] = false;
				}
				groupState.selected = false;
			}
		},

		isItemSelected(item) {
			return this.selectedItems.indexOf(item) >= 0;
		},

		isItemHighlighted(item) {
			return this.highlightedItems.indexOf(item) >= 0;
		},

		toggleSelectGroup(group, selected) {
			const model = this._getModelFromItem(group),
				groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState) {
				groupState.selected = selected ? false : true;
			}

			if (model) {
				model['selected'] = selected ? false : true;
			}
			const temp = selected ? 'deselectItem' : 'selectItem';
			group.items.forEach(this[temp], this);
		},

		isGroupSelected(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState !== undefined && groupState.selected;
		},

		_toggleSelected(value) {
			this._templateSelectors.forEach(selector => {
				const templateInstance = selector.currentElement && selector.currentElement.__tmplInstance;
				if (templateInstance) {
					templateInstance.selected = value;
				}
			});
		},

		selectAll() {
			const groups = this._groupsMap;
			let	selected = this.data;

			if (groups) {
				selected = selected.reduce((all, group) =>  {
					const state = groups.get(group);
					if (state) {
						state.selected = true;
					}
					return all.concat(group.items || []);
				}, []);
			}
			this.splice.apply(this, ['selectedItems', 0, this.selectedItems.length].concat(selected));

			// Set the selected property to all visible items
			this._toggleSelected(true);
		},

		deselectAll() {

			this.splice('selectedItems', 0, this.selectedItems.length);

			if (this._groupsMap) {

				this.data.forEach(group => {
					const groupState = this._groupsMap.get(group);
					if (groupState) {
						groupState.selected = false;
					}
				});
			}

			// Set the selected property to all visible items
			this._toggleSelected(false);
		},

		updateSize(item) {
			// Do not attempt to update size of item is not visible (for example when groups are folded)
			if (this._flatData.indexOf(item) >= 0) {
				this.$.list.updateSizeForItem(item);
			}
		},

		updateSizes(group) {
			group.items.forEach(this.updateSize, this);
		},

		toggleCollapse(item) {
			const model = this._getModelFromItem(item);
			if (!model) {
				return;
			}
			model.expanded = !model.expanded;

			let itemState = this._itemsMap.get(item);
			if (!itemState) {
				itemState = { selected: false, expanded: false };
				this._itemsMap.set(item, itemState);
			}
			itemState.expanded = model.expanded;
			this.$.list.updateSizeForItem(item);
		},

		isExpanded(item) {
			const itemState = this._itemsMap ? this._itemsMap.get(item) : undefined;
			return itemState !== undefined && itemState.expanded;
		},

		_getModelFromItem(item) {
			return this._getInstanceByProperty('item', item);
		},

		_getSlotByIndex(index) {
			return `cosmoz-glts-${index || ''}`;
		},

		_getItemType(item, isGroup = this.isGroup(item)) {
			return isGroup ? 'group' : 'item';
		}
	});
}());
<commit_msg>handle null _flatData<commit_after>(function () {

	'use strict';

	const IS_V2 = Polymer.flush != null;

	Polymer({

		is: 'cosmoz-grouped-list',

		properties: {

			data: {
				type: Array
			},

			scrollTarget: {
				type: HTMLElement
			},

			selectedItems: {
				type: Array,
				notify: true
			},

			highlightedItems: {
				type: Array,
				value() {
					return [];
				},
				notify: true
			},

			/**
			 * Indicates wether this grouped-list should render groups without items.
			 */
			displayEmptyGroups: {
				type: Boolean,
				value: false

			},

			_flatData: {
				type: Array,
			}

		},
		behaviors: [
			Cosmoz.GroupedListTemplatizeBehaviorImpl
		],

		observers: [
			'_dataChanged(data.*)',
			'_scrollTargetChanged(scrollTarget, isAttached)'
		],

		/**
		 * A map of (group,state).
		 * If source data is grouped, then this map stores group level items, with an associated state object indicating wether
		 * the group is folded and selected.
		 */
		_groupsMap: null,

		/**
		 * A map of (item,state), used to store an object indicating wether an item is selected and expanded.
		 */
		_itemsMap: null,

		/**
		 * Polymer `created` livecycle function.
		 *
		 * @return {void}
		 */
		created() {
			this._templateSelectors = [];
		},

		/**
		 * Polymer `attached` livecycle function.
		 *
		 * @return {void}
		 */
		attached() {
			this._debounceRender();
		},

		/**
		 * Polymer `detached` livecycle function.
		 *
		 * @return {void}
		 */
		detached() {
			this.cancelDebouncer('render');
		},

		_dataChanged(change) {
			if (change.path === 'data' || change.path.slice(-8) === '.splices') {
				this._debounceRender();
			} else if (change.path.slice(-7) !== '.length') {
				if (!this._forwardItemPath(change.path, change.value)) {
					this._debounceRender();
				}
			}
		},

		_debounceRender() {
			this.debounce('render', this._render);
		},

		_render() {
			if (!this.isAttached) {
				return;
			}
			this._flatData = this._prepareData(this.data);
		},

		_forwardItemPath(path, value) {
			const match = path.match(/data(?:\.#?(\d+)\.items)?\.#?(\d+)(\..+)$/i);

			if (!match) {
				return;
			}

			const
				groupIndex = match[1],
				itemIndex =  match[2],
				propertyPath = 'item' + match[3],
				item =  groupIndex ? this.data[groupIndex].items[itemIndex] : itemIndex ? this.data[itemIndex] : null;

			if (item == null) {
				console.warn('Item not found when forwarding path', path);
				return;
			}

			const instance = this._getModelFromItem(item);

			if (!instance) {
				console.warn('Template instance for item not found when forwarding path', path);
				return;
			}

			if (IS_V2) {
				instance._setPendingPropertyOrPath(propertyPath, value, false, true);
			} else {
				instance.notifyPath(propertyPath, value, true);
			}

			if (instance._flushProperties) {
				instance._flushProperties(true);
			}

			return true;
		},

		_scrollTargetChanged(scrollTarget, isAttached)  {
			if (! (scrollTarget === undefined || isAttached === undefined)) {
				if (scrollTarget && isAttached) {
					this.classList.add('has-scroll-target');
					this.$.list.scrollTarget = scrollTarget;
				} else {
					this.$.list.scrollTarget = undefined;
					this.classList.remove('has-scroll-target');
				}
			}
		},

		_prepareData(data = null) {

			this.selectedItems = [];

			if (data === null || data.length === 0 || data[0] === undefined) {
				this._expandedItems = this._foldedGroups = this._groupsMap = null;
				return null;
			}
			this._itemsMap = new WeakMap();

			if (!data[0].items) {
				// no grouping, so render items as a standard list
				this._groupsMap = null;
				return data.slice();
			}
			this._groupsMap = new WeakMap();

			return data.reduce(function (flatData, group) {
				if (!group.items) {
					console.warn('Incorrect data, group does not have items');
					return flatData;
				}

				this._groupsMap.set(group, {
					selected: false,
					folded: false
				});

				if (group.items.length) {
					return flatData.concat(group, group.items);
				} else if (this.displayEmptyGroups) {
					return flatData.concat(group);
				}
				return flatData;
			}.bind(this), []);
		},

		_onTemplateSelectorChanged(e, {item, index, hidden, selector}) {
			let idx = index !== null ? index : this._flatData ? this._flatData.indexOf(item) : '';

			const prevInstance = selector.__instance;
			if (hidden && prevInstance !== null) {
				this._reuseInstance(prevInstance);
				selector.__instance = null;
				return;
			}
			const slotName = this._getSlotByIndex(idx) + Date.now(),
				isGroup = this.isGroup(item),
				type = this._getItemType(item, isGroup),
				props = {
					[this.as]: item,
					[this.indexAs]: idx,
					selected: isGroup ? this.isGroupSelected(item) : this.isItemSelected(item),
					folded: isGroup ? this.isFolded(item) : undefined,
					expanded: !isGroup ? this.isExpanded(item) : undefined,
					highlighted: this.isItemHighlighted(item),
				},
				instance = this._getInstance(type, props, prevInstance);

			let slot = Polymer.dom(selector).querySelector('slot'); //eslint-disable-line one-var
			if (slot == null) {
				slot = Polymer.dom(selector).querySelector('content');
				slot.setAttribute('select', '[slot="' + slotName + '"]');
			} else {
				slot.setAttribute('name', slotName);
			}
			instance.element.setAttribute('slot', slotName);
			Polymer.dom(this).appendChild(instance.root);
			selector.__instance = instance;
		},

		/**
		 * Utility method that returns the element that displays the first visible item in the list.
		 * This method is mainly aimed at `cosmoz-omnitable`.
		 * @returns {HTMLElement|null} The first visible element or null
		 */
		getFirstVisibleItemElement() {
			const {_flatData: flat} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}

			const {firstVisibleIndex} = this.$.list;
			const firstVisibleInstance = this._getInstanceByProperty('index', firstVisibleIndex);
			if (firstVisibleInstance) {
				return firstVisibleInstance.element;
			}
		},

		/**
		 * Returns true if this list has rendered data.
		 * This property returns true if at least one group or item has been rendered by iron-list.
		 * If getFirstVisibleItemElement returns undefined, but hasRenderedData returns true,
		 * this means we are displaying only group templates.
		 */
		get hasRenderedData() {
			const {_flatData: flat, _instances: instances} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}
			return instances.some(instance => instance !== null);
		},

		/**
		 * Utility method to remove an item from the list.
		 * This method simply removes the specified item from the `data` using
		 * Polymer array mutation methods.
		 * Cannot be used to remove a group.
		 * @param {Object} item The item to remove
		 * @returns {Boolean|undefined} true/false or null
		 */
		removeItem(item) {
			if (this._groupsMap) {
				return this.data.some((group, groupIndex) => {
					const index = group.items.indexOf(item);
					if (index >= 0) {
						this.splice('data.' + groupIndex + '.items', index, 1);
						return true;
					}
				}, this);
			}
			const i = this.data.indexOf(item);
			if (i >= 0) {
				this.splice('data', i, 1);
			}
		},

		isGroup(item) {
			return this._groupsMap && this._groupsMap.get(item) !== undefined;
		},

		/**
		 * Returns the group of the specified item
		 * @param {Object} item The item to search for
		 * @returns {Object|null} The group the item is in or null
		 */
		getItemGroup(item) {
			if (!this._groupsMap) {
				return;
			}
			return this.data.find(group => Array.isArray(group.items) && group.items.indexOf(item) > -1);
		},

		isFolded(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState && groupState.folded;
		},

		toggleFold(templateInstance) {
			const item = templateInstance.item,
				group = this.isGroup(item) ? item : this.getItemGroup(item),
				isFolded = this.isFolded(group);

			if (isFolded) {
				this.unfoldGroup(group);
			} else {
				this.foldGroup(group);
			}

			templateInstance.folded = !isFolded;
		},

		unfoldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState && groupState.folded) {
				groupState.folded = false;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice.apply(this, ['_flatData', groupFlatIndex + 1, 0].concat(group.items));
			}

		},

		foldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState && !groupState.folded) {
				groupState.folded = true;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice('_flatData', groupFlatIndex + 1, group.items.length);
			}
		},

		selectItem(item) {
			const model = this._getModelFromItem(item);

			if (!this.isItemSelected(item)) {
				this.push('selectedItems', item);
			}
			if (model) {
				model['selected'] = true;
			}
		},

		highlightItem(item, reverse) {
			const model = this._getModelFromItem(item),
				highlightedIndex = this.highlightedItems.indexOf(item);

			if (highlightedIndex === -1 && !reverse) {
				this.push('highlightedItems', item);
			}

			if (highlightedIndex > -1 && reverse) {
				this.splice('highlightedItems', highlightedIndex, 1);
			}

			if (model) {
				model['highlighted'] = !reverse;
			}
		},

		deselectItem(item) {
			const selectedIndex = this.selectedItems.indexOf(item);
			if (selectedIndex >= 0) {
				this.splice('selectedItems', selectedIndex, 1);
			}
			const model = this._getModelFromItem(item);
			if (model) {
				model['selected'] = false;
			}
			// If the containing group was selected, then deselect it
			// as all items are not selected anymore
			const group = this.getItemGroup(item);
			if (group && this.isGroupSelected(group)) {
				const groupState = this._groupsMap.get(group),
					groupModel = this._getModelFromItem(group);
				if (groupModel) {
					groupModel['selected'] = false;
				}
				groupState.selected = false;
			}
		},

		isItemSelected(item) {
			return this.selectedItems.indexOf(item) >= 0;
		},

		isItemHighlighted(item) {
			return this.highlightedItems.indexOf(item) >= 0;
		},

		toggleSelectGroup(group, selected) {
			const model = this._getModelFromItem(group),
				groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState) {
				groupState.selected = selected ? false : true;
			}

			if (model) {
				model['selected'] = selected ? false : true;
			}
			const temp = selected ? 'deselectItem' : 'selectItem';
			group.items.forEach(this[temp], this);
		},

		isGroupSelected(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState !== undefined && groupState.selected;
		},

		_toggleSelected(value) {
			this._templateSelectors.forEach(selector => {
				const templateInstance = selector.currentElement && selector.currentElement.__tmplInstance;
				if (templateInstance) {
					templateInstance.selected = value;
				}
			});
		},

		selectAll() {
			const groups = this._groupsMap;
			let	selected = this.data;

			if (groups) {
				selected = selected.reduce((all, group) =>  {
					const state = groups.get(group);
					if (state) {
						state.selected = true;
					}
					return all.concat(group.items || []);
				}, []);
			}
			this.splice.apply(this, ['selectedItems', 0, this.selectedItems.length].concat(selected));

			// Set the selected property to all visible items
			this._toggleSelected(true);
		},

		deselectAll() {

			this.splice('selectedItems', 0, this.selectedItems.length);

			if (this._groupsMap) {

				this.data.forEach(group => {
					const groupState = this._groupsMap.get(group);
					if (groupState) {
						groupState.selected = false;
					}
				});
			}

			// Set the selected property to all visible items
			this._toggleSelected(false);
		},

		updateSize(item) {
			// Do not attempt to update size of item is not visible (for example when groups are folded)
			if (this._flatData.indexOf(item) >= 0) {
				this.$.list.updateSizeForItem(item);
			}
		},

		updateSizes(group) {
			group.items.forEach(this.updateSize, this);
		},

		toggleCollapse(item) {
			const model = this._getModelFromItem(item);
			if (!model) {
				return;
			}
			model.expanded = !model.expanded;

			let itemState = this._itemsMap.get(item);
			if (!itemState) {
				itemState = { selected: false, expanded: false };
				this._itemsMap.set(item, itemState);
			}
			itemState.expanded = model.expanded;
			this.$.list.updateSizeForItem(item);
		},

		isExpanded(item) {
			const itemState = this._itemsMap ? this._itemsMap.get(item) : undefined;
			return itemState !== undefined && itemState.expanded;
		},

		_getModelFromItem(item) {
			return this._getInstanceByProperty('item', item);
		},

		_getSlotByIndex(index) {
			return `cosmoz-glts-${index || ''}`;
		},

		_getItemType(item, isGroup = this.isGroup(item)) {
			return isGroup ? 'group' : 'item';
		}
	});
}());
<|endoftext|>


<commit_before>(function () {
	'use strict';

	Polymer({
		is: 'cosmoz-grouped-list',

		properties: {

			data: {
				type: Array
			},

			scrollTarget: {
				type: HTMLElement
			},

			selectedItems: {
				type: Array,
				notify: true
			},

			highlightedItems: {
				type: Array,
				value() {
					return [];
				},
				notify: true
			},

			/**
			 * Indicates wether this grouped-list should render groups without items.
			 */
			displayEmptyGroups: {
				type: Boolean,
				value: false

			},

			_flatData: {
				type: Array,
			}

		},
		behaviors: [
			Cosmoz.GroupedListTemplatizeBehavior
		],

		observers: [
			'_dataChanged(data.*)',
			'_scrollTargetChanged(scrollTarget, isAttached)'
		],

		/**
		 * A map of (group,state).
		 * If source data is grouped, then this map stores group level items, with an associated state object indicating wether
		 * the group is folded and selected.
		 */
		_groupsMap: null,

		/**
		 * A map of (item,state), used to store an object indicating wether an item is selected and expanded.
		 */
		_itemsMap: null,

		/**
		 * Polymer `attached` livecycle function.
		 *
		 * @return {void}
		 */
		attached() {
			this._debounceRender();
		},

		/**
		 * Polymer `detached` livecycle function.
		 *
		 * @return {void}
		 */
		detached() {
			this.cancelDebouncer('render');
		},

		_dataChanged(change) {
			if (change.path === 'data' || change.path.slice(-8) === '.splices') {
				this._debounceRender();
			} else if (change.path.slice(-7) !== '.length') {
				if (!this._forwardItemPath(change.path, change.value)) {
					this._debounceRender();
				}
			}
		},

		_debounceRender() {
			this.debounce('render', this._render);
		},

		_render() {
			if (!this.isAttached) {
				return;
			}
			this._flatData = this._prepareData(this.data);
		},

		_forwardItemPath(path, value) {
			const match = path.match(/data(?:\.#?(\d+)\.items)?\.#?(\d+)(\..+)$/i);

			if (!match) {
				return;
			}

			const groupIndex = match[1],
				itemIndex =  match[2],
				propertyPath = 'item' + match[3],
				item =  groupIndex ? this.data[groupIndex].items[itemIndex] : itemIndex ? this.data[itemIndex] : null;

			if (item == null) {
				console.warn('Item not found when forwarding path', path);
				return;
			}

			const instance = this._getInstanceByProperty('item', item);

			if (!instance) {
				console.warn('Template instance for item not found when forwarding path', path);
				return;
			}

			instance._setPendingPropertyOrPath(propertyPath, value, false, true);

			if (instance._flushProperties) {
				instance._flushProperties(true);
			}

			return true;
		},

		_scrollTargetChanged(scrollTarget, isAttached)  {
			if (! (scrollTarget === undefined || isAttached === undefined)) {
				if (scrollTarget && isAttached) {
					this.classList.add('has-scroll-target');
					this.$.list.scrollTarget = scrollTarget;
				} else {
					this.$.list.scrollTarget = undefined;
					this.classList.remove('has-scroll-target');
				}
			}
		},

		_prepareData(data = null) {

			this.selectedItems = [];

			if (data === null || data.length === 0 || data[0] === undefined) {
				this._expandedItems = this._foldedGroups = this._groupsMap = null;
				return null;
			}
			this._itemsMap = new WeakMap();

			if (!data[0].items) {
				// no grouping, so render items as a standard list
				this._groupsMap = null;
				return data.slice();
			}
			this._groupsMap = new WeakMap();

			return data.reduce(function (flatData, group) {
				if (!group.items) {
					console.warn('Incorrect data, group does not have items');
					return flatData;
				}

				this._groupsMap.set(group, {
					selected: false,
					folded: false
				});

				if (group.items.length) {
					return flatData.concat(group, group.items);
				} else if (this.displayEmptyGroups) {
					return flatData.concat(group);
				}
				return flatData;
			}.bind(this), []);
		},

		_onTemplateSelectorChanged(e, {item, index, hidden, selector}) {
			const idx = index != null ? index : this._flatData ? this._flatData.indexOf(item) : '',
				prevInstance = selector.__instance;
			if (hidden && prevInstance != null) {
				this._reuseInstance(prevInstance);
				selector.__instance = null;
				return;
			}
			const slotName = this._getSlotByIndex(idx) + Date.now(),
				isGroup = this.isGroup(item),
				type = this._getItemType(item, isGroup),
				props = {
					[this.as]: item,
					[this.indexAs]: idx,
					selected: isGroup ? this.isGroupSelected(item) : this.isItemSelected(item),
					folded: isGroup ? this.isFolded(item) : undefined,
					expanded: !isGroup ? this.isExpanded(item) : undefined,
					highlighted: this.isItemHighlighted(item),
				},
				instance = this._getInstance(type, props, prevInstance, item != null),
				slot = Polymer.dom(selector).querySelector('slot');

			slot.setAttribute('name', slotName);
			instance.element.setAttribute('slot', slotName);
			Polymer.dom(this).appendChild(
				Polymer.Settings.useShadow
					? instance.root
					: instance.element
			);
			selector.__instance = instance;
		},

		/**
		 * Utility method that returns the element that displays the first visible item in the list.
		 * This method is mainly aimed at `cosmoz-omnitable`.
		 * @returns {HTMLElement|null} The first visible element or null
		 */
		getFirstVisibleItemElement() {
			const {_flatData: flat} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}

			let {firstVisibleIndex: index} = this.$.list,
				instance = null;

			while (index < flat.length && !instance) {
				instance =  this._getInstanceByProperty('index', index);
				if (instance) {
					instance = instance.__type === 'item' && instance;
				}
				index++;
			}

			if (instance) {
				return instance.element;
			}
		},

		/**
		 * Returns true if this list has rendered data.
		 * This property returns true if at least one group or item has been rendered by iron-list.
		 * If getFirstVisibleItemElement returns undefined, but hasRenderedData returns true,
		 * this means we are displaying only group templates.
		 */
		get hasRenderedData() {
			const {_flatData: flat, _instances: instances} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}
			return instances.some(instance => instance !== null);
		},

		/**
		 * Utility method to remove an item from the list.
		 * This method simply removes the specified item from the `data` using
		 * Polymer array mutation methods.
		 * Cannot be used to remove a group.
		 * @param {Object} item The item to remove
		 * @returns {Boolean|undefined} true/false or null
		 */
		removeItem(item) {
			if (this._groupsMap) {
				return this.data.some((group, groupIndex) => {
					const index = group.items.indexOf(item);
					if (index >= 0) {
						this.splice('data.' + groupIndex + '.items', index, 1);
						return true;
					}
				}, this);
			}
			const i = this.data.indexOf(item);
			if (i >= 0) {
				this.splice('data', i, 1);
			}
		},

		isGroup(item) {
			return this._groupsMap && this._groupsMap.get(item) !== undefined;
		},

		/**
		 * Returns the group of the specified item
		 * @param {Object} item The item to search for
		 * @returns {Object|null} The group the item is in or null
		 */
		getItemGroup(item) {
			if (!this._groupsMap) {
				return;
			}
			return this.data.find(group => Array.isArray(group.items) && group.items.indexOf(item) > -1);
		},

		isFolded(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState && groupState.folded;
		},

		toggleFold(item) {
			const group = this.isGroup(item) ? item : this.getItemGroup(item),
				isFolded = this.isFolded(group);

			if (isFolded) {
				this.unfoldGroup(group);
			} else {
				this.foldGroup(group);
			}
		},

		unfoldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState && groupState.folded) {
				groupState.folded = false;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice.apply(this, ['_flatData', groupFlatIndex + 1, 0].concat(group.items));
				this._forwardPropertyByItem(group, 'folded', false, true);
			}
		},

		foldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			if (groupState && !groupState.folded) {
				groupState.folded = true;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice('_flatData', groupFlatIndex + 1, group.items.length);
				this._forwardPropertyByItem(group, 'folded', true, true);
			}
		},

		selectItem(item) {
			if (!this.isItemSelected(item)) {
				this.push('selectedItems', item);
			}
			this._forwardPropertyByItem(item, 'selected', true, true);
		},

		highlightItem(item, reverse) {
			const	highlightedIndex = this.highlightedItems.indexOf(item);

			if (highlightedIndex === -1 && !reverse) {
				this.push('highlightedItems', item);
			}

			if (highlightedIndex > -1 && reverse) {
				this.splice('highlightedItems', highlightedIndex, 1);
			}
			this._forwardPropertyByItem(item, 'highlighted', !reverse, true);
		},

		deselectItem(item) {
			const index = this.selectedItems.indexOf(item);
			if (index >= 0) {
				this.splice('selectedItems', index, 1);
			}
			this._forwardPropertyByItem(item, 'selected', false, true);
			// If the containing group was selected, then deselect it
			// as all items are not selected anymore
			const group = this.getItemGroup(item);
			if (group && this.isGroupSelected(group)) {
				const groupState = this._groupsMap.get(group);
				groupState.selected = false;
				this._forwardPropertyByItem(group, 'selected', false, true);
			}
		},

		isItemSelected(item) {
			return this.selectedItems.indexOf(item) >= 0;
		},

		isItemHighlighted(item) {
			return this.highlightedItems.indexOf(item) >= 0;
		},

		toggleSelectGroup(group, selected) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			const willSelect = selected ? false : true;
			const itemAction = willSelect ? 'selectItem' : 'deselectItem';
			if (groupState) {
				groupState.selected = willSelect;
			}
			this._forwardPropertyByItem(group, 'selected', willSelect, true);
			group.items.forEach(this[itemAction], this);
		},

		isGroupSelected(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState !== undefined && groupState.selected;
		},

		_toggleSelected(value) {
			this._instances.forEach(instance => this._forwardProperty(instance, 'selected', value, true));
		},

		selectAll() {
			const groups = this._groupsMap;
			let	selected = this.data;

			if (groups) {
				selected = selected.reduce((all, group) =>  {
					const state = groups.get(group);
					if (state) {
						state.selected = true;
					}
					return all.concat(group.items || []);
				}, []);
			}
			this.splice.apply(this, ['selectedItems', 0, this.selectedItems.length].concat(selected));

			// Set the selected property to all visible items
			this._toggleSelected(true);
		},

		deselectAll() {

			this.splice('selectedItems', 0, this.selectedItems.length);

			if (this._groupsMap) {

				this.data.forEach(group => {
					const groupState = this._groupsMap.get(group);
					if (groupState) {
						groupState.selected = false;
					}
				});
			}

			// Set the selected property to all visible items
			this._toggleSelected(false);
		},

		updateSize(item) {
			// Do not attempt to update size of item is not visible (for example when groups are folded)
			if (this._flatData.indexOf(item) >= 0) {
				this.$.list.updateSizeForItem(item);
			}
		},

		updateSizes(group) {
			group.items.forEach(this.updateSize, this);
		},

		toggleCollapse(item) {
			const	state = this._itemsMap.get(item) || { selected: false, expanded: false },
				willExpand = state.expanded = !state.expanded;
			this._itemsMap.set(item, state);
			this._forwardPropertyByItem(item, 'expanded', willExpand, true);
			this.$.list.updateSizeForItem(item);
		},

		isExpanded(item) {
			const itemState = this._itemsMap ? this._itemsMap.get(item) : undefined;
			return itemState !== undefined && itemState.expanded;
		},

		_getSlotByIndex(index) {
			return `cosmoz-glts-${index}`;
		},

		_getItemType(item, isGroup = this.isGroup(item)) {
			return isGroup ? 'group' : 'item';
		}
	});
}());
<commit_msg>Update to Polymer 2.x - part 2.<commit_after>(function () {
	'use strict';

	class CosmozGroupedList extends Polymer.mixinBehaviors([Cosmoz.GroupedListTemplatizeBehavior], Polymer.Element) {
		constructor() {
			super();
			/**
			 * A map of (group,state).
			 * If source data is grouped, then this map stores group level items, with an associated state object indicating wether
			 * the group is folded and selected.
			 */
			this._groupsMap = null;

			/**
			 * A map of (item,state), used to store an object indicating wether an item is selected and expanded.
			 */
			this._itemsMap = null;
		}

		static get is() {
			return 'cosmoz-grouped-list';
		}

		static get properties() {
			return {

				data: {
					type: Array
				},

				scrollTarget: {
					type: HTMLElement
				},

				selectedItems: {
					type: Array,
					notify: true
				},

				highlightedItems: {
					type: Array,
					value() {
						return [];
					},
					notify: true
				},

				/**
				 * Indicates wether this grouped-list should render groups without items.
				 */
				displayEmptyGroups: {
					type: Boolean,
					value: false

				},

				_flatData: {
					type: Array,
				}

			};
		}

		static get observers() {
			return [
				'_dataChanged(data.*)',
				'_scrollTargetChanged(scrollTarget, isAttached, classList)'
			];
		}

		/**
		 * Polymer `attached` livecycle function.
		 *
		 * @return {void}
		 */
		attached() {
			this._debounceRender();
		}

		/**
		 * Polymer `detached` livecycle function.
		 *
		 * @return {void}
		 */
		detached() {
			this.cancelDebouncer('render');
		}

		_dataChanged(change) {
			if (change.path === 'data' || change.path.slice(-8) === '.splices') {
				this._debounceRender();
			} else if (change.path.slice(-7) !== '.length') {
				if (!this._forwardItemPath(change.path, change.value)) {
					this._debounceRender();
				}
			}
		}

		_debounceRender() {
			this.debounce('render', this._render);
		}

		_render() {
			if (!this.isAttached) {
				return;
			}
			this._flatData = this._prepareData(this.data);
		}

		_forwardItemPath(path, value) {
			const match = path.match(/data(?:\.#?(\d+)\.items)?\.#?(\d+)(\..+)$/i);

			if (!match) {
				return;
			}

			const groupIndex = match[1],
				itemIndex =  match[2],
				propertyPath = 'item' + match[3],
				item =  groupIndex ? this.data[groupIndex].items[itemIndex] : itemIndex ? this.data[itemIndex] : null;

			if (item == null) {
				console.warn('Item not found when forwarding path', path);
				return;
			}

			const instance = this._getInstanceByProperty('item', item);

			if (!instance) {
				console.warn('Template instance for item not found when forwarding path', path);
				return;
			}

			instance._setPendingPropertyOrPath(propertyPath, value, false, true);

			if (instance._flushProperties) {
				instance._flushProperties(true);
			}

			return true;
		}

		_scrollTargetChanged(scrollTarget, isAttached, classList)  {
			if (! (scrollTarget === undefined || isAttached === undefined)) {
				if (scrollTarget && isAttached) {
					classList.add('has-scroll-target');
					this.$.list.scrollTarget = scrollTarget;
				} else {
					this.$.list.scrollTarget = undefined;
					classList.remove('has-scroll-target');
				}
			}
		}

		_prepareData(data = null) {

			this.selectedItems = [];

			if (data === null || data.length === 0 || data[0] === undefined) {
				this._expandedItems = this._foldedGroups = this._groupsMap = null;
				return null;
			}
			this._itemsMap = new WeakMap();

			if (!data[0].items) {
				// no grouping, so render items as a standard list
				this._groupsMap = null;
				return data.slice();
			}
			this._groupsMap = new WeakMap();

			return data.reduce(function (flatData, group) {
				if (!group.items) {
					console.warn('Incorrect data, group does not have items');
					return flatData;
				}

				this._groupsMap.set(group, {
					selected: false,
					folded: false
				});

				if (group.items.length) {
					return flatData.concat(group, group.items);
				} else if (this.displayEmptyGroups) {
					return flatData.concat(group);
				}
				return flatData;
			}.bind(this), []);
		}

		_onTemplateSelectorChanged(e, {item, index, hidden, selector}) {
			const idx = index != null ? index : this._flatData ? this._flatData.indexOf(item) : '',
				prevInstance = selector.__instance;
			if (hidden && prevInstance != null) {
				this._reuseInstance(prevInstance);
				selector.__instance = null;
				return;
			}
			const slotName = this._getSlotByIndex(idx) + Date.now(),
				isGroup = this.isGroup(item),
				type = this._getItemType(item, isGroup),
				props = {
					[this.as]: item,
					[this.indexAs]: idx,
					selected: isGroup ? this.isGroupSelected(item) : this.isItemSelected(item),
					folded: isGroup ? this.isFolded(item) : undefined,
					expanded: !isGroup ? this.isExpanded(item) : undefined,
					highlighted: this.isItemHighlighted(item),
				},
				instance = this._getInstance(type, props, prevInstance, item != null),
				slot = Polymer.dom(selector).querySelector('slot');

			slot.setAttribute('name', slotName);
			instance.element.setAttribute('slot', slotName);
			Polymer.dom(this).appendChild(
				Polymer.Settings.useShadow
					? instance.root
					: instance.element
			);
			selector.__instance = instance;
		}

		/**
		 * Utility method that returns the element that displays the first visible item in the list.
		 * This method is mainly aimed at `cosmoz-omnitable`.
		 * @returns {HTMLElement|null} The first visible element or null
		 */
		getFirstVisibleItemElement() {
			const {_flatData: flat} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}

			let {firstVisibleIndex: index} = this.$.list,
				instance = null;

			while (index < flat.length && !instance) {
				instance =  this._getInstanceByProperty('index', index);
				if (instance) {
					instance = instance.__type === 'item' && instance;
				}
				index++;
			}

			if (instance) {
				return instance.element;
			}
		}

		/**
		 * Returns true if this list has rendered data.
		 * This property returns true if at least one group or item has been rendered by iron-list.
		 * If getFirstVisibleItemElement returns undefined, but hasRenderedData returns true,
		 * this means we are displaying only group templates.
		 */
		get hasRenderedData() {
			const {_flatData: flat, _instances: instances} = this;
			if (!Array.isArray(flat) || flat.length === 0) {
				return false;
			}
			return instances.some(instance => instance !== null);
		}

		/**
		 * Utility method to remove an item from the list.
		 * This method simply removes the specified item from the `data` using
		 * Polymer array mutation methods.
		 * Cannot be used to remove a group.
		 * @param {Object} item The item to remove
		 * @returns {Boolean|undefined} true/false or null
		 */
		removeItem(item) {
			if (this._groupsMap) {
				return this.data.some((group, groupIndex) => {
					const index = group.items.indexOf(item);
					if (index >= 0) {
						this.splice('data.' + groupIndex + '.items', index, 1);
						return true;
					}
				}, this);
			}
			const i = this.data.indexOf(item);
			if (i >= 0) {
				this.splice('data', i, 1);
			}
		}

		isGroup(item) {
			return this._groupsMap && this._groupsMap.get(item) !== undefined;
		}

		/**
		 * Returns the group of the specified item
		 * @param {Object} item The item to search for
		 * @returns {Object|null} The group the item is in or null
		 */
		getItemGroup(item) {
			if (!this._groupsMap) {
				return;
			}
			return this.data.find(group => Array.isArray(group.items) && group.items.indexOf(item) > -1);
		}

		isFolded(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState && groupState.folded;
		}

		toggleFold(item) {
			const group = this.isGroup(item) ? item : this.getItemGroup(item),
				isFolded = this.isFolded(group);

			if (isFolded) {
				this.unfoldGroup(group);
			} else {
				this.foldGroup(group);
			}
		}

		unfoldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);

			if (groupState && groupState.folded) {
				groupState.folded = false;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice.apply(this, ['_flatData', groupFlatIndex + 1, 0].concat(group.items));
				this._forwardPropertyByItem(group, 'folded', false, true);
			}
		}

		foldGroup(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			if (groupState && !groupState.folded) {
				groupState.folded = true;
				const groupFlatIndex = this._flatData.indexOf(group);
				this.splice('_flatData', groupFlatIndex + 1, group.items.length);
				this._forwardPropertyByItem(group, 'folded', true, true);
			}
		}

		selectItem(item) {
			if (!this.isItemSelected(item)) {
				this.push('selectedItems', item);
			}
			this._forwardPropertyByItem(item, 'selected', true, true);
		}

		highlightItem(item, reverse) {
			const	highlightedIndex = this.highlightedItems.indexOf(item);

			if (highlightedIndex === -1 && !reverse) {
				this.push('highlightedItems', item);
			}

			if (highlightedIndex > -1 && reverse) {
				this.splice('highlightedItems', highlightedIndex, 1);
			}
			this._forwardPropertyByItem(item, 'highlighted', !reverse, true);
		}

		deselectItem(item) {
			const index = this.selectedItems.indexOf(item);
			if (index >= 0) {
				this.splice('selectedItems', index, 1);
			}
			this._forwardPropertyByItem(item, 'selected', false, true);
			// If the containing group was selected, then deselect it
			// as all items are not selected anymore
			const group = this.getItemGroup(item);
			if (group && this.isGroupSelected(group)) {
				const groupState = this._groupsMap.get(group);
				groupState.selected = false;
				this._forwardPropertyByItem(group, 'selected', false, true);
			}
		}

		isItemSelected(item) {
			return this.selectedItems.indexOf(item) >= 0;
		}

		isItemHighlighted(item) {
			return this.highlightedItems.indexOf(item) >= 0;
		}

		toggleSelectGroup(group, selected) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			const willSelect = selected ? false : true;
			const itemAction = willSelect ? 'selectItem' : 'deselectItem';
			if (groupState) {
				groupState.selected = willSelect;
			}
			this._forwardPropertyByItem(group, 'selected', willSelect, true);
			group.items.forEach(this[itemAction], this);
		}

		isGroupSelected(group) {
			const groupState = this._groupsMap && this._groupsMap.get(group);
			return groupState !== undefined && groupState.selected;
		}

		_toggleSelected(value) {
			this._instances.forEach(instance => this._forwardProperty(instance, 'selected', value, true));
		}

		selectAll() {
			const groups = this._groupsMap;
			let	selected = this.data;

			if (groups) {
				selected = selected.reduce((all, group) =>  {
					const state = groups.get(group);
					if (state) {
						state.selected = true;
					}
					return all.concat(group.items || []);
				}, []);
			}
			this.splice.apply(this, ['selectedItems', 0, this.selectedItems.length].concat(selected));

			// Set the selected property to all visible items
			this._toggleSelected(true);
		}

		deselectAll() {

			this.splice('selectedItems', 0, this.selectedItems.length);

			if (this._groupsMap) {

				this.data.forEach(group => {
					const groupState = this._groupsMap.get(group);
					if (groupState) {
						groupState.selected = false;
					}
				});
			}

			// Set the selected property to all visible items
			this._toggleSelected(false);
		}

		updateSize(item) {
			// Do not attempt to update size of item is not visible (for example when groups are folded)
			if (this._flatData.indexOf(item) >= 0) {
				this.$.list.updateSizeForItem(item);
			}
		}

		updateSizes(group) {
			group.items.forEach(this.updateSize, this);
		}

		toggleCollapse(item) {
			const	state = this._itemsMap.get(item) || { selected: false, expanded: false },
				willExpand = state.expanded = !state.expanded;
			this._itemsMap.set(item, state);
			this._forwardPropertyByItem(item, 'expanded', willExpand, true);
			this.$.list.updateSizeForItem(item);
		}

		isExpanded(item) {
			const itemState = this._itemsMap ? this._itemsMap.get(item) : undefined;
			return itemState !== undefined && itemState.expanded;
		}

		_getSlotByIndex(index) {
			return `cosmoz-glts-${index}`;
		}

		_getItemType(item, isGroup = this.isGroup(item)) {
			return isGroup ? 'group' : 'item';
		}
	}
	customElements.define(CosmozGroupedList.is, CosmozGroupedList);
}());
<|endoftext|>