Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/*
(c) 2019 by [[User:Magnus Manske]]. GPLv3 or later.

This tool lets you quickly add statements for Structured Data on Commons (SDC) to (selected) files on galleries, category pages, and serach results.

Demo video: https://www.youtube.com/watch?v=RIjXRJNcbL0

To use this script, add it to your [https://commons.wikimedia.org/wiki/Special:MyPage/common.js common.js page], like so:
importScript('User:Magnus Manske/sdc_tool.js') ;

Activate by clicking on the "SDC" link in the box in the lower-right corner, or by pressing S with (Alt/Ctrl/whatever your browser uses).

*/
$(document).ready ( function () {

	// Dialog
	function SDCdialog( config ) {
		SDCdialog.super.call( this, config );
	}
	OO.inheritClass( SDCdialog, OO.ui.Dialog ); 

	// Specify a name for .addWindows()
	SDCdialog.static.name = 'SDCdialog';
	// Specify a title statically (or, alternatively, with data passed to the opening() method).
	SDCdialog.static.title = 'Find a target item for SDC';

	SDCdialog.prototype.update_results = function () {
		let html = "" ;
		$.each ( this.results , function ( result_id , v ) {
			html += "<div style='margin:2px;padding:2px;border:1px solid #DDD;border-radius:3;display:flex;'>" ;
			html += "<div style='margin-right:0.2rem;'><input style='color:green;font-size:1.5em;' class='sdc_dialog_cegision_button' result='" ;
			html += result_id+"' type='button' value='✓' /></div>" ;
			html += "<div>" ;
			html += "<div><b class='result_label' result='"+result_id+"'></b> <small>[<a href='https://www.wikidata.org/wiki/"+v.q+"' target='_blank'>"+v.q + "</a>]</small></div>" ;
			html += "<div class='result_description' result='"+result_id+"'></div>" ;
			html += "</div>" ;
			html += "</div>" ;
		} ) ;
		if ( html == '' ) html = '<div style="margin:2px;"><i>No results found on Wikidata</i></div>' ;
		$('#sdc_dialog_results').html(html);
		$.each ( this.results , function ( result_id , v ) {
			$('#sdc_dialog_results b.result_label[result="'+result_id+'"]').text(v.label);
			$('#sdc_dialog_results div.result_description[result="'+result_id+'"]').text(v.description);
		} ) ;
		let me = this ;
		$('.sdc_dialog_cegision_button').click(function(){
			let result_id = $(this).attr('result')*1 ;
			let item = me.results[result_id] ;
			me.on_decision(item);

		});
	}

	// Customize the initialize() function: This is where to add content to the dialog body and set up event handlers.
	SDCdialog.prototype.initialize = function () {
		// Call the parent method.
		SDCdialog.super.prototype.initialize.call( this );
		// Create and append a layout and some content.
		this.content = new OO.ui.PanelLayout( { 
			padded: true,
			expanded: false 
		} );

		let html = "<p>";
		html += "<div>Enter a search query for Wikidata items</div>" ;
		html += "<div><input type='text' id='sdc_dialog_query' style='width:100%' placeholder='search query' /></div>" ;
		html += "<div id='sdc_dialog_results' style='height:10rem;overflow:auto;'></div>" ;
		html += "</p>" ;

		this.content.$element.append( html );
		this.$body.append( this.content.$element );

		this.results = [] ;

		let me = this ;
		me.query_counter = 0 ;
		me.last_query = '' ;
		$('#sdc_dialog_query').keyup(function(){
			let query = $(this).val();
			me.results = [] ;
			if ( query=='' || query==me.last_query ) return ;
			me.last_query = query ;
			me.query_counter++ ;
			let qc = me.query_counter ;
			$.getJSON('https://www.wikidata.org/w/api.php?callback=?',{
				action:'wbsearchentities',
				search:query,
				language:wgUserLanguage,
				limit:50,
				type:'item',
				format:'json'
			},function(d){
				if ( me.query_counter != qc ) return ; // New query was started while this one was running
				$(d.search).each(function(k,v){
					me.results.push({q:v.id,label:v.label,description:v.description||''});
				});
				me.update_results();
			});
		});
	};

	// Override the getBodyHeight() method to specify a custom height (or don't to use the automatically generated height).
	SDCdialog.prototype.getBodyHeight = function () {
		return this.content.$element.outerHeight( true );
	};

	var sdc = {

		exclude_files:['Gtk-dialog-info-14px.png','Reasonator_logo_proposal_no_background.png'],
		is_stopped : true,
		active : false,
		dialog: new SDCdialog( { size: 'medium' } ),
		windowManager: new OO.ui.WindowManager(),
		mid_cache:[],
		mid2file:[],
		media_items:[],
		wikidata_items_to_load:[],
		wikidata_item_labels:[],
		target_item : {
			q:'',
			label:'',
			description:''
		},

		init : function () {
			if ( wgNamespaceNumber == 6 ) return ; // Not for single images
			if ( $('a.image').length == 0 ) return ; // No possible thumbnails
			let me = this ;
			me.dialog.on_decision = me.set_target_item;
			$( document.body ).append( me.windowManager.$element );
			me.windowManager.addWindows( [ me.dialog ] );

			me.show_main_element();
			me.try_guess_main_item();
			//me.toggle_main(); // Auto-open
		} ,

		try_guess_main_item : function () {
			let me = this ;
			if ( typeof wgWikibaseItemId == 'undefined' || wgWikibaseItemId == '' ) return ;

			me.try_set_main_item(wgWikibaseItemId);
		} ,

		try_set_main_item : function (q) {
			let me = this ;
			me.wikidata_items_to_load=[q] ;
			me.load_wikidata_items(function(){
				$.getJSON("https://www.wikidata.org/w/api.php?callback=?&action=wbgetentities&format=json&ids="+q,function(d){
					let item = (d.entities||{})[q] ;
					if ( typeof item == 'undefined' ) return ;
					if ( me.does_item_have_property_target(item,'P31','Q4167836') ) {
						let statement = ((item.claims||item.statements||{}).P301||[])[0] ;
						if ( typeof statement == 'undefined' ) return ; // No "category's main topic"
						let target_id = (((statement.mainsnak||{}).datavalue||{}).value||{}).id ;
						if ( typeof target_id != 'undefined' && target_id != q ) me.try_set_main_item(target_id);
						return ;
					}
					let label = $(me.wikidata_item_labels[q]).text();
					me.set_target_item({q:q,label:label,description:''});
				});
			});
		} ,

		update_main_position : function () {
			let bottom = $('#cat_a_lot_toggle').length>0 ? 30 : 0 ;
			$('#sdc_main').css({bottom:bottom+'px'})
		} ,

		show_action_button : function () {
			let me = this ;
			var action_button = new OO.ui.ButtonWidget( {
				label: 'Set '+me.get_property()+' to '+me.target_item.q,
				href: '#',
				flags: 'progressive'
			} );    
			$('#sdc_action_button_container').html(action_button.$element);
			$('#sdc_action_button_container a.oo-ui-buttonElement-button').click(function(){
				if ( $('input.sdc_checkbox:checked').length == 0 ) {
					alert("No files selected");
					return false ;
				}
				me.is_stopped = false ;
				me.show_action_stop_button();
				me.edit_next();
				return false;
			});
		} ,

		show_action_stop_button : function () {
			let me = this ;
			var action_button = new OO.ui.ButtonWidget( {
				label: 'Stop editing',
				href: '#',
				flags: 'destructive'
			} );    
			$('#sdc_action_button_container').html(action_button.$element);
			$('#sdc_action_button_container a.oo-ui-buttonElement-button').click(function(){
				me.is_stopped=true;
				$('#sdc_action_button_container').html("<i>Stopping edits...</i>");
				return false;
			});
		} ,

		edit_next : function () {
			let me = this ;
			if ( me.is_stopped ) return me.show_action_button(); // User stopped this
			let cbs = $('input.sdc_checkbox:checked') ;
			if ( cbs.length == 0 ) return me.show_action_button(); // All done
			let cb = $(cbs.get(0)) ;
			let file = decodeURIComponent(cb.attr('file')) ;
			if ( typeof file == 'undefined' ) return me.show_action_button(); // All done

			me.get_mediainfo_id_for_file ( file , function ( mediainfo_id ) {
				me.add_item_statement_to_item(mediainfo_id,me.get_property(),me.target_item.q,me.is_prominent(),function(ok){
					// TODO check ok
					cb.attr('checked', false);
					cb.prop('checked', false);
					me.edit_next();
					//delete me.media_items[mediainfo_id] ;
					me.load_media_items([mediainfo_id]);
				});
			} ) ;

		} ,

		is_prominent : function() {
			return $('#sdc_prominent').is(":checked");
		} ,

		// `file` WITHOUT File: prefix!
		get_mediainfo_id_for_file : function ( file , callback ) {
			let me = this ;
			if ( typeof me.mid_cache[file] != 'undefined') {
				return callback ( me.mid_cache[file] ) ;
			}
			$.get('/w/api.php',{
				action:'query',
				prop:'info',
				titles:"File:"+file,
				format:'json'
			},function(d){
				let mid = -1 ;
				$.each ( d.query.pages , function ( page_id , page_info ) { mid = page_id } ) ;
				if ( mid == -1 ) mid = '' ;
				else mid = 'M'+mid ;
				me.mid_cache[file] = mid ;
				callback(mid);
			},'json');
		} ,

		get_token : function ( callback ) {
			$.post ( '/w/api.php' , {
				action : 'query' ,
				meta : 'tokens' ,
				format : 'json' ,
			} , function ( d ) {
				callback(d.query.tokens.csrftoken);
			} ) ;
		} ,

		does_item_have_property_target : function ( item , property , target_item_id ) {
			let statement_present = false ;
			$.each ( ((item.statements||item.claims||{})[property]||[]) , function ( dummy , statement ) {
				if ( ((((statement||{}).mainsnak||{}).datavalue||{}).value||{}).id == target_item_id ) {
					statement_present = true ;
				}
			} ) ;
			return statement_present ;
		} ,

		add_item_statement_to_item : function ( item_id , property , target_item_id , prominent , callback ) {
			let me = this ;
			let value = {'entity-type':'item',id:target_item_id} ;
			let data = {claims:[{mainsnak:{snaktype:"value",property:property,datavalue:{value:value,type:'wikibase-entityid'}},type:"statement",rank:"normal"}]} ;
			let summary = 'SDC: added [[:d:Property:'+property+'|'+property+']] => [[:d:'+target_item_id+'|'+target_item_id+']]' ;
			if ( prominent ) {
				data.claims[0].rank = 'preferred' ;
				summary += ', prominent' ;
			}

			// Check if statement already present
			if ( typeof me.media_items[item_id] != 'undefined' ) {
				if ( me.does_item_have_property_target(me.media_items[item_id],property,target_item_id) ) return callback(true);
			}

			me.get_token ( function ( token ) {
				let params = {
					action:'wbeditentity',
					id:item_id,
					data:JSON.stringify(data),
					token:token,
					summary:summary,
					format:'json'
				} ;
				$.post('/w/api.php',params,function(d){
					callback(true);
				},'json');
			} );
		} ,

		set_target_item : function ( item ) {
			let me = sdc ;
			me.target_item = item ;

			let html = "" ;
			html += "<small>Target item: [<a href='https://www.wikidata.org/wiki/"+item.q+"' target='_blank'>"+item.q+"</a>]</small><br/><div id='sdc_target_item_label'></div>";
			html = "<div style='margin:0.2rem;'>"+html+"</div>" ;
			$('#sdc_target_item_display').html(html).show();
			$('#sdc_target_item_label').text(item.label);

			me.show_action_button();

			me.update_main_position();
			me.windowManager.closeWindow( me.dialog );
		} ,

		get_property : function () {
			return $('#sdc_property').val();
		} ,

		open_dialog : function () {
			this.windowManager.openWindow( this.dialog );
			setTimeout(function(){$('#sdc_dialog_query').focus()},500);
		} ,

		show_main_element : function () {
			let me = this ;
			let html = "<div id='sdc_main' style='position:fixed;right:0px;bottom:0px;background-color:#FEF6E7;box-shadow:0 2px 4px rgba(0,0,0,0.5);font-size:.75em;padding:5px;'>" ;
			html += "<a href='#' id='sdc_main_button' accesskey='s' title='Structured Data on Commons tagger [shortcut:S]'>SDC</a>" ;
			html += "<div id='sdc_options' style='display:none'>" ;

			html += "<div><span id='sdc_cb_all'></span> <span id='sdc_cb_toggle'></span> <span id='sdc_cb_none'></span></div>" ;
			/*
			html += "<div>Check " ;
			html += "<a href='#' id='sdc_cb_all'>all</a> | " ;
			html += "<a href='#' id='sdc_cb_toggle'>toggle</a> | " ;
			html += "<a href='#' id='sdc_cb_none'>none</a>" ;
			html += "</div>" ;
			*/

			html += "<div style='margin:0.2em;'>Property "
			html += "<select id='sdc_property'>" ;
			html += "<option value='P180' selected>depicts [P180]</option>" ;
			html += "<option value='P195'>collection [P195]</option>" ;
			html += "</select>" ;
			html += "</div>" ;

			html += "<div id='sdc_target_item' style='margin:2px;padding:2px;border:1px solid #DDD;border-radius:3px'>";
			html += "<div id='sdc_target_item_display' style='display:none;'></div>" ;
			html += "<div id='sdc_target_item_button' style='text-align:center;'></div>" ;
			html += "</div>" ;

			html += "<div id='sdc_action' style='text-align:center'>" ;
			html += "<div><label><input type='checkbox' id='sdc_prominent' /> Prominent</label></div>" ;
			html += "<div id='sdc_action_button_container'><i>Set a target item to perform an action</i></div>" ;
			html += "</div>" ;

			html += "</div>" ;
			html += "</div>" ;
			$('body').append(html);
			$('#sdc_main_button').click(function(){ me.toggle_main(); return false; });

			let button_all = new OO.ui.ButtonWidget( { label: 'All',href: '#' } ) ;
			let button_toggle = new OO.ui.ButtonWidget( { label: 'Toggle',href: '#' } ) ;
			let button_none = new OO.ui.ButtonWidget( { label: 'None',href: '#' } ) ;
			$('#sdc_cb_all ').html(button_all.$element);
			$('#sdc_cb_toggle').html(button_toggle.$element);
			$('#sdc_cb_none').html(button_none.$element);
			$('#sdc_cb_all a.oo-ui-buttonElement-button').click(function(){ $('input.sdc_checkbox').attr('checked', true); return false; });
			$('#sdc_cb_toggle a.oo-ui-buttonElement-button').click(function(){ $('input.sdc_checkbox').click(); return false; });
			$('#sdc_cb_none a.oo-ui-buttonElement-button').click(function(){ $('input.sdc_checkbox').attr('checked', false); return false; });

			let button_sti = new OO.ui.ButtonWidget( { label: 'Set target item',href: '#' } ) ;
			$('#sdc_target_item_button ').html(button_sti.$element);
			$('#sdc_target_item_button a.oo-ui-buttonElement-button').click(function(){me.open_dialog();});

			me.update_main_position();
			setTimeout(me.update_main_position,200);
		} ,

		toggle_main : function () {
			this.active = !this.active ;
			if ( this.active ) {
				this.show_checkboxes();
				$('#sdc_options').show();
			} else {
				$('div.sdc_checkbox_container').remove();
				$('#sdc_options').hide();
			}
			this.update_main_position();
		} ,

		cache_file_media_ids : function ( files ) {
			let me = this ;
			let chunks = [ [] ] ;
			let MAX_CHUNK_SIZE = 50 ;
			$.each ( files , function ( dummy , file ) {
				if ( chunks[chunks.length-1].length < MAX_CHUNK_SIZE ) {
					chunks[chunks.length-1].push ( file ) ;
				} else {
					chunks.push ( [ file ] ) ;
				}
			} ) ;
			me.wikidata_items_to_load = [] ;
			$.each ( chunks , function ( dummy , chunk ) {
				me.cache_file_media_ids_chunk(chunk) ;
			} ) ;
		} ,

		cache_file_media_ids_chunk : function ( files ) {
			let me = this ;
			let params = {
				action:'query',
				prop:'info',
				titles:"File:"+files.join("|File:"),
				format:'json'
			} ;
			$.post('/w/api.php',params,function(d){
				let to_load = [] ;
				$.each ( d.query.pages , function ( page_id , page_info ) {
					if ( page_id == -1 ) return ; // Paranoia
					let mid = 'M'+page_id ;
					let file = page_info.title.replace(/^File:/,'').replace(/ /g,'_');
					me.mid_cache[file] = mid ;
					me.mid2file[mid] = file ;
					to_load.push ( mid ) ;
				} ) ;
				me.load_media_items(to_load);
			},'json');
		} ,

		load_media_items : function ( mids ) {
			if ( mids.length == 0 ) return ;
			let me = this ;
			let params = {
				action:'wbgetentities',
				ids:mids.join('|'),
				format:'json'
			} ;
			$.post('/w/api.php',params,function(d){
				$.each ( d.entities , function ( mid , mi ) {
					me.media_items[mid] = mi ;
					me.update_file_sdc(mid);
				} ) ;
				me.load_wikidata_items();
			},'json');
		} ,

		load_wikidata_items : function ( callback ) {
			let me = this ;
			let to_load = [] ; // Should never be more than 50
			$.each ( me.wikidata_items_to_load , function ( dummy , q ) {
				if ( typeof me.wikidata_item_labels[q] != 'undefined' ) {
					me.update_q_labels(q);
					return ;
				}
				if ( $.inArray(q,to_load) !== -1 ) return ;
				to_load.push(q) ;
			} ) ;
			me.wikidata_items_to_load = [] ;
			if ( to_load.length == 0 ) return ;
			$.getJSON('https://www.wikidata.org/w/api.php?callback=?',{
				action:'wbformatentities',
				ids:to_load.join('|'),
				format:'json'
			},function(d){
				$.each ( d.wbformatentities , function ( q , html ) {
					me.wikidata_item_labels[q] = html.replace('<a ','<a target="_blank" ') ;
					me.update_q_labels(q);
				} ) ;
				if ( typeof callback != 'undefined' ) callback() ;
			});
		} ,

		update_q_labels : function ( q ) {
			let me = this ;
			$('td.q_to_load[q="'+q.replace(/'/g, "&#39;")+'"]').removeClass('q_to_load').html(me.wikidata_item_labels[q]);
		} ,

		get_wikidata_item_label_td : function ( q , prominent ) {
			let me = this ;
			let style = 'vertical-align:top;' ;
			if ( prominent ) style += 'font-weight:bold;' ;
			if ( typeof me.wikidata_item_labels[q] == 'undefined' ) {
				return "<td class='q_to_load' q='"+q+"' style='"+style+"'>"+q+"</td>" ;
			} else {
				return "<td q='"+q+"' style='"+style+"'>"+me.wikidata_item_labels[q]+"</td>" ;
			}
		} ,

		update_file_sdc : function ( mid ) {
			let me = this ;
			let file = me.mid2file[mid] ;
			if ( typeof file == 'undefined' ) return ;
			let mi = me.media_items[mid] ;
			if ( typeof mi == 'undefined' ) return ;
			let out = [] ;
			$.each ( (mi.statements||{}) , function ( property , statements ) {
				$.each ( statements , function ( dummy , statement ) {
					if ( ((statement.mainsnak||{}).datavalue||{}).type != 'wikibase-entityid' ) return ;
					me.wikidata_items_to_load.push(property);
					let wd_item_id = statement.mainsnak.datavalue.value.id ;
					me.wikidata_items_to_load.push(wd_item_id);
					out.push({property:property,target:wd_item_id,prominent:statement.rank=='preferred'});
				} ) ;
			} ) ;

			let h = '' ;
			if ( out.length == 0 ) {
				let html = "<div class='sdc_statements' mid='"+mid+"' style='font-size:9pt;width:100%;text-align:left;'>" ;
				html += "<div style='color:#b32424;font-weight:bold;'>No SDC</div>" ;
				html += "</div>" ;
				h = $(html);
			} else {
				let html = "<div class='sdc_statements' mid='"+mid+"' style='font-size:9pt;width:100%;text-align:left;'>" ;
				html += "<table style='border-spacing:0;border-collapse:collapse;'><tbody>" ;
				$.each ( out , function ( dummy , row ) {
					html += "<tr>" ;
					html += me.get_wikidata_item_label_td(row.property,false) ;
					html += me.get_wikidata_item_label_td(row.target,row.prominent) ;
					html += "</tr>" ;
				} ) ;
				html += "</tbody></table></div>" ;
				h = $(html);
				h.find('td').css({padding:'1px','text-align':'left'});
			}

			let element = $($('input.sdc_checkbox[file="'+me.sanitize_file_attribute(file)+'"]').parent()) ;
			$('div.sdc_statements[mid="'+mid+'"').remove();
			element.after(h);
		} ,

		sanitize_file_attribute : function ( file ) {
		 	return encodeURIComponent(file).replace(/'/g, "&#39;") ;
		 } ,

		show_checkboxes : function () {
			let me = this ;
			let files = [] ;
			$('a.image').each(function(num,a){
				let file = decodeURIComponent($(a).attr('href')).replace(/^.+\/File:/,'') ;
				if ( $.inArray(file,me.exclude_files) > -1 ) return ; // Bad file!
				if ( $(a).parents('#wdinfobox').length > 0 ) return ; // In infobox
				files.push(file);
				let html = "<div class='sdc_checkbox_container' style='display:flex'>" ;
				html += "<div><input type='checkbox' class='sdc_checkbox' file='"+me.sanitize_file_attribute(file)+"' /></div>" ;
				html += "</div>" ;
				$(a).after(html);
			});
			me.cache_file_media_ids(files);
		}

	} ;

	sdc.init();
});