User:Magnus Manske/wdutil.js

From Wikidata
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>
/*
This is a helper script used by Authority control gadget, Mix'n'match gadget, and more
*/

var wdutil ;
var wdutil_app ;
var wdutil_loaded_callbacks ;
if ( typeof wdutil_loaded_callbacks=='undefined' ) wdutil_loaded_callbacks = [] ;

mw.loader.using(['vue', '@wikimedia/codex', 'jquery.ui', 'mediawiki.api'], function ( require ) {

	if ( typeof wdutil_app!='undefined' ) return ;

	const CdxTab = require( '@wikimedia/codex' ).CdxTab;
	const CdxTabs = require( '@wikimedia/codex' ).CdxTabs;

	wdutil = {
	api: mw.config.get('wgServer')+'/w/api.php' ,
	entity_cache : {} ,

	init : function ( require ) {
		let self = this ;
		self.q = mw.config.get("wgTitle") ;
		wdutil_loaded_callbacks.forEach ( function ( callback ) { self.loadCallback(callback) } ) ;
		wdutil_loaded_callbacks = [] ;
	} ,
	loadCallback : function ( callback ) {
		callback();
	},
	onError: function ( message ) {
		console.log("ERROR:",message);
		alert(message);
	} ,

	// wikibase methods
	loadEntity : function ( entity_id , callback ) {
		return this.loadEntities([entity_id],callback) ;
	} ,
	loadEntities : function ( entity_ids , callback ) {
		let self = this ;
		let entity_ids_to_load = entity_ids.filter(function(entity_id){return typeof self.entity_cache[entity_id]=='undefined'}) ;
		if ( entity_ids_to_load.length == 0 ) return callback();
		$.get ( self.api , {
			action : 'wbgetentities' ,
			format : 'json' ,
			ids : entity_ids_to_load.join('|') ,
		} , function ( data ) {
			if ( typeof data=='undefined' || typeof data.entities=='undefined' ) {
				console.log("wdutil::loadEntities",entity_ids_to_load,data);
			} else {
				$.each(data.entities,function(id,entity){
					self.entity_cache[id] = entity ;
				}) ;
				
			}
			callback();
		} , 'json' ) ;
	} ,
	getEntityName : function ( entity_id ) {
		let self = this ;
		if ( typeof self.entity_cache[entity_id] == 'undefined' ) return entity_id ;
		let p = self.entity_cache[entity_id] ;
		let lang = mw.config.get('wgUserLanguage') ;
		if ( typeof p.labels == 'undefined' ) return entity_id ;
		if ( typeof p.labels[lang] != 'undefined' ) return p.labels[lang].value ;
		lang = 'en' ;
		if ( typeof p.labels[lang] != 'undefined' ) return p.labels[lang].value ;
		return entity_id ;
	} ,
	getValuesForProperty : function ( entity , property ) {
		let ret = [] ;
		if ( typeof entity.claims=='undefined' ) return ret ;
		if ( typeof entity.claims[property]=='undefined' ) return ret ;
		$.each ( entity.claims[property] , function ( dummy , claim ) {
			if ( typeof claim.mainsnak=='undefined' ) return ;
			if ( typeof claim.mainsnak.datavalue=='undefined' ) return ;
			if ( typeof claim.mainsnak.datavalue.value=='undefined' ) return ;
			ret.push(claim.mainsnak.datavalue.value);
		} ) ;
		return ret ;
	} ,
	getPropertyDatatype : function ( property ) {
		let self = this ;
		return ((self.entity_cache[property]||{}).datatype||'') ;
	} ,


	// UI
	addItemStatement : function ( property , item_id , qualifiers , summary , callback ) {
		let self = this ;

		let item_id_numeric = item_id.replace ( /\D/g , '' ) * 1 ;
		let json_value = {"entity-type": "item","numeric-id": item_id_numeric,"id": item_id} ;

		// Create statement in item
		let data = { property:property , value:JSON.stringify(json_value) , type:'wikibase-entityid' , summary:summary , qualifiers:qualifiers } ;
		if ( qualifiers.length>0 ) data.qualifiers = self.buildQualifers(qualifiers);
		self.createStatementInItem ( data , function ( data ) {
			if ( (data.exists||false) ) return callback(false);
			if ( (data.success||0)!=1 ) { // Some error
				console.log("addItemStatement:",data);
				return callback(false);
			}
			self.loadEntities([property,item_id],function(){
				// TODO this interface looks like the real thing but it's not.
				if ( $("#"+property).length == 0 ) self.createPropertyContainerElement(property) ;
				let value_html = self.getValueHTML(property,item_id);
				self.createStatementElement ( property , value_html , '' , '' ) ;
				callback(true);
			}) ;
		} ) ;
	} ,

	addExternalIdStatement : function ( property , value , summary , callback ) {
		let self = this ;

		// Create statement in item
		let data = { property:property , value:JSON.stringify(value) , type:'string' , summary:summary } ;
		self.createStatementInItem ( data , function ( data ) {
			if ( (data.exists||false) ) return callback(false);
			if ( (data.success||0)!=1 ) { // Some error
				console.log("addExternalIdStatement:",data);
				return callback(false);
			}
			self.loadEntity(property,function(){
				// TODO this interface looks like the real thing but it's not.
				if ( $("#"+property).length == 0 ) self.createPropertyContainerElement(property) ;
				let value_html = self.getValueHTML(property,value);
				self.createStatementElement ( property , value_html , '' , '' ) ;
				callback(true);
			}) ;
		} ) ;
	} ,

	buildQualifers : function ( qualifiers ) {
		let self = this ;
		let ret = qualifiers.map(function(q){
			q[2] = q[1].replace(/\D/g,'')*1 ;
			return {snaktype:"value",property:q[0],datavalue:{value:{"entity-type": "item","numeric-id": q[2],"id": q[1]},type:'wikibase-entityid'}}
		}) ;
		return ret ;
	} ,
	getValueHTML : function ( property , value ) {
		let self = this ;
		let datatype = self.getPropertyDatatype(property);
		if ( datatype == 'external-id' ) {
			let text = $("<div>").text(value).html() ;
			let values = self.getValuesForProperty(self.entity_cache[property],'P1630') ;
			if ( values.length == 0 ) return text ;
			let url = values[0].replace('$1',encodeURIComponent(text)) ;
			let el = document.createElement('a');
			el.setAttribute('href',url);
			el.setAttribute('class','wb-external-id external');
			el.setAttribute('rel','nofollow');
			el.textContent = value ;
			let html = el.outerHTML ;
			return html;
		}
		if ( datatype == 'wikibase-item' ) {
			let text = $("<div>").text(value).html() ;
			//let values = self.getValuesForProperty(self.entity_cache[property],'P1630') ;
			//if ( values.length == 0 ) return text ;
			let url = '/wiki/'+value ;
			let el = document.createElement('a');
			el.setAttribute('href',url);
			el.setAttribute('title',value);
			el.textContent = self.getEntityName(value) ;
			let html = el.outerHTML ;
			return html;
		}
		if ( datatype == 'commonsMedia' ) {
			let image_url = 'https://commons.wikimedia.org/wiki/Special:Redirect/file/'+encodeURIComponent(value)+'?width=200';
			let el = document.createElement('img');
			el.setAttribute('src',image_url);
			el.setAttribute('title',value);
			el.setAttribute('style','max-height: 200px');
			let html = el.outerHTML ;
			return html;
		}
		// TODO better rendering for other cases
		return $("<div>").text(value).html() ;
	} ,
	createStatementInItem : function ( data , callback ) {
		let self = this ;
		if ( typeof data.summary=='undefined' ) data.summary = '' ;
		self.checkIfStatementExists ( data.property , data.value , function ( does_exist ) {
			if ( does_exist ) callback({success:1,exists: true,claim:{id:'dummy',mainsnak:{hash:'dummy'}}});
			else self.createStatementInItemMW(data,callback);
		} ) ;
	},
	getCreateStatementPayload : function ( data ) {
		let self = this ;
		let new_data = {
			claims:[
				{
					mainsnak:{
						snaktype: 'value',
						property: data.property,
						datavalue: {
							value: JSON.parse(data.value),
							type: data.type
						},
					},
					type: 'statement',
					rank: 'normal'
				}
			]
		} ;
		if ( (data.qualifiers||[]).length>0 ) {
			new_data.claims[0].qualifiers = data.qualifiers ;
		}
		let ret = {
			action: 'wbeditentity',
			id: self.q,
			data: JSON.stringify(new_data) ,
			summary: data.summary
		} ;
		return ret ;
	} ,
	// `value` is JSON.stringify(value in datavalue)
	checkIfStatementExists : function ( property , value , callback ) {
		let self = this ;
		let entity = self.q ;
		$.get ( self.api , {
			action : 'wbgetentities' ,
			format : 'json' ,
			ids : entity ,
			props : 'info|claims'
		} , function ( data ) {
			let exists = false ;
			if ( undefined !== data.entities[entity] ) {
				if ( undefined !== data.entities[entity].claims ) {
					if ( undefined !== data.entities[entity].claims[property] ) {
						let n = data.entities[entity].claims[property] ;
						$.each ( n , function ( k , v ) {
							if ( JSON.stringify(v.mainsnak.datavalue.value) == value ) exists = true ;
						} ) ;
					}
				}
			}
			callback(exists);
		} , 'json' ) ;
	} ,
	createStatementInItemMW : function ( data , callback ) {
		let self = this ;
		let payload = self.getCreateStatementPayload(data) ;
		let api = new mw.Api();
		api.postWithToken("csrf", payload ).done(function( token_data ) {
			callback(token_data) ;
		} ).fail( function(code, token_data) {
			console.log("createStatementInItemMW:",code,token_data);
			self.onError( api.getErrorMessage( token_data ).text());
		} );
	} ,
	createStatementElement : function ( property , value_html , uuid , statement_uuid ) {
		// TODO show qualifiers
		let self = this ;
		let html = `
<div id="`+uuid+`" class="wikibase-statementview wikibase-statement-`+uuid+` wb-normal listview-item wikibase-toolbar-item ui-droppable">
<div class="wikibase-statementview-rankselector"><div class="wikibase-rankselector ui-state-disabled">
<span class="ui-icon ui-icon-rankselector wikibase-rankselector-normal" title="Normal rank"></span>
</div></div>
<div class="wikibase-statementview-mainsnak-container">
<div class="wikibase-statementview-mainsnak" dir="auto"><div class="wikibase-snakview wikibase-snakview-`+statement_uuid+`">
<div class="wikibase-snakview-property-container">
<div class="wikibase-snakview-property" dir="auto"></div>
</div>
<div class="wikibase-snakview-value-container" dir="auto">
<div class="wikibase-snakview-typeselector"></div>
<div class="wikibase-snakview-body">
<div class="wikibase-snakview-value wikibase-snakview-variation-valuesnak">
`+
value_html
+`
</div>
<div class="wikibase-snakview-indicators"></div>
</div>
</div>
</div></div>
<div class="wikibase-statementview-qualifiers"></div>
</div>
<span class="wikibase-toolbar-container wikibase-edittoolbar-container"><span class="wikibase-toolbar wikibase-toolbar-item wikibase-toolbar-container"><span class="wikibase-toolbarbutton wikibase-toolbar-item wikibase-toolbar-button wikibase-toolbar-button-edit"><a href="#" title=""><span class="wb-icon"></span>edit</a></span></span></span>
<div class="wikibase-statementview-references-container">
<div class="wikibase-statementview-references-heading"><a class="ui-toggler ui-toggler-toggle ui-state-default ui-toggler-toggle-collapsed"><span class="ui-toggler-icon ui-icon ui-icon-triangle-1-e"></span><span class="ui-toggler-label">0 references</span></a><div class="wikibase-tainted-references-container" data-v-app=""><div class="wb-tr-app"><!----></div></div></div>
<div class="wikibase-statementview-references wikibase-initially-collapsed"><div class="wikibase-listview">
</div></div><div class="wikibase-addtoolbar wikibase-toolbar-item wikibase-toolbar wikibase-addtoolbar-container wikibase-toolbar-container"><span class="wikibase-toolbarbutton wikibase-toolbar-item wikibase-toolbar-button wikibase-toolbar-button-add"><a href="#" title=""><span class="wb-icon"></span>add reference</a></span></div></div>
</div>
</div>` ;
		setTimeout(function(){ self.onStatementAddition(property); },250);
		$("#"+property+" div.wikibase-statementlistview-listview").prepend(html);
	} ,
	onStatementAddition : function ( property ) {
		// Activate subsequent tools, if loaded
		if ( property=='P227' && typeof window.try_gnd_reveal=='function' ) window.try_gnd_reveal() ;
		if ( typeof window.try_ac2wd=='function' ) window.try_ac2wd() ;
	} ,
	createPropertyContainerElement : function ( property ) {
		let self = this ;
		let list = $('div.wikibase-statementgrouplistview');
		let datatype = self.getPropertyDatatype(property);
		if ( datatype == 'external-id' ) list = list.last();
		else list = list.first();
		let property_name = self.getEntityName(property) ;
		let html = `
<div class="wikibase-statementgroupview listview-item" id="`+property+`" data-property-id="`+property+`">
<div class="wikibase-statementgroupview-property">
<div class="wikibase-statementgroupview-property-label" dir="auto"><a title="Property:`+property+`" href="/wiki/Property:`+property+`">`+property_name+`</a>
<br><span>`+property+`</span></div>
</div>
<div class="wikibase-statementlistview">
<div class="wikibase-statementlistview-listview">
<!-- statements go here -->
</div>
<span class="wikibase-toolbar-container"></span>
<span class="wikibase-toolbar-wrapper">
<div class="wikibase-addtoolbar wikibase-toolbar-item wikibase-toolbar wikibase-addtoolbar-container wikibase-toolbar-container">
<span class="wikibase-toolbarbutton wikibase-toolbar-item wikibase-toolbar-button wikibase-toolbar-button-add">
<a href="#" title="Add a new value"><span class="wb-icon"></span>add value</a>
</span>
</div>
</span>
</div>
</div>` ;
		list.prepend(html)
	} ,

} ;

let wdUtilApp = {
    // Enable Vue 3 mode with compatConfig and compilerOptions
    compatConfig: {
        MODE: 3
    },
    compilerOptions: {
        whitespace: 'condense'
    },
    // The rest of your component definition goes here:
    components: {
        CdxTabs,
		CdxTab
    },
	data() {
		return {
			tabsData: [{name: '', label: '' , floating:false , dragging:false , last_mouse_position:{} }],
			currentTab: ''
		}
	} ,
	created : function () {
		let styles = `
		.wdutil-floating {
		  position: fixed;
		  width: 55rem;
		  background-color: white;
		  border: 2px solid #DDD;
		  padding:2px;
		  z-index: 5;
		  cursor: grab;
		  box-shadow: 10px 10px 3px #DDD;
		}`;
		let styleSheet = document.createElement("style")
		styleSheet.innerText = styles
		document.head.appendChild(styleSheet)
	} ,
    methods: {
        addTab : function ( tab , callback ) {
        	let self = this ;
        	$('#wdutil-wrapper').show();
    	    let id = '#wdutil-tab-'+tab.name;

        	// Check if tab exists
        	let existing_tabs = self.tabsData.filter(function(t){return t.name==tab.name}) ;
        	if ( existing_tabs.length > 0 ) {
        		self.currentTab = tab.name ;
        		self.alertWhenExists(id,callback);
        		return ;
        	}

        	if ( self.tabsData.length == 1 && self.tabsData[0].name=='' ) self.tabsData[0] = tab ;
        	else self.tabsData.push(tab);
    	    self.currentTab = tab.name ;
    	    self.alertWhenExists(id,callback);
        } ,
        toggleFloating : function () {
        	let self = this ;
        	self.floating = !self.floating ;
        	if ( self.floating ) $('#wdutil-wrapper').css({top:200,left:200}).addClass('wdutil-floating') ;
        	else $('#wdutil-wrapper').removeClass('wdutil-floating').css({top:0,left:0}) ;
        } ,
        alertWhenExists : function ( id , callback ) {
        	let self = this ;
        	if ( $(id).length > 0 ) return callback(id);
        	setTimeout ( function() {self.alertWhenExists(id,callback)} , 100 ) ;
        } ,
        mouseDown : function ( point ) {
        	this.dragging = this.floating ;
        	if ( this.dragging ) this.last_mouse_position = point ;
        } ,
        mouseUp : function ( point ) {
        	this.dragging = false ;
        } ,
        mouseMove : function ( event ) {
        	if ( !this.dragging ) return ;
        	let diff_x = event.clientX - this.last_mouse_position.clientX ;
        	let diff_y = event.clientY - this.last_mouse_position.clientY ;
        	let element_position = $('#wdutil-wrapper').position();
        	$('#wdutil-wrapper').css({
        		left:element_position.left+diff_x,
        		top:element_position.top+diff_y,
        	});
        	this.last_mouse_position = event ;
        } ,
    } ,
    template : `<div class='wdutil-tabs-container' @mousedown='mouseDown' @mouseup='mouseUp' @mousemove='mouseMove'>
    <div style='float:right;'>
    	<a @click.prevent='toggleFloating'>
	    	<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/9/94/Icon-Pinnnadel.svg/18px-Icon-Pinnnadel.svg.png' />
	    </a>
    </div>
    <cdx-tabs v-model:active="currentTab" :framed="framed">
    	<cdx-tab
			v-for="( tab, index ) in tabsData"
			:key="index"
			:name="tab.name"
			:label="tab.label"
		>
			<div :id="'wdutil-tab-'+tab.name" style="max-height: 20rem; overflow-y: auto;"></div>
		</cdx-tab>
    </cdx-tabs>
    </div>` ,
};

	$('#toc').before($('<div id="wdutil-wrapper" style="display:none" class="wikibase-entitytermsview"></div>'));
	let wdutil_app_base = Vue.createMwApp( wdUtilApp ) ;
	wdutil_app = wdutil_app_base.mount( '#wdutil-wrapper' );
	wdutil.init();
});

// </nowiki>