<commit_before>var initScanned = false;

Collection={
	artists:[],
	albums:[],
	songs:[],
	artistsById:{},
	albumsById:{},
	loaded:false,
	loading:false,
	loadedListeners:[],
	load:function(ready){
		if(ready){
			Collection.loadedListeners.push(ready);
		}
		if(!Collection.loading){
			Collection.loading=true;
			Collection.artists=[];
			Collection.albums=[];
			Collection.songs=[];
			Collection.artistsById={};
			Collection.albumsById={};
			$.ajax({
				url: OC.linkTo('media','ajax/api.php')+'?action=get_collection',
				dataType: 'json',
				success: function(data){
					//normalize the data
					for(var i=0;i<data.artists.length;i++){
						var artist=data.artists[i];
						var artistData={name:artist.artist_name,songs:[],albums:[]};
						Collection.artistsById[artist.artist_id]=artistData;
						Collection.artists.push(artistData);
					}
					for(var i=0;i<data.albums.length;i++){
						var album=data.albums[i];
						if(Collection.artistsById[album.album_artist]){
							var artistName=Collection.artistsById[album.album_artist].name;
						}else{
							var artistName='unknown';
						}
						var albumData={name:album.album_name,artist:artistName,songs:[]};
						Collection.albumsById[album.album_id]=albumData;
						Collection.albums.push(albumData);
						if(Collection.artistsById[album.album_artist]){
							Collection.artistsById[album.album_artist].albums.push(albumData);
						}
					}
					for(var i=0;i<data.songs.length;i++){
						var song=data.songs[i];
						if(Collection.artistsById[song.song_artist] && Collection.albumsById[song.song_album]){
							var songData={
								name:song.song_name,
								artist:Collection.artistsById[song.song_artist].name,
								album:Collection.albumsById[song.song_album].name,
								lastPlayed:song.song_lastplayed,
								length:song.song_length,
								path:song.song_path,
								playCount:song.song_playcount,
							};
							Collection.songs.push(songData);
							Collection.artistsById[song.song_artist].songs.push(songData);
							Collection.albumsById[song.song_album].songs.push(songData);
						}
					}

					Collection.artists.sort(function(a,b){
						if(!a.name){
							return -1;
						}
						return a.name.localeCompare(b.name);
					});

					Collection.loaded=true;
					Collection.loading=false;
					for(var i=0;i<Collection.loadedListeners.length;i++){
						Collection.loadedListeners[i]();
					}
					if(data.songs.length==0 && initScanned == false){
						$('#scan input.start').click();
						initScanned = true;
					}
				}
			});
		}
	},
	display:function(){
		if(Collection.parent){
			Collection.parent.show();
		}
		if(!Collection.loaded){
			Collection.load(Collection.display);
		}else{
			if(Collection.parent){
				Collection.parent.find('tr:not(.template)').remove();
				var template=Collection.parent.find('tr.template');
				$.each(Collection.artists,function(i,artist){
					if(artist.name && artist.songs.length>0){
						var tr=template.clone().removeClass('template');
						if(artist.songs.length>1){
							tr.find('td.title a').text(artist.songs.length+' '+t('media','songs'));
							tr.find('td.album a').text(artist.albums.length+' '+t('media','albums'));
						}else{
							tr.find('td.title a').text(artist.songs[0].name);
							tr.find('td.album a').text(artist.albums[0].name);
						}
						tr.find('td.artist a').text(artist.name);
						tr.data('artistData',artist);
						tr.find('td.artist a').click(function(event){
							event.preventDefault();
							PlayList.add(artist);
							PlayList.play(0);
							Collection.parent.find('tr').removeClass('active');
							$('tr[data-artist="'+artist.name+'"]').addClass('active');
						});
						expander='';
						if(artist.songs.length>1){
							expander=$('>');
							expander.data('expanded',false);
							expander.click(function(event){
								var tr=$(this).parent().parent();
								if(expander.data('expanded')){
									Collection.hideArtist(tr.data('artist'));
								}else{
									Collection.showArtist(tr.data('artist'));
								}
							});
						}
						tr.find('td.artist').addClass('buttons');
						Collection.addButtons(tr,artist);
						tr.children('td.artist-expander').append(expander);
						tr.attr('data-artist',artist.name);
						Collection.parent.find('tbody').append(tr);
					}
				});
			}
		}
	},
	showArtist:function(artist){
		var tr=Collection.parent.find('tr[data-artist="'+artist+'"]');
		var lastRow=tr;
		var artist=tr.data('artistData');
		var first=true;
		$.each(artist.albums,function(j,album){
			$.each(album.songs,function(i,song){
				var newRow;
				if(first){
					newRow=tr;
				}else{
					newRow=tr.clone();
					newRow.find('td.artist').text('');
					newRow.find('.expander').remove();
				}
				newRow.find('td.album-expander .expander').remove();
				if(i==0){
					newRow.find('td.album a').text(album.name);
					newRow.find('td.album a').click(function(event){
						event.preventDefault();
						PlayList.add(album);
						PlayList.play(0);
						Collection.parent.find('tr').removeClass('active');
						$('tr[data-album="'+album.name+'"]').addClass('active');
					});
					if(album.songs.length>1){
						var expander=$('v ');
						expander.data('expanded',true);
						expander.click(function(event){
							var tr=$(this).parent().parent();
							if(expander.data('expanded')) {
								Collection.hideAlbum(tr.data('artist'),tr.data('album'));
							} else {
								Collection.showAlbum(tr.data('artist'),tr.data('album'));
							}
						});
						newRow.children('td.album-expander').append(expander);
					}
					Collection.addButtons(newRow,album);
				} else {
					newRow.find('td.album a').text('');
					Collection.addButtons(newRow,song);
				}
				newRow.find('td.title a').text(song.name);
				newRow.find('td.title a').click(function(event){
					event.preventDefault();
					PlayList.add(song);
					PlayList.play(0);
					Collection.parent.find('tr').removeClass('active');
					$('tr[data-title="'+song.name+'"]').addClass('active');
				});
				newRow.attr('data-album',album.name);
				newRow.attr('data-title',song.name);
				newRow.attr('data-artist',artist.name);
				newRow.data('albumData',album);
				if(!first){
					lastRow.after(newRow);
				}
				first=false;
				lastRow=newRow;
			});
		});
		tr.removeClass('collapsed');
		tr.find('td.artist-expander a.expander').data('expanded',true);
		tr.find('td.artist-expander a.expander').addClass('expanded');
		tr.find('td.artist-expander a.expander').text('v');
	},
	hideArtist:function(artist){
		var tr=Collection.parent.find('tr[data-artist="'+artist+'"]');
		var artist=tr.first().data('artistData');
		tr.first().find('td.album a').first().text(artist.albums.length+' '+t('media','albums'));
		tr.first().find('td.album-expander a.expander').remove();
		tr.first().find('td.title a').text(artist.songs.length+' '+t('media','songs'));
		tr.first().find('td.album a').unbind('click');
		tr.first().find('td.title a').unbind('click');
		tr.each(function(i,row){
			if(i>0){
				$(row).remove();
			}
		});
		tr.find('td.artist-expander a.expander').data('expanded',false);
		tr.find('td.artist-expander a.expander').removeClass('expanded');
		tr.find('td.artist-expander a.expander').text('>');
		Collection.addButtons(tr,artist);
	},
	showAlbum:function(artist,album){
		var tr = Collection.parent.find('tr[data-artist="'+artist+'"][data-album="'+album+'"]');
		var lastRow=tr;
		var albumData=tr.data('albumData');
		tr.find('td.album-expander a.expander').data('expanded',true);
		tr.find('td.album-expander a.expander').addClass('expanded');
		tr.find('td.album-expander a.expander').text('v');
		$.each(albumData.songs,function(i,song){
			var newRow;
			if(i>0){
				newRow=tr.clone();
				newRow.find('a.expander').remove();
				newRow.find('td.album a').text('');
				newRow.find('td.artist a').text('');
			}else{
				newRow=tr;
			}
			newRow.find('td.title a').text(song.name);
			newRow.find('td.title a').click(function(event){
				event.preventDefault();
				PlayList.add(song);
				PlayList.play(0);
				Collection.parent.find('tr').removeClass('active');
				$('tr[data-title="'+song.name+'"]').addClass('active');
			});
			if(i>0){
				lastRow.after(newRow);
			}
			lastRow=newRow;
		});
	},
	hideAlbum:function(artist,album){
		var tr = Collection.parent.find('tr[data-artist="'+artist+'"][data-album="'+album+'"]');
		var albumData=tr.data('albumData');
		tr.first().find('td.title a').text(albumData.songs.length+' '+t('media','songs'));
		tr.find('td.album-expander a.expander').data('expanded',false);
		tr.find('td.album-expander a.expander').removeClass('expanded');
		tr.find('td.album-expander a.expander').text('> ');
		tr.each(function(i,row){
			if(i>0){
				$(row).remove();
			}
		});
	},
	parent:null,
	hide:function(){
		if(Collection.parent){
			Collection.parent.hide();
		}
	},
	registerPlay:function(item){
		if(item){
			var song=Collection.findSong(item.artist,item.album,item.name);
			song.song_playcount++;
		}
	},
	addButtons:function(parent,data){
		buttons = parent.find('.buttons');
		if(buttons.find('.add').length<=0) {
			buttons.prepend('');
		}
		buttons.find('.add').unbind('click');
		buttons.find('.add').click(function(event){
			event.preventDefault();
			PlayList.add(data,true);
			PlayList.render();
		});
	},
	find:function(artistName,albumName,songName){
		if(songName){
			return Collection.findSong(artistName,albumName,songName);
		}else if(albumName){
			return Collection.findAlbum(artistName,albumName);
		}else{
			return Collection.findArtist(artistName);
		}
	},
	findArtist:function(name){
		for(var i=0;i<Collection.artists.length;i++){
			if(Collection.artists[i].name==name){
				return Collection.artists[i];
			}
		}
	},
	findAlbum:function(artistName,albumName){
		var artist=Collection.findArtist(artistName);
		if(artist){
			for(var i=0;i<artist.albums.length;i++){
				if(artist.albums[i].name==albumName){
					return artist.albums[i];
				}
			}
		}
	},
	findSong:function(artistName,albumName,songName){
		var album=Collection.findAlbum(artistName,albumName);
		if(album){
			for(var i=0;i<album.songs.length;i++){
				if(album.songs[i].name==songName){
					return album.songs[i];
				}
			}
		}
	},
	addSong:function(song){
		var artist=Collection.findArtist(song.artist);
		if(!artist){
			artist={name:song.artist,albums:[],songs:[]};
			Collection.artists.push(artist);
			Collection.artistsById[song.song_artist]=artist;
		}
		var album=Collection.findAlbum(song.artist,song.album);
		if(!album){
			album={name:song.album,artist:song.song_artist,songs:[]};
			artist.albums.push(album);
			Collection.albums.push(album);
			Collection.albumsById[song.song_album]=album;
		}
		var songData={
			name:song.song_name,
			artist:Collection.artistsById[song.song_artist].name,
			album:Collection.albumsById[song.song_album].name,
			lastPlayed:song.song_lastplayed,
			length:song.song_length,
			path:song.song_path,
			playCount:song.song_playcount,
		};
		album.songs.push(songData);
		artist.songs.push(songData);
		Collection.songs.push(songData);
	}
};

$(document).ready(function(){
	Collection.parent=$('#collection');
	Collection.load();
	Collection.parent.hide();
	$('#scan input.start').click(function(){
		$('#scan input.start').hide();
		$('#scan input.stop').show();
		$('#scan input.stop').click(function(){
			Scanner.toggle();
		});
		Scanner.scanCollection();
	});
});
<commit_msg>Variable not declared in media app, fixes bug oc-633. Thanks to die3lustigen2.<commit_after>var initScanned = false;

Collection={
	artists:[],
	albums:[],
	songs:[],
	artistsById:{},
	albumsById:{},
	loaded:false,
	loading:false,
	loadedListeners:[],
	load:function(ready){
		if(ready){
			Collection.loadedListeners.push(ready);
		}
		if(!Collection.loading){
			Collection.loading=true;
			Collection.artists=[];
			Collection.albums=[];
			Collection.songs=[];
			Collection.artistsById={};
			Collection.albumsById={};
			$.ajax({
				url: OC.linkTo('media','ajax/api.php')+'?action=get_collection',
				dataType: 'json',
				success: function(data){
					//normalize the data
					for(var i=0;i<data.artists.length;i++){
						var artist=data.artists[i];
						var artistData={name:artist.artist_name,songs:[],albums:[]};
						Collection.artistsById[artist.artist_id]=artistData;
						Collection.artists.push(artistData);
					}
					for(var i=0;i<data.albums.length;i++){
						var album=data.albums[i];
						if(Collection.artistsById[album.album_artist]){
							var artistName=Collection.artistsById[album.album_artist].name;
						}else{
							var artistName='unknown';
						}
						var albumData={name:album.album_name,artist:artistName,songs:[]};
						Collection.albumsById[album.album_id]=albumData;
						Collection.albums.push(albumData);
						if(Collection.artistsById[album.album_artist]){
							Collection.artistsById[album.album_artist].albums.push(albumData);
						}
					}
					for(var i=0;i<data.songs.length;i++){
						var song=data.songs[i];
						if(Collection.artistsById[song.song_artist] && Collection.albumsById[song.song_album]){
							var songData={
								name:song.song_name,
								artist:Collection.artistsById[song.song_artist].name,
								album:Collection.albumsById[song.song_album].name,
								lastPlayed:song.song_lastplayed,
								length:song.song_length,
								path:song.song_path,
								playCount:song.song_playcount,
							};
							Collection.songs.push(songData);
							Collection.artistsById[song.song_artist].songs.push(songData);
							Collection.albumsById[song.song_album].songs.push(songData);
						}
					}

					Collection.artists.sort(function(a,b){
						if(!a.name){
							return -1;
						}
						return a.name.localeCompare(b.name);
					});

					Collection.loaded=true;
					Collection.loading=false;
					for(var i=0;i<Collection.loadedListeners.length;i++){
						Collection.loadedListeners[i]();
					}
					if(data.songs.length==0 && initScanned == false){
						$('#scan input.start').click();
						initScanned = true;
					}
				}
			});
		}
	},
	display:function(){
		if(Collection.parent){
			Collection.parent.show();
		}
		if(!Collection.loaded){
			Collection.load(Collection.display);
		}else{
			if(Collection.parent){
				Collection.parent.find('tr:not(.template)').remove();
				var template=Collection.parent.find('tr.template');
				$.each(Collection.artists,function(i,artist){
					if(artist.name && artist.songs.length>0){
						var tr=template.clone().removeClass('template');
						if(artist.songs.length>1){
							tr.find('td.title a').text(artist.songs.length+' '+t('media','songs'));
							tr.find('td.album a').text(artist.albums.length+' '+t('media','albums'));
						}else{
							tr.find('td.title a').text(artist.songs[0].name);
							tr.find('td.album a').text(artist.albums[0].name);
						}
						tr.find('td.artist a').text(artist.name);
						tr.data('artistData',artist);
						tr.find('td.artist a').click(function(event){
							event.preventDefault();
							PlayList.add(artist);
							PlayList.play(0);
							Collection.parent.find('tr').removeClass('active');
							$('tr[data-artist="'+artist.name+'"]').addClass('active');
						});
						var expander='';
						if(artist.songs.length>1){
							expander=$('>');
							expander.data('expanded',false);
							expander.click(function(event){
								var tr=$(this).parent().parent();
								if(expander.data('expanded')){
									Collection.hideArtist(tr.data('artist'));
								}else{
									Collection.showArtist(tr.data('artist'));
								}
							});
						}
						tr.find('td.artist').addClass('buttons');
						Collection.addButtons(tr,artist);
						tr.children('td.artist-expander').append(expander);
						tr.attr('data-artist',artist.name);
						Collection.parent.find('tbody').append(tr);
					}
				});
			}
		}
	},
	showArtist:function(artist){
		var tr=Collection.parent.find('tr[data-artist="'+artist+'"]');
		var lastRow=tr;
		var artist=tr.data('artistData');
		var first=true;
		$.each(artist.albums,function(j,album){
			$.each(album.songs,function(i,song){
				var newRow;
				if(first){
					newRow=tr;
				}else{
					newRow=tr.clone();
					newRow.find('td.artist').text('');
					newRow.find('.expander').remove();
				}
				newRow.find('td.album-expander .expander').remove();
				if(i==0){
					newRow.find('td.album a').text(album.name);
					newRow.find('td.album a').click(function(event){
						event.preventDefault();
						PlayList.add(album);
						PlayList.play(0);
						Collection.parent.find('tr').removeClass('active');
						$('tr[data-album="'+album.name+'"]').addClass('active');
					});
					if(album.songs.length>1){
						var expander=$('v ');
						expander.data('expanded',true);
						expander.click(function(event){
							var tr=$(this).parent().parent();
							if(expander.data('expanded')) {
								Collection.hideAlbum(tr.data('artist'),tr.data('album'));
							} else {
								Collection.showAlbum(tr.data('artist'),tr.data('album'));
							}
						});
						newRow.children('td.album-expander').append(expander);
					}
					Collection.addButtons(newRow,album);
				} else {
					newRow.find('td.album a').text('');
					Collection.addButtons(newRow,song);
				}
				newRow.find('td.title a').text(song.name);
				newRow.find('td.title a').click(function(event){
					event.preventDefault();
					PlayList.add(song);
					PlayList.play(0);
					Collection.parent.find('tr').removeClass('active');
					$('tr[data-title="'+song.name+'"]').addClass('active');
				});
				newRow.attr('data-album',album.name);
				newRow.attr('data-title',song.name);
				newRow.attr('data-artist',artist.name);
				newRow.data('albumData',album);
				if(!first){
					lastRow.after(newRow);
				}
				first=false;
				lastRow=newRow;
			});
		});
		tr.removeClass('collapsed');
		tr.find('td.artist-expander a.expander').data('expanded',true);
		tr.find('td.artist-expander a.expander').addClass('expanded');
		tr.find('td.artist-expander a.expander').text('v');
	},
	hideArtist:function(artist){
		var tr=Collection.parent.find('tr[data-artist="'+artist+'"]');
		var artist=tr.first().data('artistData');
		tr.first().find('td.album a').first().text(artist.albums.length+' '+t('media','albums'));
		tr.first().find('td.album-expander a.expander').remove();
		tr.first().find('td.title a').text(artist.songs.length+' '+t('media','songs'));
		tr.first().find('td.album a').unbind('click');
		tr.first().find('td.title a').unbind('click');
		tr.each(function(i,row){
			if(i>0){
				$(row).remove();
			}
		});
		tr.find('td.artist-expander a.expander').data('expanded',false);
		tr.find('td.artist-expander a.expander').removeClass('expanded');
		tr.find('td.artist-expander a.expander').text('>');
		Collection.addButtons(tr,artist);
	},
	showAlbum:function(artist,album){
		var tr = Collection.parent.find('tr[data-artist="'+artist+'"][data-album="'+album+'"]');
		var lastRow=tr;
		var albumData=tr.data('albumData');
		tr.find('td.album-expander a.expander').data('expanded',true);
		tr.find('td.album-expander a.expander').addClass('expanded');
		tr.find('td.album-expander a.expander').text('v');
		$.each(albumData.songs,function(i,song){
			var newRow;
			if(i>0){
				newRow=tr.clone();
				newRow.find('a.expander').remove();
				newRow.find('td.album a').text('');
				newRow.find('td.artist a').text('');
			}else{
				newRow=tr;
			}
			newRow.find('td.title a').text(song.name);
			newRow.find('td.title a').click(function(event){
				event.preventDefault();
				PlayList.add(song);
				PlayList.play(0);
				Collection.parent.find('tr').removeClass('active');
				$('tr[data-title="'+song.name+'"]').addClass('active');
			});
			if(i>0){
				lastRow.after(newRow);
			}
			lastRow=newRow;
		});
	},
	hideAlbum:function(artist,album){
		var tr = Collection.parent.find('tr[data-artist="'+artist+'"][data-album="'+album+'"]');
		var albumData=tr.data('albumData');
		tr.first().find('td.title a').text(albumData.songs.length+' '+t('media','songs'));
		tr.find('td.album-expander a.expander').data('expanded',false);
		tr.find('td.album-expander a.expander').removeClass('expanded');
		tr.find('td.album-expander a.expander').text('> ');
		tr.each(function(i,row){
			if(i>0){
				$(row).remove();
			}
		});
	},
	parent:null,
	hide:function(){
		if(Collection.parent){
			Collection.parent.hide();
		}
	},
	registerPlay:function(item){
		if(item){
			var song=Collection.findSong(item.artist,item.album,item.name);
			song.song_playcount++;
		}
	},
	addButtons:function(parent,data){
		buttons = parent.find('.buttons');
		if(buttons.find('.add').length<=0) {
			buttons.prepend('');
		}
		buttons.find('.add').unbind('click');
		buttons.find('.add').click(function(event){
			event.preventDefault();
			PlayList.add(data,true);
			PlayList.render();
		});
	},
	find:function(artistName,albumName,songName){
		if(songName){
			return Collection.findSong(artistName,albumName,songName);
		}else if(albumName){
			return Collection.findAlbum(artistName,albumName);
		}else{
			return Collection.findArtist(artistName);
		}
	},
	findArtist:function(name){
		for(var i=0;i<Collection.artists.length;i++){
			if(Collection.artists[i].name==name){
				return Collection.artists[i];
			}
		}
	},
	findAlbum:function(artistName,albumName){
		var artist=Collection.findArtist(artistName);
		if(artist){
			for(var i=0;i<artist.albums.length;i++){
				if(artist.albums[i].name==albumName){
					return artist.albums[i];
				}
			}
		}
	},
	findSong:function(artistName,albumName,songName){
		var album=Collection.findAlbum(artistName,albumName);
		if(album){
			for(var i=0;i<album.songs.length;i++){
				if(album.songs[i].name==songName){
					return album.songs[i];
				}
			}
		}
	},
	addSong:function(song){
		var artist=Collection.findArtist(song.artist);
		if(!artist){
			artist={name:song.artist,albums:[],songs:[]};
			Collection.artists.push(artist);
			Collection.artistsById[song.song_artist]=artist;
		}
		var album=Collection.findAlbum(song.artist,song.album);
		if(!album){
			album={name:song.album,artist:song.song_artist,songs:[]};
			artist.albums.push(album);
			Collection.albums.push(album);
			Collection.albumsById[song.song_album]=album;
		}
		var songData={
			name:song.song_name,
			artist:Collection.artistsById[song.song_artist].name,
			album:Collection.albumsById[song.song_album].name,
			lastPlayed:song.song_lastplayed,
			length:song.song_length,
			path:song.song_path,
			playCount:song.song_playcount,
		};
		album.songs.push(songData);
		artist.songs.push(songData);
		Collection.songs.push(songData);
	}
};

$(document).ready(function(){
	Collection.parent=$('#collection');
	Collection.load();
	Collection.parent.hide();
	$('#scan input.start').click(function(){
		$('#scan input.start').hide();
		$('#scan input.stop').show();
		$('#scan input.stop').click(function(){
			Scanner.toggle();
		});
		Scanner.scanCollection();
	});
});
<|endoftext|>


<commit_before>"use strict";

/*global module, require*/

var _ = require("lodash"),
    d3 = require("d3"),
    sharejs = require('../node_modules/share/lib/client/index.js'),
    BCSocket = require('../node_modules/browserchannel/dist/bcsocket-uncompressed.js').BCSocket,
    helpers = require("./helpers.js"),
    isNum = helpers.isNum,
    callbacks = helpers.callbackHandler,
    projectsListFactory = require("./projects-list.js"),

    connected = "connected",
    connecting = "connecting",
    disconnected = "disconnected",
    stopped = "stopped";

/*
 Connects to a sharejs server.

 Exposes some functions about it.
 */
module.exports = function(maintainConnection, url) {
    var ensureConnected = function() {
	if (maintainConnection && isDown()) {
	    connection = new sharejs.Connection(
		new BCSocket(
		    url,
		    {
			reconnect: true
		    })
	    );

	    connection.debug = debug;

	    [connected, connecting, disconnected, stopped]
		.forEach(function(state) {
		    connection.on(state, function() {
			if (state !== lastState) {
			    lastState = state;
			    switch(state) {
			    case connected:
				onUp();
				break;
			    case connecting:
				// We don't yet know whether the server is up or down.
				break;
			    case disconnected:
			    case stopped:
				onDown();
				break;
			    default:
				throw new Error("Unknown connection state " + state);
			    }
			}
			if (isUp()) {
			    onUp();
			} else {
			    onDown();
			}
		    });
		});
	}
    },

	connection,
	onUp = callbacks(),
	onDown = callbacks(),
	lastState,

	debug = false,

	isUp = function() {
	    return connection && connection.state === connected;
	},

	/*
	 There is no connection at all, or the connection there is has fallen right over.
	 */
	isDown = function() {
	    return !connection || connection.state === stopped;
	},

	projectsList = projectsListFactory(url),

	m = {
	    search: function(coll, text, callback, errback) {
		d3.json(
		    [url, "search", coll].join("/") + "?q=" + text,
		    function(error, results) {
			if (error) {
			    errback(error);
			} else {
			    callback(_.pluck(results, "name"));
			}
		    }
		);
	    },

	    /*
	     Gets a document.
	     */
	    load: function(coll, name, callback) {
		ensureConnected();
		var doc = connection.get(coll, name.toLowerCase());
		if (!doc.subscribed) {
		    doc.subscribe();
		}
		doc.whenReady(function() {
		    callback(doc);
		});
	    },

	    /*
	     Gets a document snapshot without loading the whole document.

	     Used when we are in offline mode.
	     */
	    loadSnapshot: function(coll, name, callback, errback) {
		d3.json(
		    [url, "current", coll, name].join("/"),
		    function(error, json) {
			if (error) {
			    errback(error.response);
			} else if (json.data) {
			    callback(json.data);
			}
		    }
		);
	    },

	    /*
	     Object returns has 3 properties:
	     doc - the deserialized json snapshot
	     v - the version of that snapshot
	     latestV - the most recent version of the document available
	     */
	    loadVersion: function(coll, name, version, callback, errback) {
		if (!isNum(version)) {
		    throw new Error("Not a valid version " + version);
		}

		d3.json(
		    [url, "history", coll, name, version].join("/"),
		    function(error, json) {
			if (error) {
			    errback(error.response);
			} else {
			    callback(json);
			}
		    }
		);
	    },

	    getVersionsList: function(coll, name, versionsFrom, callback, errback) {
		d3.json(
		    [url, "versions", coll, name, versionsFrom].join("/"),
		    function(error, json) {
			if (error) {
			    errback(error.response);
			} else {
			    callback(json);
			}
		    }
		);
	    },

	    getProjectsList: function() {
		return projectsList.get();
	    },

	    deleteDoc: function(coll, name) {
		ensureConnected();
		var toDelete = connection.get(coll, name.toLowerCase());
		if (!toDelete.subscribed) {
		    toDelete.subscribe();
		}
		toDelete.whenReady(function() {
		    toDelete.del();
		    toDelete.destroy();
		});
	    },

	    onUp: onUp.add,
	    onDown: onDown.add,
	    isUp: isUp,

	    isDebugging: function() {
		return debug;
	    },

	    setDebug: function(value) {
		if (debug !== !!value) {
		    debug = !!value;
		    if (connection) {
			connection.disconnect();
		    }
		    ensureConnected();
		}
	    }
	};

    if (maintainConnection) {
	ensureConnected();
    }

    return m;
};
<commit_msg>Improved error handling.<commit_after>"use strict";

/*global module, require*/

var _ = require("lodash"),
    d3 = require("d3"),
    sharejs = require('../node_modules/share/lib/client/index.js'),
    BCSocket = require('../node_modules/browserchannel/dist/bcsocket-uncompressed.js').BCSocket,
    helpers = require("./helpers.js"),
    isNum = helpers.isNum,
    callbacks = helpers.callbackHandler,
    projectsListFactory = require("./projects-list.js"),

    connected = "connected",
    connecting = "connecting",
    disconnected = "disconnected",
    stopped = "stopped";

/*
 Connects to a sharejs server.

 Exposes some functions about it.
 */
module.exports = function(maintainConnection, url) {
    var ensureConnected = function() {
	if (maintainConnection && isDown()) {
	    connection = new sharejs.Connection(
		new BCSocket(
		    url,
		    {
			reconnect: true
		    })
	    );

	    connection.debug = debug;

	    [connected, connecting, disconnected, stopped]
		.forEach(function(state) {
		    connection.on(state, function() {
			if (state !== lastState) {
			    lastState = state;
			    switch(state) {
			    case connected:
				onUp();
				break;
			    case connecting:
				// We don't yet know whether the server is up or down.
				break;
			    case disconnected:
			    case stopped:
				onDown();
				break;
			    default:
				throw new Error("Unknown connection state " + state);
			    }
			}
			if (isUp()) {
			    onUp();
			} else {
			    onDown();
			}
		    });
		});
	}
    },

	connection,
	onUp = callbacks(),
	onDown = callbacks(),
	lastState,

	debug = false,

	isUp = function() {
	    return connection && connection.state === connected;
	},

	/*
	 There is no connection at all, or the connection there is has fallen right over.
	 */
	isDown = function() {
	    return !connection || connection.state === stopped;
	},

	projectsList = projectsListFactory(url),

	handleError = function(error, errback) {
	    if (errback) {
		errback(error.response);
	    } else {
		console.error(error.response);
	    }
	},

	m = {
	    search: function(coll, text, callback, errback) {
		d3.json(
		    [url, "search", coll].join("/") + "?q=" + text,
		    function(error, results) {
			if (error) {
			    handleError(error, errback);
			} else {
			    callback(_.pluck(results, "name"));
			}
		    }
		);
	    },

	    /*
	     Gets a document.
	     */
	    load: function(coll, name, callback) {
		ensureConnected();
		var doc = connection.get(coll, name.toLowerCase());
		if (!doc.subscribed) {
		    doc.subscribe();
		}
		doc.whenReady(function() {
		    callback(doc);
		});
	    },

	    /*
	     Gets a document snapshot without loading the whole document.

	     Used when we are in offline mode.
	     */
	    loadSnapshot: function(coll, name, callback, errback) {
		d3.json(
		    [url, "current", coll, name].join("/"),
		    function(error, json) {
			if (error) {
			    handleError(error, errback);

			} else if (json.data) {
			    callback(json.data);
			}
		    }
		);
	    },

	    /*
	     Object returns has 3 properties:
	     doc - the deserialized json snapshot
	     v - the version of that snapshot
	     latestV - the most recent version of the document available
	     */
	    loadVersion: function(coll, name, version, callback, errback) {
		if (!isNum(version)) {
		    throw new Error("Not a valid version " + version);
		}

		d3.json(
		    [url, "history", coll, name, version].join("/"),
		    function(error, json) {
			if (error) {
			    handleError(error, errback);

			} else {
			    callback(json);
			}
		    }
		);
	    },

	    getVersionsList: function(coll, name, versionsFrom, callback, errback) {
		d3.json(
		    [url, "versions", coll, name, versionsFrom].join("/"),
		    function(error, json) {
			if (error) {
			    handleError(error, errback);

			} else {
			    callback(json);
			}
		    }
		);
	    },

	    getProjectsList: function() {
		return projectsList.get();
	    },

	    deleteDoc: function(coll, name) {
		ensureConnected();
		var toDelete = connection.get(coll, name.toLowerCase());
		if (!toDelete.subscribed) {
		    toDelete.subscribe();
		}
		toDelete.whenReady(function() {
		    toDelete.del();
		    toDelete.destroy();
		});
	    },

	    onUp: onUp.add,
	    onDown: onDown.add,
	    isUp: isUp,

	    isDebugging: function() {
		return debug;
	    },

	    setDebug: function(value) {
		if (debug !== !!value) {
		    debug = !!value;
		    if (connection) {
			connection.disconnect();
		    }
		    ensureConnected();
		}
	    }
	};

    if (maintainConnection) {
	ensureConnected();
    }

    return m;
};
<|endoftext|>


<commit_before>import React from 'react';
import renderer from 'react-test-renderer';
import ReactTestUtils from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';

import { KEY_CODES } from '../../utils/values';
import removeAccents from '../../utils/remove-accents';
import Ssuggestor, { Suggestor } from '../Suggestor';

jest.mock('../../utils/remove-accents', () => {
	return jest.fn(text => text);
});

const PROPS = {
	list: ['temporise', 'whencesoeve', 'turophile', 'umlaut'],
	reference: jest.fn(),
	onChange: jest.fn(),
	onSelect: jest.fn(),
	onKey: jest.fn(),
	value: '',
	openOnClick: false,
	selectOnTab: false,
	placeholder: 'placeholder text',
	tooltip: 'tooltip text',
	className: 'classname to apply',
	suggestOn: 1,
	style: { color: '#eee' },
	required: false,
	useKeys: false,
	accents: false,
	arrow: false,
	close: false
};

describe('<Ssuggestor />', () => {
	it('snapshot - without suggestions', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} list={[]} />).toJSON();
		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with suggestions', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} />).toJSON();
		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with suggestions visible', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} openOnClick />);

		tree.getInstance().wrapped.setState({ open: true });

		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with arrow', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} arrow />);

		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with close', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} close />);

		tree.getInstance().wrapped.setState({ value: 'temp' });

		expect(tree).toMatchSnapshot();
	});
});

describe('Suggestor component', () => {
	beforeEach(() => {
		PROPS.onChange.mockReset();
		PROPS.onSelect.mockReset();
	});

	it('should initialize state.open as false', () => {
		const component = mount(<Suggestor {...PROPS} />);

		expect(component.state().open).toBeFalsy();
	});

	it('should filter list into initial state ', () => {
		const component = mount(<Suggestor {...PROPS} />);

		expect(component.state()).toMatchObject({
			filtered: [
				{ word: 'temporise', index: 0 },
				{ word: 'whencesoeve', index: 0 },
				{ word: 'turophile', index: 0 },
				{ word: 'umlaut', index: 0 }
			],
			value: PROPS.value,
			index: 0
		});
	});

	it('should call changeValue when remove the value: remove -> changeValue', () => {
		const component = mount(<Suggestor {...PROPS} openOnClick />);

		const spy = jest.spyOn(component.instance(), 'changeValue');

		component.instance().remove();

		expect(spy).toBeCalled();
	});

	it('handleItemMouseEnter -> setState', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'setState');
		const index = 2;

		component.instance().handleItemMouseEnter(index);

		expect(spy).toBeCalledWith({ index });
	});

	it('handleItemClick -> changeValue', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'changeValue');
		const payload = { word: 'decrassify' };

		component.instance().handleItemClick(payload);

		expect(spy).toBeCalledWith(payload.word, true);
	});

	it('focus -> input.focus', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance().input, 'focus');

		component.instance().focus();

		expect(spy).toBeCalled();
	});

	it('changeValue -> [setState, props.onChange, props.onSelect]', () => {
		const component = shallow(<Suggestor {...PROPS} />);
		const instance = component.instance();

		const spies = {
			setState: jest.spyOn(instance, 'setState'),
			handleClose: jest.spyOn(instance, 'handleClose')
		};

		const value = 'umlaut';
		instance.changeValue(value, true);

		expect(spies.setState).toBeCalled();
		expect(spies.handleClose).toBeCalledWith();
		expect(PROPS.onChange).toBeCalledWith(value);
		expect(PROPS.onSelect).toBeCalledWith(value);
	});

	it('handleClickOut -> handleClose', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'handleClose');

		component.instance().handleClickOut();

		expect(spy).toBeCalled();
	});

	describe('componentWillReceiveProps', () => {
		it('update state.value if props.value changed', () => {
			const component = shallow(<Suggestor {...PROPS} />);

			expect(component.state()).toMatchObject({ value: '' });

			const value = 'xesturgy';

			component.setProps({ value });

			expect(component.state()).toMatchObject({ value });
		});

		it('do not update state.value if props.value match with state.value', () => {
			const component = shallow(<Suggestor {...PROPS} />);

			const value = 'hame';
			component.setState({ value });

			const spy = jest.spyOn(component.instance(), 'setState');

			component.setProps({ value });

			expect(spy).not.toBeCalled();
		});
	});

	describe('handleClick function', () => {
		it('should not call toggleList (when openOnClick prop is falsy)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const spy = jest.spyOn(component.instance(), 'toggleList');

			component.instance().handleClick();

			expect(spy).not.toBeCalled();
		});

		it('handleClick -> toggleList (if openOnClick prop is truthy)', () => {
			const component = mount(<Suggestor {...PROPS} openOnClick />);

			const spy = jest.spyOn(component.instance(), 'toggleList');

			component.instance().handleClick();

			expect(spy).toBeCalled();
		});
	});

	describe('... handles input changes', () => {
		const event = {
			stopPropagation: jest.fn(),
			target: { value: 'temp' }
		};

		beforeEach(() => {
			event.stopPropagation.mockReset();
			PROPS.onChange.mockReset();
		});

		it('should stop event propagation', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.find('input').simulate('change', event);

			expect(event.stopPropagation).toBeCalled();
		});

		it('should update state', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.find('input').simulate('change', event);

			expect(component.state().value).toBe(event.target.value);
			expect(component.state().open).toBeTruthy();
		});

		it('should call onChange prop callback', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.find('input').simulate('change', event);

			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).not.toBeCalled();
		});

		it('should call onSelect prop callback when select value', () => {
			const component = mount(<Suggestor {...PROPS} openOnClick />);

			component.instance().changeValue('temp', true);

			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).toBeCalled();
		});

		it('should call handleClose when select value', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const spy = jest.spyOn(component.instance(), 'handleClose');
			component.update();

			component.instance().changeValue('temp', true);

			expect(spy).toBeCalled();
		});

		it('should call handleClose when select value (2)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const spy = jest.spyOn(component.instance(), 'handleClose');

			component.instance().changeValue('no match!');

			expect(spy).toBeCalled();
		});
	});

	describe('... handles keys', () => {
		const event = {
			preventDefault: jest.fn(),
			keyCode: KEY_CODES.ENTER
		};

		beforeEach(() => {
			event.preventDefault.mockReset();
			PROPS.onKey.mockReset();
			PROPS.onChange.mockReset();
			PROPS.onSelect.mockReset();
		});

		it('should call prop onKey callback', () => {
			const component = shallow(<Suggestor {...PROPS} />);

			component.find('div').simulate('keyDown', event);

			expect(event.preventDefault).not.toBeCalled();
			expect(PROPS.onKey).toBeCalled();
		});

		it('should prevent event default action (with keys: ENTER, ESCAPE, UP, DOWN)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);

			expect(event.preventDefault).not.toBeCalled();

			const keys = [KEY_CODES.ENTER, KEY_CODES.ESCAPE, KEY_CODES.UP, KEY_CODES.DOWN];

			keys.map((keyCode, index) => {
				component.find('div').simulate('keyDown', { ...event, keyCode });

				expect(event.preventDefault).toHaveBeenCalledTimes(index + 1);
			});

			expect(event.preventDefault).toBeCalled();
		});

		it('should not prevent event default action (with key: TAB)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.TAB });

			expect(event.preventDefault).not.toBeCalled();
		});

		it('should only close suggestion list - if selectOnTab is off (TAB key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);

			const changeValueSpy = jest.spyOn(component.instance(), 'changeValue');
			const handleCloseSpy = jest.spyOn(component.instance(), 'handleClose');

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.TAB });

			expect(changeValueSpy).not.toBeCalled();
			expect(handleCloseSpy).toBeCalled();
		});

		it('should change value (TAB key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys selectOnTab />);
			component.setState({ open: true });

			const changeValueSpy = jest.spyOn(component.instance(), 'changeValue');
			const handleCloseSpy = jest.spyOn(component.instance(), 'handleClose');

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.TAB });

			expect(changeValueSpy).toBeCalled();
			expect(handleCloseSpy).toBeCalled();
			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).toBeCalled();
		});

		it('should clear selected value if any and suggestion list is closed (ESC key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys selectOnTab />);
			component.setState({ open: false, value: 'temporise' });

			const changeValueSpy = jest.spyOn(component.instance(), 'changeValue');
			const handleCloseSpy = jest.spyOn(component.instance(), 'handleClose');

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.ESCAPE });

			expect(handleCloseSpy).toBeCalled();
			expect(changeValueSpy).toBeCalledWith('');
			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).not.toBeCalled(); // check actual behavior
		});

		it('should change value (ENTER key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);
			const instance = component.instance();

			component.setState({ open: true, index: 0 });

			const spies = {
				changeValue: jest.spyOn(instance, 'changeValue'),
				toggleList: jest.spyOn(instance, 'toggleList')
			};

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.ENTER });

			const selectedItem = PROPS.list[0];

			expect(PROPS.onKey).toBeCalled();
			expect(spies.toggleList).toBeCalled();
			expect(spies.changeValue).toBeCalledWith(selectedItem, true);
			expect(PROPS.onSelect).toBeCalledWith(selectedItem);
			expect(event.preventDefault).toBeCalled();

		});

		it('should only call props.onKey for unsupported keys', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);
			const instance = component.instance();

			const spies = {
				changeValue: jest.spyOn(instance, 'changeValue'),
				handleClose: jest.spyOn(instance, 'handleClose'),
				setState: jest.spyOn(instance, 'setState'),
				toggleList: jest.spyOn(instance, 'toggleList')
			};

			component.find('div').simulate('keyDown', { ...event, keyCode: 0 });

			expect(event.preventDefault).not.toBeCalled();
			expect(spies.changeValue).not.toBeCalled();
			expect(spies.handleClose).not.toBeCalled();
			expect(spies.setState).not.toBeCalled();
			expect(spies.toggleList).not.toBeCalled();
			expect(PROPS.onSelect).not.toBeCalled();
			expect(PROPS.onKey).toBeCalled();
		});
	});

	describe('filter', () => {
		beforeEach(() => {
			removeAccents.mockClear();
		});

		it('should call removeAccents (if accents not allowed)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.instance().filter('illaudable');

			expect(removeAccents).toBeCalled();
		});

		it('should not call removeAccents (if accents support)', () => {
			const component = mount(<Suggestor {...PROPS} accents />);

			component.instance().filter('illaudable');

			expect(removeAccents).not.toBeCalled();
		});

		it('should return all item on suggestion list (if onlyMatch arg set to falsy)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const value = 'temporise';

			const result = component.instance().filter(value, false);

			expect(result.length).toBe(4);

			const [first, ...tail] = result;

			expect(first).toEqual({
				word: value,
				index: 0
			});
			expect(tail.every(item => item.index === -1)).toBeTruthy();
		});

		it('should return only matches from suggestion list', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const result = component.instance().filter('temporise');

			expect(result.length).toBe(1);
			expect(result[0].index).toBeGreaterThanOrEqual(0);
		});

		it('should return all if search pattern is empty', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const result = component.instance().filter();

			expect(result.every(item => item.index === 0)).toBeTruthy();
			expect(result.length).toBe(4);
		});
	});
});

test('Suggestor renders suggestion list closed', () => {
	const component = ReactTestUtils.renderIntoDocument(<Ssuggestor {...PROPS} />);

	expect(component.wrapped.state.open).toBeFalsy();
});
<commit_msg><suggestor> coverage update * statements, branch, functions, lines -> 100% cov.<commit_after>import React from 'react';
import renderer from 'react-test-renderer';
import ReactTestUtils from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';

import { KEY_CODES } from '../../utils/values';
import removeAccents from '../../utils/remove-accents';
import Ssuggestor, { Suggestor } from '../Suggestor';

jest.mock('../../utils/remove-accents', () => {
	return jest.fn(text => text);
});

const PROPS = {
	list: ['temporise', 'whencesoeve', 'turophile', 'umlaut'],
	reference: jest.fn(),
	onChange: jest.fn(),
	onSelect: jest.fn(),
	onKey: jest.fn(),
	value: '',
	openOnClick: false,
	selectOnTab: false,
	placeholder: 'placeholder text',
	tooltip: 'tooltip text',
	className: 'classname to apply',
	suggestOn: 1,
	style: { color: '#eee' },
	required: false,
	useKeys: false,
	accents: false,
	arrow: false,
	close: false
};

describe('<Ssuggestor />', () => {
	it('snapshot - without suggestions', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} list={[]} />).toJSON();
		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with suggestions', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} />).toJSON();
		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with suggestions visible', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} openOnClick />);

		tree.getInstance().wrapped.setState({ open: true });

		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with arrow', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} arrow />);

		expect(tree).toMatchSnapshot();
	});

	it('snapshot - with close', () => {
		const tree = renderer.create(<Ssuggestor {...PROPS} close />);

		tree.getInstance().wrapped.setState({ value: 'temp' });

		expect(tree).toMatchSnapshot();
	});
});

describe('Suggestor component', () => {
	beforeEach(() => {
		PROPS.onChange.mockReset();
		PROPS.onSelect.mockReset();
	});

	it('should initialize state.open as false', () => {
		const component = mount(<Suggestor {...PROPS} />);

		expect(component.state().open).toBeFalsy();
	});

	it('should filter list into initial state ', () => {
		const component = mount(<Suggestor {...PROPS} />);

		expect(component.state()).toMatchObject({
			filtered: [
				{ word: 'temporise', index: 0 },
				{ word: 'whencesoeve', index: 0 },
				{ word: 'turophile', index: 0 },
				{ word: 'umlaut', index: 0 }
			],
			value: PROPS.value,
			index: 0
		});
	});

	it('should call _bind when created', () => {
		const spy = jest.spyOn(Suggestor.prototype, '_bind');

		const component = shallow(<Suggestor {...PROPS} />);

		expect(spy).toHaveBeenCalled();
	});

	it('should call changeValue when remove the value: remove -> changeValue', () => {
		const component = mount(<Suggestor {...PROPS} openOnClick />);

		const spy = jest.spyOn(component.instance(), 'changeValue');

		component.instance().remove();

		expect(spy).toBeCalled();
	});

	it('handleItemMouseEnter -> setState', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'setState');
		const index = 2;

		component.instance().handleItemMouseEnter(index);

		expect(spy).toBeCalledWith({ index });
	});

	it('handleItemClick -> changeValue', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'changeValue');
		const payload = { word: 'decrassify' };

		component.instance().handleItemClick(payload);

		expect(spy).toBeCalledWith(payload.word, true);
	});

	it('focus -> input.focus', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance().input, 'focus');

		component.instance().focus();

		expect(spy).toBeCalled();
	});

	it('changeValue -> [setState, props.onChange, props.onSelect]', () => {
		const component = shallow(<Suggestor {...PROPS} />);
		const instance = component.instance();

		const spies = {
			setState: jest.spyOn(instance, 'setState'),
			handleClose: jest.spyOn(instance, 'handleClose')
		};

		const value = 'umlaut';
		instance.changeValue(value, true);

		expect(spies.setState).toBeCalled();
		expect(spies.handleClose).toBeCalledWith();
		expect(PROPS.onChange).toBeCalledWith(value);
		expect(PROPS.onSelect).toBeCalledWith(value);
	});

	it('handleClickOut -> handleClose', () => {
		const component = mount(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'handleClose');

		component.instance().handleClickOut();

		expect(spy).toBeCalled();
	});

	it('toggleList -> setState (open suggestion list)', () => {
		const component = shallow(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'setState');
		expect(component.state().open).toBeFalsy();

		component.instance().toggleList();

		expect(spy).toBeCalled();
		expect(component.state().open).toBeTruthy();
	});

	it('toggleList -> handleClose (if suggestion list is visible)', () => {
		const component = shallow(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'handleClose');

		component.instance().toggleList();

		expect(spy).not.toBeCalled();

		component.instance().toggleList();

		expect(spy).toBeCalled();
	});

	it('handleChange -> changeValue', () => {
		const event = {
			stopPropagation: jest.fn(),
			target: {
				value: 'whencesoeve'
			}
		};
		const component = shallow(<Suggestor {...PROPS} />);

		const spy = jest.spyOn(component.instance(), 'changeValue');

		component.instance().handleChange(event);

		expect(spy).toBeCalled();
		expect(component.state()).toMatchObject({
			value: event.target.value,
			open: true
		});
	});

	describe('componentWillReceiveProps', () => {
		it('update state.value if props.value changed', () => {
			const component = shallow(<Suggestor {...PROPS} />);

			expect(component.state()).toMatchObject({ value: '' });

			const value = 'xesturgy';

			component.setProps({ value });

			expect(component.state()).toMatchObject({ value });
		});

		it('do not update state.value if props.value match with state.value', () => {
			const component = shallow(<Suggestor {...PROPS} />);

			const value = 'hame';
			component.setState({ value });

			const spy = jest.spyOn(component.instance(), 'setState');

			component.setProps({ value });

			expect(spy).not.toBeCalled();
		});
	});

	describe('handleClick function', () => {
		it('should not call toggleList (when openOnClick prop is falsy)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const spy = jest.spyOn(component.instance(), 'toggleList');

			component.instance().handleClick();

			expect(spy).not.toBeCalled();
		});

		it('handleClick -> toggleList (if openOnClick prop is truthy)', () => {
			const component = mount(<Suggestor {...PROPS} openOnClick />);

			const spy = jest.spyOn(component.instance(), 'toggleList');

			component.instance().handleClick();

			expect(spy).toBeCalled();
		});
	});

	describe('... handles input changes', () => {
		const event = {
			stopPropagation: jest.fn(),
			target: { value: 'temp' }
		};

		beforeEach(() => {
			event.stopPropagation.mockReset();
			PROPS.onChange.mockReset();
		});

		it('should stop event propagation', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.find('input').simulate('change', event);

			expect(event.stopPropagation).toBeCalled();
		});

		it('should update state', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.find('input').simulate('change', event);

			expect(component.state().value).toBe(event.target.value);
			expect(component.state().open).toBeTruthy();
		});

		it('should call onChange prop callback', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.find('input').simulate('change', event);

			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).not.toBeCalled();
		});

		it('should call onSelect prop callback when select value', () => {
			const component = mount(<Suggestor {...PROPS} openOnClick />);

			component.instance().changeValue('temp', true);

			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).toBeCalled();
		});

		it('should call handleClose when select value', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const spy = jest.spyOn(component.instance(), 'handleClose');
			component.update();

			component.instance().changeValue('temp', true);

			expect(spy).toBeCalled();
		});

		it('should call handleClose when select value (2)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const spy = jest.spyOn(component.instance(), 'handleClose');

			component.instance().changeValue('no match!');

			expect(spy).toBeCalled();
		});
	});

	describe('... handles keys', () => {
		const event = {
			preventDefault: jest.fn(),
			keyCode: KEY_CODES.ENTER
		};

		beforeEach(() => {
			event.preventDefault.mockReset();
			PROPS.onKey.mockReset();
			PROPS.onChange.mockReset();
			PROPS.onSelect.mockReset();
		});

		it('should call prop onKey callback', () => {
			const component = shallow(<Suggestor {...PROPS} />);

			component.find('div').simulate('keyDown', event);

			expect(event.preventDefault).not.toBeCalled();
			expect(PROPS.onKey).toBeCalled();
		});

		it('should prevent event default action (with keys: ENTER, ESCAPE, UP, DOWN)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);

			expect(event.preventDefault).not.toBeCalled();

			const keys = [KEY_CODES.ENTER, KEY_CODES.ESCAPE, KEY_CODES.UP, KEY_CODES.DOWN];

			keys.map((keyCode, index) => {
				component.find('div').simulate('keyDown', { ...event, keyCode });

				expect(event.preventDefault).toHaveBeenCalledTimes(index + 1);
			});

			expect(event.preventDefault).toBeCalled();
		});

		it('should not prevent event default action (with key: TAB)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.TAB });

			expect(event.preventDefault).not.toBeCalled();
		});

		it('should only close suggestion list - if selectOnTab is off (TAB key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);

			const changeValueSpy = jest.spyOn(component.instance(), 'changeValue');
			const handleCloseSpy = jest.spyOn(component.instance(), 'handleClose');

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.TAB });

			expect(changeValueSpy).not.toBeCalled();
			expect(handleCloseSpy).toBeCalled();
		});

		it('should change value (TAB key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys selectOnTab />);
			component.setState({ open: true });

			const changeValueSpy = jest.spyOn(component.instance(), 'changeValue');
			const handleCloseSpy = jest.spyOn(component.instance(), 'handleClose');

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.TAB });

			expect(changeValueSpy).toBeCalled();
			expect(handleCloseSpy).toBeCalled();
			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).toBeCalled();
		});

		it('should clear selected value if any and suggestion list is closed (ESC key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys selectOnTab />);
			component.setState({ open: false, value: 'temporise' });

			const changeValueSpy = jest.spyOn(component.instance(), 'changeValue');
			const handleCloseSpy = jest.spyOn(component.instance(), 'handleClose');

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.ESCAPE });

			expect(handleCloseSpy).toBeCalled();
			expect(changeValueSpy).toBeCalledWith('');
			expect(PROPS.onChange).toBeCalled();
			expect(PROPS.onSelect).not.toBeCalled(); // check actual behavior
		});

		it('should change value (ENTER key)', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);
			const instance = component.instance();

			component.setState({ open: true, index: 0 });

			const spies = {
				changeValue: jest.spyOn(instance, 'changeValue'),
				toggleList: jest.spyOn(instance, 'toggleList')
			};

			component.find('div').simulate('keyDown', { ...event, keyCode: KEY_CODES.ENTER });

			const selectedItem = PROPS.list[0];

			expect(PROPS.onKey).toBeCalled();
			expect(spies.toggleList).toBeCalled();
			expect(spies.changeValue).toBeCalledWith(selectedItem, true);
			expect(PROPS.onSelect).toBeCalledWith(selectedItem);
			expect(event.preventDefault).toBeCalled();

		});

		it('should only call props.onKey for unsupported keys', () => {
			const component = shallow(<Suggestor {...PROPS} useKeys />);
			const instance = component.instance();

			const spies = {
				changeValue: jest.spyOn(instance, 'changeValue'),
				handleClose: jest.spyOn(instance, 'handleClose'),
				setState: jest.spyOn(instance, 'setState'),
				toggleList: jest.spyOn(instance, 'toggleList')
			};

			component.find('div').simulate('keyDown', { ...event, keyCode: 0 });

			expect(event.preventDefault).not.toBeCalled();
			expect(spies.changeValue).not.toBeCalled();
			expect(spies.handleClose).not.toBeCalled();
			expect(spies.setState).not.toBeCalled();
			expect(spies.toggleList).not.toBeCalled();
			expect(PROPS.onSelect).not.toBeCalled();
			expect(PROPS.onKey).toBeCalled();
		});
	});

	describe('filter', () => {
		beforeEach(() => {
			removeAccents.mockClear();
		});

		it('should call removeAccents (if accents not allowed)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			component.instance().filter('illaudable');

			expect(removeAccents).toBeCalled();
		});

		it('should not call removeAccents (if accents support)', () => {
			const component = mount(<Suggestor {...PROPS} accents />);

			component.instance().filter('illaudable');

			expect(removeAccents).not.toBeCalled();
		});

		it('should return all item on suggestion list (if onlyMatch arg set to falsy)', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const value = 'temporise';

			const result = component.instance().filter(value, false);

			expect(result.length).toBe(4);

			const [first, ...tail] = result;

			expect(first).toEqual({
				word: value,
				index: 0
			});
			expect(tail.every(item => item.index === -1)).toBeTruthy();
		});

		it('should return only matches from suggestion list', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const result = component.instance().filter('temporise');

			expect(result.length).toBe(1);
			expect(result[0].index).toBeGreaterThanOrEqual(0);
		});

		it('should return all if search pattern is empty', () => {
			const component = mount(<Suggestor {...PROPS} />);

			const result = component.instance().filter();

			expect(result.every(item => item.index === 0)).toBeTruthy();
			expect(result.length).toBe(4);
		});
	});
});

describe('Suggestor noop func', () => {
	it('when not onChange, onSelect, oKey props func provided, then use noop', () => {
		const { onChange, onSelect, onKey, ...props } = PROPS;
		const component = shallow(<Suggestor {...props} />);

		expect(component.props().onChange).toBe(component.props().onSelect);
		expect(component.props().onSelect).toBe(component.props().onKey);

		expect(component.instance().props.onSelect()).toBe(undefined);
	});
});

test('Suggestor renders suggestion list closed', () => {
	const component = ReactTestUtils.renderIntoDocument(<Ssuggestor {...PROPS} />);

	expect(component.wrapped.state.open).toBeFalsy();
});
<|endoftext|>


<commit_before>(function() {
  "use strict";
  var bannerWidth = 300;
  var bannerHeight = 100;
  var content = null;
  var placeholder = '
';
  var bannersPerWindow = 0;
  var bannersPerLine = 0;
  var bannersPerColumn = 0;
  var jpg = [0, 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 28, 29, 33, 38, 39, 43, 44, 45, 46, 47, 52, 54, 57, 59, 60, 61, 64, 66, 67, 69, 71, 72, 76, 77, 81, 82, 83, 84, 88, 90, 91, 96, 98, 99, 100, 104, 106, 116, 119, 137, 140, 148, 149, 150, 154, 156, 157, 158, 159, 161, 162, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 178, 179, 180, 181, 182, 183, 186, 189, 190, 192, 193, 194, 197, 198, 200, 201, 202, 203, 205, 206, 207, 208, 210, 213, 214, 215, 216, 218, 219, 220, 221, 222, 223, 224, 227];
  var gif = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 28, 29, 30, 33, 34, 35, 36, 37, 39, 40, 42, 44, 45, 46, 48, 50, 52, 54, 55, 57, 58, 59, 60, 61, 63, 64, 66, 67, 68, 69, 70, 72, 73, 75, 76, 77, 78, 80, 81, 82, 83, 86, 87, 88, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 109, 110, 111, 112, 113, 115, 116, 117, 118, 119, 120, 122, 123, 124, 127, 129, 130, 131, 134, 135, 136, 138, 139, 141, 144, 146, 148, 149, 153, 154, 155, 157, 158, 159, 160, 161, 162, 164, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 181, 182, 183, 185, 186, 187, 188, 189, 190, 191, 192, 193, 195, 196, 197, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 219, 220, 221, 222, 224, 225, 226, 227, 228, 230, 232, 233, 234, 235, 238, 240, 241, 243, 244, 245, 246, 247, 249, 250, 251, 253];
  var png = [0, 1, 2, 3, 5, 6, 9, 10, 11, 12, 14, 16, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 37, 39, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 57, 58, 59, 64, 66, 67, 68, 69, 70, 71, 72, 76, 78, 81, 82, 85, 86, 87, 89, 95, 98, 100, 101, 102, 105, 106, 107, 109, 110, 111, 112, 113, 114, 115, 116, 118, 119, 120, 121, 122, 123, 126, 128, 130, 134, 136, 138, 139, 140, 142, 145, 146, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 178, 179, 180, 181, 182, 184, 186, 188, 190, 192, 193, 194, 195, 196, 197, 198, 200, 202, 203, 205, 206, 207, 209, 212, 213, 214, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 229, 231, 232, 233, 234, 235, 237, 238, 239, 240, 241, 242, 244, 245, 246, 247, 248, 249, 250, 253, 254, 255, 256, 257, 258, 259, 260, 262, 268];
  var indexPointers = [0, 0, 0];
  var maxIndices = [jpg[jpg.length - 1], gif[gif.length - 1], png[png.length - 1]];
  var extensions = ["jpg", "gif", "png"];
  var extensionPointer = 0;
  var maxExtension = extensions.length;
  var baseUrl = "//s.4cdn.org/image/title/";
  var downCount = 0;
  var allTheWayFor = [false, false, false];
  var allTheWayDown = false;
  var okayToScroll = true;
  var scrollTimeout = 500;
  var scrollTimeoutContainer = null;

  function resetStuff() {
    content.innerHTML = "";
    content.style.top = 0;
    indexPointers = [0, 0, 0];
    extensionPointer = 0;
    downCount = 0;
    okayToScroll = true;
    allTheWayFor = [false, false, false];
    allTheWayDown = false;
    clearTimeout(scrollTimeoutContainer);
    scrollTimeoutContainer = null;
    computeStuff();
    doFirst();
  }

  function computeStuff() {
    bannersPerLine = Math.floor(window.innerWidth / bannerWidth);
    bannersPerColumn = Math.floor(window.innerHeight / bannerHeight);
    bannersPerWindow = bannersPerLine * bannersPerColumn;
  }

  function onKeyDown(e) {
    if (e.ctrlKey && e.keyCode === 38) {
      resetStuff();
    }
    else if (e.keyCode === 40 && okayToScroll) {
      if (!allTheWayDown) {
        scrollTimeoutContainer = setTimeout(makeScrollOkay, scrollTimeout);
      }
      okayToScroll = false;
      scrollMore();
    }
  }

  function makeScrollOkay() {
    okayToScroll = true;
    scrollTimeoutContainer = null;
  }

  function scrollMore() {
    downCount++;
    content.style.top = "-" + downCount * bannersPerColumn * bannerHeight + "px";
    var imgs = document.getElementsByTagName("img");
    for (var elem of imgs) {
      imgs.innerHTML = placeholder;
    }
    for (var i = 0; i < bannersPerWindow; i++) {
       if (!getBanner()) {
         if (checkAllTheWayDown()) {
          break;
        }
        i--;
       }
    }
  }

  function onResize() {
    window.removeEventListener("resize", onResize);
    alert("Resizing the window breaks this page.");
  }

  function onScroll() {
    if (okayToScroll) {
      if (!allTheWayDown) {
        scrollTimeoutContainer = setTimeout(makeScrollOkay, scrollTimeout);
      }
      okayToScroll = false;
      scrollMore();
    }
  }

  function initialize() {
    document.removeEventListener("DOMContentLoaded", initialize);
    content = document.getElementById("content");
    computeStuff();
    doFirst();
  }

  function doFirst() {
    for (var i = 0; i < 2 * bannersPerWindow; i++) {
      if (!getBanner()) {
        if (checkAllTheWayDown()) {
          break;
        }
        i--;
      }
    }
  }

  function checkAllTheWayDown() {
    var allTheWay = true;
    for (var way of allTheWayFor) {
      if (!way) {
        allTheWay = false;
        break;
      }
    }
    allTheWayDown = allTheWay;
    return allTheWayDown;
  }

  function getBanner() {
    if (allTheWayFor[extensionPointer]) {
      if (++extensionPointer >= maxExtension) {
        extensionPointer = 0;
      }
      return false;
    }
    var extension = extensions[extensionPointer];
    var indexPointer = indexPointers[extensionPointer]++;
    if (indexPointer > maxIndices[extensionPointer]) {
      allTheWayFor[extensionPointer] = true;
      if (++extensionPointer >= maxExtension) {
        extensionPointer = 0;
      }
      return false;
    }
    var index = 0;
    if (extension === "jpg") {
      index = jpg[indexPointer];
    }
    else if (extension === "gif") {
      index = gif[indexPointer];
    }
    else if (extension === "png") {
      index = png[indexPointer];
    }
    if (++extensionPointer >= maxExtension) {
      extensionPointer = 0;
    }
    if (index === undefined) {
      return false;
    }
    var img = document.createElement("img");
    img.className = "img";
    img.src = baseUrl + index + "." + extension;
    img.title = index + "." + extension;
    img.onerror = imgOnerror;
    content.appendChild(img);
    return true;
  }

  function imgOnerror() {
    var split = this.src.split("/");
    var failure = split[split.length - 1];
    this.outerHTML = placeholder.replace("T", "Failed to load " + failure);
  }

  document.addEventListener("keydown", onKeyDown);
  window.addEventListener("resize", onResize);
  window.addEventListener("DOMMouseScroll", onScroll);
  window.addEventListener("mousewheel", onScroll);
  document.addEventListener("DOMContentLoaded", initialize);
}).call(this);
<commit_msg>Update banners.js<commit_after>(function() {
  "use strict";
  var bannerWidth = 300;
  var bannerHeight = 100;
  var content = null;
  var placeholder = '
';
  var bannersPerWindow = 0;
  var bannersPerLine = 0;
  var bannersPerColumn = 0;
  var jpg = [0, 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 28, 29, 33, 38, 39, 43, 44, 45, 46, 47, 52, 54, 57, 59, 60, 61, 64, 66, 67, 69, 71, 72, 76, 77, 81, 82, 83, 84, 88, 90, 91, 96, 98, 99, 100, 104, 106, 116, 119, 137, 140, 148, 149, 150, 154, 156, 157, 158, 159, 161, 162, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 178, 179, 180, 181, 182, 183, 186, 189, 190, 192, 193, 194, 197, 198, 200, 201, 202, 203, 205, 206, 207, 208, 210, 213, 214, 215, 216, 218, 219, 220, 221, 222, 223, 224, 227];
  var gif = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 28, 29, 30, 33, 34, 35, 36, 37, 39, 40, 42, 44, 45, 46, 48, 50, 52, 54, 55, 57, 58, 59, 60, 61, 63, 64, 66, 67, 68, 69, 70, 72, 73, 75, 76, 77, 78, 80, 81, 82, 83, 86, 87, 88, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 109, 110, 111, 112, 113, 115, 116, 117, 118, 119, 120, 122, 123, 124, 127, 129, 130, 131, 134, 135, 136, 138, 139, 141, 144, 146, 148, 149, 153, 154, 155, 157, 158, 159, 160, 161, 162, 164, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 181, 182, 183, 185, 186, 187, 188, 189, 190, 191, 192, 193, 195, 196, 197, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 219, 220, 221, 222, 224, 225, 226, 227, 228, 230, 232, 233, 234, 235, 238, 240, 241, 243, 244, 245, 246, 247, 249, 250, 251, 253];
  var png = [0, 1, 2, 3, 5, 6, 9, 10, 11, 12, 14, 16, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 37, 39, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 57, 58, 59, 64, 66, 67, 68, 69, 70, 71, 72, 76, 78, 79, 81, 82, 85, 86, 87, 89, 95, 98, 100, 101, 102, 105, 106, 107, 109, 110, 111, 112, 113, 114, 115, 116, 118, 119, 120, 121, 122, 123, 126, 128, 130, 134, 136, 138, 139, 140, 142, 145, 146, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 178, 179, 180, 181, 182, 184, 186, 188, 190, 192, 193, 194, 195, 196, 197, 198, 200, 202, 203, 205, 206, 207, 209, 212, 213, 214, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 229, 231, 232, 233, 234, 235, 237, 238, 239, 240, 241, 242, 244, 245, 246, 247, 248, 249, 250, 253, 254, 255, 256, 257, 258, 259, 260, 262, 268];
  var indexPointers = [0, 0, 0];
  var maxIndices = [jpg[jpg.length - 1], gif[gif.length - 1], png[png.length - 1]];
  var extensions = ["jpg", "gif", "png"];
  var extensionPointer = 0;
  var maxExtension = extensions.length;
  var baseUrl = "//s.4cdn.org/image/title/";
  var downCount = 0;
  var allTheWayFor = [false, false, false];
  var allTheWayDown = false;
  var okayToScroll = true;
  var scrollTimeout = 500;
  var scrollTimeoutContainer = null;

  function resetStuff() {
    content.innerHTML = "";
    content.style.top = 0;
    indexPointers = [0, 0, 0];
    extensionPointer = 0;
    downCount = 0;
    okayToScroll = true;
    allTheWayFor = [false, false, false];
    allTheWayDown = false;
    clearTimeout(scrollTimeoutContainer);
    scrollTimeoutContainer = null;
    computeStuff();
    doFirst();
  }

  function computeStuff() {
    bannersPerLine = Math.floor(window.innerWidth / bannerWidth);
    bannersPerColumn = Math.floor(window.innerHeight / bannerHeight);
    bannersPerWindow = bannersPerLine * bannersPerColumn;
  }

  function onKeyDown(e) {
    if (e.ctrlKey && e.keyCode === 38) {
      resetStuff();
    }
    else if (e.keyCode === 40 && okayToScroll) {
      if (!allTheWayDown) {
        scrollTimeoutContainer = setTimeout(makeScrollOkay, scrollTimeout);
      }
      okayToScroll = false;
      scrollMore();
    }
  }

  function makeScrollOkay() {
    okayToScroll = true;
    scrollTimeoutContainer = null;
  }

  function scrollMore() {
    downCount++;
    content.style.top = "-" + downCount * bannersPerColumn * bannerHeight + "px";
    var imgs = document.getElementsByTagName("img");
    for (var elem of imgs) {
      imgs.innerHTML = placeholder;
    }
    for (var i = 0; i < bannersPerWindow; i++) {
       if (!getBanner()) {
         if (checkAllTheWayDown()) {
          break;
        }
        i--;
       }
    }
  }

  function onResize() {
    window.removeEventListener("resize", onResize);
    alert("Resizing the window breaks this page.");
  }

  function onScroll() {
    if (okayToScroll) {
      if (!allTheWayDown) {
        scrollTimeoutContainer = setTimeout(makeScrollOkay, scrollTimeout);
      }
      okayToScroll = false;
      scrollMore();
    }
  }

  function initialize() {
    document.removeEventListener("DOMContentLoaded", initialize);
    content = document.getElementById("content");
    computeStuff();
    doFirst();
  }

  function doFirst() {
    for (var i = 0; i < 2 * bannersPerWindow; i++) {
      if (!getBanner()) {
        if (checkAllTheWayDown()) {
          break;
        }
        i--;
      }
    }
  }

  function checkAllTheWayDown() {
    var allTheWay = true;
    for (var way of allTheWayFor) {
      if (!way) {
        allTheWay = false;
        break;
      }
    }
    allTheWayDown = allTheWay;
    return allTheWayDown;
  }

  function getBanner() {
    if (allTheWayFor[extensionPointer]) {
      if (++extensionPointer >= maxExtension) {
        extensionPointer = 0;
      }
      return false;
    }
    var extension = extensions[extensionPointer];
    var indexPointer = indexPointers[extensionPointer]++;
    if (indexPointer > maxIndices[extensionPointer]) {
      allTheWayFor[extensionPointer] = true;
      if (++extensionPointer >= maxExtension) {
        extensionPointer = 0;
      }
      return false;
    }
    var index = 0;
    if (extension === "jpg") {
      index = jpg[indexPointer];
    }
    else if (extension === "gif") {
      index = gif[indexPointer];
    }
    else if (extension === "png") {
      index = png[indexPointer];
    }
    if (++extensionPointer >= maxExtension) {
      extensionPointer = 0;
    }
    if (index === undefined) {
      return false;
    }
    var img = document.createElement("img");
    img.className = "img";
    img.src = baseUrl + index + "." + extension;
    img.title = index + "." + extension;
    img.onerror = imgOnerror;
    content.appendChild(img);
    return true;
  }

  function imgOnerror() {
    var split = this.src.split("/");
    var failure = split[split.length - 1];
    this.outerHTML = placeholder.replace("T", "Failed to load " + failure);
  }

  document.addEventListener("keydown", onKeyDown);
  window.addEventListener("resize", onResize);
  window.addEventListener("DOMMouseScroll", onScroll);
  window.addEventListener("mousewheel", onScroll);
  document.addEventListener("DOMContentLoaded", initialize);
}).call(this);
<|endoftext|>


<commit_before>let Bedrock = function () {
    let $ = Object.create (null);

    $.get = function (queryString, onSuccess) {
        let request = new XMLHttpRequest ();
        request.overrideMimeType ("application/json");
        request.open ("GET", queryString, true);
        request.onload = function (event) {
            if (request.status === 200) {
                let response = JSON.parse (this.responseText);
                onSuccess (response);
            }
        };
        request.send ();
    };

    $.getFromServiceBase = function (queryString, onSuccess) {
        this.fetch (queryString, function (response) {
            console.log (queryString + " (status: " + response.status + ")");
            if (response.status === "ok") {
                onSuccess (response.response);
            }
        });
    };

    $.Base = function () {
        let _ = Object.create (null);

        _.new = function (parameters) {
            return Object.create (this).init (parameters);
        };

        return _;
    } ();

    return $;
} ();
<commit_msg>I renamed the function, but didn't change the name when I call it.<commit_after>let Bedrock = function () {
    let $ = Object.create (null);

    $.get = function (queryString, onSuccess) {
        let request = new XMLHttpRequest ();
        request.overrideMimeType ("application/json");
        request.open ("GET", queryString, true);
        request.onload = function (event) {
            if (request.status === 200) {
                let response = JSON.parse (this.responseText);
                onSuccess (response);
            }
        };
        request.send ();
    };

    $.getFromServiceBase = function (queryString, onSuccess) {
        this.get (queryString, function (response) {
            console.log (queryString + " (status: " + response.status + ")");
            if (response.status === "ok") {
                onSuccess (response.response);
            }
        });
    };

    $.Base = function () {
        let _ = Object.create (null);

        _.new = function (parameters) {
            return Object.create (this).init (parameters);
        };

        return _;
    } ();

    return $;
} ();
<|endoftext|>


<commit_before>import peek42 from '../universal/base';
import Console from './console/console';
import './peek42.scss';

function _output(arg, comment) {
  Console.instance.then(_console => {
    _console.output(arg, comment);
  });
}

Console.instance.then(_console => {
  Object.assign(peek42, {
    _console
  });
});

Object.assign(peek42, {
  _output
});

export default peek42;
<commit_msg>Add newline<commit_after>import peek42 from '../universal/base';
import Console from './console/console';

import './peek42.scss';

function _output(arg, comment) {
  Console.instance.then(_console => {
    _console.output(arg, comment);
  });
}

Console.instance.then(_console => {
  Object.assign(peek42, {
    _console
  });
});

Object.assign(peek42, {
  _output
});

export default peek42;
<|endoftext|>


<commit_before>const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Gdk = imports.gi.Gdk;
const Gtk = imports.gi.Gtk;

Gio.resources_register(Gio.resource_load('org.gnome.Gnometa.gresource'));

const ArrayView = imports.ArrayView;
const OutputView = imports.OutputView;
const PopoverView = imports.PopoverView;
const SplitView = imports.SplitView;
const TextView = imports.TextView;
//const UiHelper = imports.UiHelper;
const UiHelper = imports.UiHelperClient;
const Utils = imports.Utils;

// Source mapping
const OMetaMap = JSON.parse(Utils.loadFile('standalone.js.map'));

let _ometaFiles = {};
let ometaCode = function(filename, start, stop) {
  if (!_ometaFiles[filename]) {
    _ometaFiles[filename] = Utils.loadFile('../' + filename);
  }
  return _ometaFiles[filename].slice(start, stop).trim();
};

let ometaLabel = function(id) {
  let sitem = OMetaMap.map[id];
  let filename = OMetaMap.filenames[sitem[0]];
  return [filename, Utils.loadFile('../' + filename), sitem[1], sitem[2]];
};

// Structure searching
let _structure = null;
let findMatchingStructureChild = function(parent, startOffset, endOffset) {
  for (let i = 0; i < parent.children.length; i++) {
    let child = parent.children[i];
    if (child.start.idx <= startOffset && child.stop.idx >= endOffset)
      return child;
  }
  return null;
};
let getMatchStructure = function(startOffset, endOffset) {
  let matches = [], child = _structure;
  let iter = 0;
  do {
    for (let i = 0; i < child.ids.length; i++)
      matches.unshift({ id: child.ids[i], start: child.start, stop: child.stop, value: child.value });
    child = findMatchingStructureChild(child, startOffset, endOffset);
    iter++;
  } while (child);
  return matches;
};

let bestStructureMatch = function(matches) {
  for (let i = 0; i < matches.length; i++)
    if (matches[i].stop.idx - matches[i].start.idx >= 2)
      return [i, matches[i]];
  return [matches.length - 1, matches[matches.length - 1]];
};
let bestNamedStructureMatch = function(matches) {
  for (let i = 0; i < matches.length; i++)
    if (matches[i].id != -1 &&
        (matches[i].stop.idx - matches[i].start.idx) >= 2)
      return [i, matches[i]];
  return [matches.length - 1, matches[matches.length - 1]];
};

// Structure tree
let _structureTreeIdx = -1;

//
Gtk.init(null, null);

let paned = new SplitView.SplitView();

let scrolled = new Gtk.ScrolledWindow();
let textview = new TextView.TextView();
scrolled.add(textview);
paned.addWidget(scrolled);

let popover = new Gtk.Popover({
  position: Gtk.PositionType.BOTTOM,
  relative_to: textview,
});
popover.set_size_request(400, 400);
let popoverview = new PopoverView.PopoverView();
popover.add(popoverview);

let structview = new OutputView.OutputView();
paned.addWidget(structview);

//
textview.onChange(function(text) {
  let data = { name: 'OMeta',
               rule: 'topLevel',
               input: text,
               output: 'view0', };
  UiHelper.executeCommand('translate', data, function(error, ret) {
    if (error) {
      textview.hightlightRange('error', error.idx, text.length - 1);
      return;
    }
    textview.removeSelection('error');
  }.bind(this));
}.bind(this));

textview.connect('offset-changed', function(widget, offset) {
  let data = { input: 'view0', output: 'view0', offset: { start: offset, end: offset }, };
  UiHelper.executeCommand('match-structure', data, function(error, ret) {
    if (error) {
      log(error);
      return;
    }
    UiHelper.executeCommand('get-best-match', { input: 'view0' }, function(error, ret) {
      if (error) {
        log(error);
        return;
      }
      let  [idx, match] = ret;
      _structureTreeIdx = idx;
      textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
      structview.setData(match.value);
    }.bind(this));
  }.bind(this));
}.bind(this));
textview.connect('selection-changed', function(widget, startOffset, endOffset) {
  let data = { input: 'view0', output: 'view0', offset: { start: startOffset, end: endOffset }, };
  UiHelper.executeCommand('match-structure', data, function(error, ret) {
    textview.removeSelection();
    if (error) {
      log(error);
      return;
    }
    _structureTreeIdx = 0;
    UiHelper.executeCommand('get-match', { input: 'view0', index: _structureTreeIdx }, function(error, match) {
      if (error) {
        log(error);
        return;
      }
      textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
      structview.setData(match.value);
    }.bind(this));
  }.bind(this));
});

textview.connect('alternate-menu', function(widget, startOffset, endOffset) {
  let data = { input: 'view0', output: 'view0', offset: { start: startOffset, end: endOffset }, };
  UiHelper.executeCommand('match-structure', data, function(error, ret) {
    if (error) {
      log(error);
      return;
    }
    UiHelper.executeCommand('get-best-match', { input: 'view0' }, function(error, ret) {
      if (error) {
        log(error);
        return;
      }
      let  [idx, match] = ret;
      _structureTreeIdx = idx;
      textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
      let rect = textview.getRectForRange(match.start.idx, match.stop.idx);
      popover.pointing_to = rect;
      popoverview.setData.apply(popoverview, ometaLabel(match.id));
      popover.show();
      structview.setData(match.value);
    }.bind(this));
  }.bind(this));
}.bind(this));

popoverview.connect('rule-move', function(widget, way) {
  _structureTreeIdx += way;
  UiHelper.executeCommand('get-match', { input: 'view0', index: _structureTreeIdx }, function(error, match) {
    if (error) {
      log(error);
        return;
    }
    textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
    popoverview.setData.apply(popoverview, ometaLabel(match.id));
    textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
    structview.setData(match.value);
  }.bind(this));
}.bind(this));

//
let source = Utils.loadFile(ARGV[0]);
textview.setData(source);

//
let builder = Gtk.Builder.new_from_resource('/org/gnome/Gnometa/ui.ui');
let widget = function(name) { return builder.get_object(name); };

let win = widget('main-window');
win.set_titlebar(widget('titlebar'));
widget('close-button').connect('clicked', (function() { win.hide(); popover.hide(); Gtk.main_quit(); }).bind(this));
win.connect('key-press-event', function(widget, event) {
  let keyval = event.get_keyval()[1];
  switch (keyval) {
  case Gdk.KEY_F5: paned.removeLastWidget(); break;
  case Gdk.KEY_F6: paned.addWidget(new OutputView.OutputView()); break;
  case Gdk.KEY_F7: paned.shrinkFocusedChild(10); break;
  case Gdk.KEY_F8: paned.growFocusedChild(10); break;
  }
  return false;
});

win.resize(800, 400);
win.add(paned);
win.show();

Gtk.main();
<commit_msg>ui: remove error highlight when translation is successful<commit_after>const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Gdk = imports.gi.Gdk;
const Gtk = imports.gi.Gtk;

Gio.resources_register(Gio.resource_load('org.gnome.Gnometa.gresource'));

const ArrayView = imports.ArrayView;
const OutputView = imports.OutputView;
const PopoverView = imports.PopoverView;
const SplitView = imports.SplitView;
const TextView = imports.TextView;
//const UiHelper = imports.UiHelper;
const UiHelper = imports.UiHelperClient;
const Utils = imports.Utils;

// Source mapping
const OMetaMap = JSON.parse(Utils.loadFile('standalone.js.map'));

let _ometaFiles = {};
let ometaCode = function(filename, start, stop) {
  if (!_ometaFiles[filename]) {
    _ometaFiles[filename] = Utils.loadFile('../' + filename);
  }
  return _ometaFiles[filename].slice(start, stop).trim();
};

let ometaLabel = function(id) {
  let sitem = OMetaMap.map[id];
  let filename = OMetaMap.filenames[sitem[0]];
  return [filename, Utils.loadFile('../' + filename), sitem[1], sitem[2]];
};

// Structure searching
let _structure = null;
let findMatchingStructureChild = function(parent, startOffset, endOffset) {
  for (let i = 0; i < parent.children.length; i++) {
    let child = parent.children[i];
    if (child.start.idx <= startOffset && child.stop.idx >= endOffset)
      return child;
  }
  return null;
};
let getMatchStructure = function(startOffset, endOffset) {
  let matches = [], child = _structure;
  let iter = 0;
  do {
    for (let i = 0; i < child.ids.length; i++)
      matches.unshift({ id: child.ids[i], start: child.start, stop: child.stop, value: child.value });
    child = findMatchingStructureChild(child, startOffset, endOffset);
    iter++;
  } while (child);
  return matches;
};

let bestStructureMatch = function(matches) {
  for (let i = 0; i < matches.length; i++)
    if (matches[i].stop.idx - matches[i].start.idx >= 2)
      return [i, matches[i]];
  return [matches.length - 1, matches[matches.length - 1]];
};
let bestNamedStructureMatch = function(matches) {
  for (let i = 0; i < matches.length; i++)
    if (matches[i].id != -1 &&
        (matches[i].stop.idx - matches[i].start.idx) >= 2)
      return [i, matches[i]];
  return [matches.length - 1, matches[matches.length - 1]];
};

// Structure tree
let _structureTreeIdx = -1;

//
Gtk.init(null, null);

let paned = new SplitView.SplitView();

let scrolled = new Gtk.ScrolledWindow();
let textview = new TextView.TextView();
scrolled.add(textview);
paned.addWidget(scrolled);

let popover = new Gtk.Popover({
  position: Gtk.PositionType.BOTTOM,
  relative_to: textview,
});
popover.set_size_request(400, 400);
let popoverview = new PopoverView.PopoverView();
popover.add(popoverview);

let structview = new OutputView.OutputView();
paned.addWidget(structview);

//
textview.onChange(function(text) {
  let data = { name: 'OMeta',
               rule: 'topLevel',
               input: text,
               output: 'view0', };
  UiHelper.executeCommand('translate', data, function(error, ret) {
    if (error) {
      textview.hightlightRange('error', error.idx, text.length - 1);
      return;
    }
    textview.removeHighlightRange('error');
  }.bind(this));
}.bind(this));

textview.connect('offset-changed', function(widget, offset) {
  let data = { input: 'view0', output: 'view0', offset: { start: offset, end: offset }, };
  UiHelper.executeCommand('match-structure', data, function(error, ret) {
    if (error) {
      log(error);
      return;
    }
    UiHelper.executeCommand('get-best-match', { input: 'view0' }, function(error, ret) {
      if (error) {
        log(error);
        return;
      }
      let  [idx, match] = ret;
      _structureTreeIdx = idx;
      textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
      structview.setData(match.value);
    }.bind(this));
  }.bind(this));
}.bind(this));
textview.connect('selection-changed', function(widget, startOffset, endOffset) {
  let data = { input: 'view0', output: 'view0', offset: { start: startOffset, end: endOffset }, };
  UiHelper.executeCommand('match-structure', data, function(error, ret) {
    textview.removeSelection();
    if (error) {
      log(error);
      return;
    }
    _structureTreeIdx = 0;
    UiHelper.executeCommand('get-match', { input: 'view0', index: _structureTreeIdx }, function(error, match) {
      if (error) {
        log(error);
        return;
      }
      textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
      structview.setData(match.value);
    }.bind(this));
  }.bind(this));
});

textview.connect('alternate-menu', function(widget, startOffset, endOffset) {
  let data = { input: 'view0', output: 'view0', offset: { start: startOffset, end: endOffset }, };
  UiHelper.executeCommand('match-structure', data, function(error, ret) {
    if (error) {
      log(error);
      return;
    }
    UiHelper.executeCommand('get-best-match', { input: 'view0' }, function(error, ret) {
      if (error) {
        log(error);
        return;
      }
      let  [idx, match] = ret;
      _structureTreeIdx = idx;
      textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
      let rect = textview.getRectForRange(match.start.idx, match.stop.idx);
      popover.pointing_to = rect;
      popoverview.setData.apply(popoverview, ometaLabel(match.id));
      popover.show();
      structview.setData(match.value);
    }.bind(this));
  }.bind(this));
}.bind(this));

popoverview.connect('rule-move', function(widget, way) {
  _structureTreeIdx += way;
  UiHelper.executeCommand('get-match', { input: 'view0', index: _structureTreeIdx }, function(error, match) {
    if (error) {
      log(error);
        return;
    }
    textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
    popoverview.setData.apply(popoverview, ometaLabel(match.id));
    textview.hightlightRange('highlight', match.start.idx, match.stop.idx);
    structview.setData(match.value);
  }.bind(this));
}.bind(this));

//
let source = Utils.loadFile(ARGV[0]);
textview.setData(source);

//
let builder = Gtk.Builder.new_from_resource('/org/gnome/Gnometa/ui.ui');
let widget = function(name) { return builder.get_object(name); };

let win = widget('main-window');
win.set_titlebar(widget('titlebar'));
widget('close-button').connect('clicked', (function() { win.hide(); popover.hide(); Gtk.main_quit(); }).bind(this));
win.connect('key-press-event', function(widget, event) {
  let keyval = event.get_keyval()[1];
  switch (keyval) {
  case Gdk.KEY_F5: paned.removeLastWidget(); break;
  case Gdk.KEY_F6: paned.addWidget(new OutputView.OutputView()); break;
  case Gdk.KEY_F7: paned.shrinkFocusedChild(10); break;
  case Gdk.KEY_F8: paned.growFocusedChild(10); break;
  }
  return false;
});

win.resize(800, 400);
win.add(paned);
win.show();

Gtk.main();
<|endoftext|>


<commit_before>
let r = require('r-dom')
let mori = require('mori')
let PropTypes = require('react').PropTypes
let ShallowComponent = require('../shallow-component')

let AppActions = require('../../actions/app-actions')

let platform_data = require('../../constants/platform-data')
let platform = require('../../util/os').itch_platform()

class Thumbnail extends ShallowComponent {
  render () {
    let game = this.props.game

    let platform_list = []
    for (let platform_spec of platform_data) {
      if (!mori.get(game, platform_spec.field)) {
        continue
      }
      let active = (platform === platform_spec.platform)
      let classSet = { icon: true, active }
      classSet[`icon-${platform_spec.icon}`] = true
      platform_list.push(r.span({ classSet }))
    }

    let style = {}

    let cover_url = mori.get(game, 'cover_url')
    if (cover_url) {
      style.backgroundImage = `url('${cover_url}')`
    }

    let onClick = (e) => this.on_click(e)

    let classSet = {
      game_thumb: true,
      has_cover: cover_url
    }
    let children = []

    children.push(r.div({ style, classSet, onClick }))

    if (platform_list.length) {
      children.push(r.div({className: 'platforms'}, platform_list))
    }
    return r.div({className: 'bordered'}, children)
  }

  on_click (e) {
    let cave = this.props.cave
    let cave_id = mori.get(cave, '_id')

    let game = this.props.game
    let game_id = mori.get(game, 'id')

    if (e.ctrlKey || e.shiftKey) {
      AppActions.cave_explore(cave_id)
    } else if (e.altKey) {
      AppActions.cave_probe(cave_id)
    } else {
      AppActions.game_browse(game_id)
    }
  }
}

Thumbnail.propTypes = {
  game: PropTypes.any,
  cave: PropTypes.any
}

module.exports = Thumbnail
<commit_msg>:art: Use mori-ext in game-cell/Thumbnail component #412<commit_after>
let r = require('r-dom')
import {get} from 'mori-ext'

let PropTypes = require('react').PropTypes
let ShallowComponent = require('../shallow-component')

let AppActions = require('../../actions/app-actions')

let platform_data = require('../../constants/platform-data')
let platform = require('../../util/os').itch_platform()

class Thumbnail extends ShallowComponent {
  render () {
    let game = this.props.game

    let platform_list = []
    for (let platform_spec of platform_data) {
      if (!game::get(platform_spec.field)) {
        continue
      }
      let active = (platform === platform_spec.platform)
      let classSet = { icon: true, active }
      classSet[`icon-${platform_spec.icon}`] = true
      platform_list.push(r.span({ classSet }))
    }

    let style = {}

    let cover_url = game::get('cover_url')
    if (cover_url) {
      style.backgroundImage = `url('${cover_url}')`
    }

    let onClick = (e) => this.on_click(e)

    let classSet = {
      game_thumb: true,
      has_cover: cover_url
    }
    let children = []

    children.push(r.div({ style, classSet, onClick }))

    if (platform_list.length) {
      children.push(r.div({className: 'platforms'}, platform_list))
    }
    return r.div({className: 'bordered'}, children)
  }

  on_click (e) {
    let {cave, game} = this.props

    let cave_id = cave::get('_id')
    let game_id = game::get('id')

    if (e.ctrlKey || e.shiftKey) {
      AppActions.cave_explore(cave_id)
    } else if (e.altKey) {
      AppActions.cave_probe(cave_id)
    } else {
      AppActions.game_browse(game_id)
    }
  }
}

Thumbnail.propTypes = {
  game: PropTypes.any,
  cave: PropTypes.any
}

module.exports = Thumbnail
<|endoftext|>