User:So9q/CreateNewEntity.js

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.
/**
 * Forked from https://www.wikidata.org/w/index.php?title=User:Efly/Gadget-CreateNewItem.js&oldid=688915103 by [[User:Efly]]
 * @original_author Efly, forked by @author So9q to add support for lexemes.
--
 * Add a "create new item" link in the dropdown menu for when you want to
 * add an item to a property, but the item does not exist.
 * 
 * Known bugs:
 * 1) there is a little delay from the not-found message is displayed 
 * until we replace it. This is caused by a timeout hack to await population 
 * of the list.
 * 2) one click on "create new item" results in more than one tab opened (in Firefox). 
 * The cause of this is unknown
 *
 * https://phabricator.wikimedia.org/T107693
 * 
 * Install by adding: importScript('User:So9q/CreateNewEntity.js'); //Linkback: [[User:So9q/CreateNewEntity.js]] 
 * to your common.js
 *
 */

const EntityType = {
  Item: 'Item',
  Lexeme: 'Lexeme',
}

const languages = {
	"Q9035": {
		name: "Danish",
		code: "da"
	},
	"Q9027": {
		name: "Swedish",
		code: "sv"
	},
	"Q1860": {
		name: "English",
		code: "en"
	},
	"Q143": {
		name: "Esperanto",
		code: "eo"
	},
	"Q150": {
		name: "French",
		code: "fr"
	},
	"Q188": {
		name: "German",
		code: "de"
	},
	"Q652": {
		name: "Italian",
		code: "it"
	},
	"Q8641": {
		name: "Yiddish",
		code: "yi"
	},
	"Q2417210": {
		name: "Old Swedish",
		code: "mis"
	},
	"Q25433": {
		name: "Low German",
		code: "mis"
	},
	"Q35505": {
		name: "Old Norse",
		code: "mis"
	},
	"Q1163234": {
		name: "Medieval Latin",
		code: "mis",
	},
	"Q1503113": {
		name: "Late Latin",
		code: "mis",
	},
	"Q1248221": {
		name: "New Latin",
		code: "mis"
	},
	"Q1473289": {
		name: "Middle French",
		code: "mis"
	},
	"Q35497": {
		name: "Ancient Greek",
		code: "mis"
	},
	"Q36510": {
		name: "Modern Greek",
		code: "el"
	}
};

( function ( $, mw ) {
	'use strict';
	var newEntityLabel = "test";
	var newEntityLabelNotFound = "";

	function checkNotFound( textarea, firstLi ) {
		//console.log("checkNotFound running");
		//console.log("textarea is:");
		//console.log(textarea);
		if ($(textarea).hasClass("valueview-expert-wikibaseitem-input")) {
			newEntityLabelNotFound = "Not found. Click to create a new item";
		}
		else if ($(textarea).hasClass("valueview-expert-wikibaselexeme-input")) {
			newEntityLabelNotFound = "Not found. Click to create a new lexeme";
		}
		var replacementMade = false;
		/* Since we have no idea which entityselector list belongs to which 
		*  textarea we just pick the first and hope it is right
		*  see https://phabricator.wikimedia.org/T290298
		*/
		var li = $("li.ui-entityselector-notfound").first();
		if (li.length == 1){
			// Change the text
			var $innerA = $( li ).find( 'a' ).first();
			console.log($innerA);
			$innerA.on( 'click', function () {
				// why is this here?
				li.remove();
				createNewHandler( textarea );
				// close the dialog automatically after click
				return false;
			} );
			$innerA.text( newEntityLabelNotFound );
			replacementMade = true;
		}
		console.log("replacementMade:" + replacementMade);
		return replacementMade;
	}
	
    function appendLi(list, textarea){
		console.log("Appending to list");
		if ($(textarea).hasClass("valueview-expert-wikibaseitem-input")) {
			newEntityLabel = "Create new item";
		}
		else if ($(textarea).hasClass("valueview-expert-wikibaselexeme-input")) {
			newEntityLabel = "Create new lexeme";
		}
		$( list ).append(
				$('<li>')
					.prop( 'class', 'ui-ooMenu-item ui-ooMenu-customItem create-new-entity' )
					.prop( 'dir', 'auto' )
					.prop( 'tabindex', '-1' )
					.css('border-top-style', 'solid')
					.css('background-color', 'light blue')
				.append( $('<a>').prop( 'href', '#' )
					.css('text-align', 'right')
					.css('padding-right', '1.2em')
					.css('font-weight', 'bold')
					.text( newEntityLabel )
					.click( function() {
							createNewHandler( textarea );
							// Hide the oo-menu
							$( list ).hide();
							return false;
					})
				)
		);
	}	
	function openNewLexemePopup( textarea ) {
		console.log("openNewLexemePopup textarea is:");
		console.log(textarea);
		// .submit( requestDeletion )
		var currentLabel = $(textarea).val();
		console.log("openNewLexemePopup");
		mw.loader.using( 'oojs-ui-core' ).done( function () {
			function hide() {
				popup.toggle( false );
			}
			var h3 = $("<h3>").text("Add new lexeme");
			var div = $("<div>").prop('class','vector-menu-content')
				.css('padding-left', '5px')
				.css('padding-bottom', '10px')
				.css('direction', 'rtl');
			console.log("Generating the html");
			for (let key in languages) {
				const lexicalCategoryId = ['Q1084', 'Q24905', 'Q34698', 'Q184511', 'Q187931', 'Q62155', 'Q83034', 'Q111029'];
				const lexicalCategoryName = ['noun', 'verb', 'adjective', 'idiom', 'phrase', 'affix', 'interjection', 'root'];
				// one li for each language to hold the links,
				var li = $('<li>').text(languages[key]["name"] + ": ")
					.css('padding-left', '5px');
				$.each(lexicalCategoryId, function(j) {
					// Add the links
					$('<a>').text(lexicalCategoryName[j] + " ").click( function() {
						window.open('/wiki/Special:NewLexeme?lexeme-language='+key+
							'&lemma-language='+languages[key]["code"]+
							'&lexicalcategory='+lexicalCategoryId[j]+
							'&lemma='+currentLabel);
						hide();
					})
					.appendTo(li);
				});
				li.appendTo(div);
			}
			// Append the finished div
			var popupWindow = $('<div>').append(h3, div);
			var popup = new OO.ui.PopupWidget( {
				$content: popupWindow,
				padded: true,
				width: 450,
				head: true
			} );
			popup.$element.attr( 'style', 'z-index: 9999999;' );
			$(textarea).parent().append( popup.$element );
			popup.toggle( true );
		} );
	}
	// Measure when the function was last run to prevent opening multiple tabs 
	// on one click
    var t0 = performance.now();
    var t1 = 0;
    var count = 0;
    function createNewHandler( currentField ) {
    	// FIXME bug with handler running multiple times on one click
    	console.log("createNewHandler is running");
    	if ($(currentField).hasClass("valueview-expert-wikibaseitem-input")) {
    		window.open('/wiki/Special:NewItem?label='+ $(currentField).val());
			/* disabled
	        if (count == 0 && t1 == 0) {
	        	t1 = performance.now();
	        	console.log("opening window now");
	        	window.open('/wiki/Special:NewItem?label='+ $(currentField).val());
	        	count += 1;
	        } else {
	        	if (t1 - t0 > 2000) {
	        		console.log("resetting count");
	        		// reset count
	        		count = 0;
	        		t1 = 0;
	        		// call itself
	        		createNewHandler (currentField);
	        	} else {
	        		console.log("repeated opening of tab prevented");
	        	}
	        } */
	        
	    } else if ($(currentField).hasClass("valueview-expert-wikibaselexeme-input"))  {
            openNewLexemePopup( currentField );
	        //window.open('/wiki/Special:NewLexeme?lemma='+ currentField.val())
	    }
	    else {
	    	console.log("ERROR: Could not detect input type")
	    }
    }

	function appendToSuitableLexemeLists(textarea){
		// Goes through the lists and appends if not an unsuitable list
		var lists = $( "ul.ui-entityselector-list" );
    	$(lists).each(function(){
    		//console.log("Going through the lists");
			var listLength = $(this).children().length;
			// check if suggestions-special-menu
			var suggestionsSpecial = $(this).find("div.suggestions-special");
			//console.log(suggestionsSpecial);
			//console.log("suggestionsSpecial.length="+suggestionsSpecial.length);
			if (listLength > 0 && suggestionsSpecial.length === 0) {
				console.log(`Found suitable list with ${listLength} elements`);
				console.log(this);
				// Check if we already have appended to this list before
				var alreadyAppended = $(this).find(".create-new-entity")
				console.log(alreadyAppended)
				console.log("length:"+$(alreadyAppended).length)
				if ($(alreadyAppended).length === 0) {
					console.log("We did not append yet")
					// after pasting the LID we don't want the script to append
					if (listLength == 1) {
						var spans = $(this).find("span.ui-entityselector-label");
						// console.log(spans);
						var spanText = $(spans).first().text();
						// console.log("span="+spanText);
						// This matches the suggestion that has not been indexed yet
						// e.g. "L1 (L1)"
						var spanPattern = new RegExp("L\d+ \(L\d+\)");
						var lidFound = spanPattern.test(spanText); 
						if (lidFound) {
							// continue
							return;
						}
					}
					var inputValue = $(textarea).val().trim();
					// This matches e.g. "test (L1)"
					var nikkiPattern = new RegExp(".+ \(L\d+\)");
					var testNikkiSuggestion = nikkiPattern.test(inputValue);
					var nakedLid = new RegExp("L\d+");
					var testNakedLid = nakedLid.test(inputValue);
					if (inputValue != "") {
						if (testNikkiSuggestion === false && testNakedLid === false) {
							console.log("Value longer than 0 chars and is not a LID");
							appendLi(this, textarea);
						} else {
							if (testNikkiSuggestion === true){
								console.log(`testNikkiSuggestion was true for "${inputValue}"`);
							} else if (testNakedLid === true) {
								console.log(`testNakedLid was true for "${inputValue}"`);
							}
						}
					}
					
				} else {
					console.log("We already appended to this list");
				}
    		}
    	});
	}
	
	function init() {
		/***************************
		Handle main search input box
		****************************/
		// the searchbox only searches items so that is the only thing we 
		// support for now
		// inspired by https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
		// Select the node that will be observed for mutations
		/* DISABLED for now
		const targetNode = document.querySelector("#searchInput");
		//console.log(targetNode);
		// Options for the observer (which mutations to observe)
		const config = { attributes: true, childList: false, subtree: false };
		var value = "";
		// Callback function to execute when mutations are observed
		const callback = function(mutationsList, observer) {
		    // Use traditional 'for loops' for IE 11
		    for(const mutation of mutationsList) {
		        if (mutation.type === 'attributes') {
		            //console.log('The ' + mutation.attributeName + ' attribute was modified.');
		            if (mutation.attributeName == "class"){
		            	value = $(targetNode).val();
		            	// We only fire when the user has typed something in.
		            	if (value !== "") {
			            	console.log('class changed value is: ' + value);
							checkNotFound(targetNode, true);			            	
		            	}
		            }
		        }
		    }
		};
		// Create an observer instance linked to the callback function
		const observer = new MutationObserver(callback);
		
		// Start observing the target node for configured mutations
		observer.observe(targetNode, config);
		*/
		
		/**************************************
		Handle new lexeme/item statement input boxes
		***************************************/
		/*
		This is tricky because we only want to fire when the user is 
		trying to add a statement but just observing the textarea is not enough.
		We need to observe $("ul.ui-entityselector-list") for changes after the 
		user begins to add a statement
		*/
		console.log("Trying to observe new lexeme/item textareas");
		// "observer" from Elfly:
		var textarea;
		var willEntitySelectorListUpdate = false;
		$( document ).on( 'input propertychange paste', '.wikibase-snakview-value-container:has(.ui-suggester-input)', function () {
			textarea = $( this ).find( '.ui-suggester-input' ).first();
			// console.log("Entering: "+currentFieldText);
			willEntitySelectorListUpdate = true;
		} );
		$( document ).on( 'DOMSubtreeModified', '.ui-entityselector-list', function () {
			if ( willEntitySelectorListUpdate ) {
				// Now we know the user has started adding something
				var textareaType;
				if ($(textarea).hasClass("valueview-expert-wikibaselexeme-input")){
					textareaType = EntityType.Lexeme;
				}
				else if ($(textarea).hasClass("valueview-expert-wikibaseitem-input")){
					textareaType = EntityType.Item;
				}
				window.setTimeout(function () {
					var firstLi = $( this ).find( 'li' ).first();
					// debug
					console.log(firstLi);
		            if (textareaType == "Lexeme" && textarea.length > 0) {
		            	console.log("Found lexeme input textarea :)");
			    		console.log("Running checkNotFound");
			    		// First check if we got a not-found list
			    		var found = checkNotFound(textarea, firstLi);
			    		console.log("found was:"+found);
		            	if (!found) {
		            		// Append to bottom of suggester list
		            		appendToSuitableLexemeLists(textarea);
		            	}
		            	// Stop the loop
		            	willEntitySelectorListUpdate = false
		            } else if (textareaType == "Item" && textarea.length > 0){
		            	console.log("Found item input textarea :)");
		            	// TODO add checkNotFound
		            	var lists = $( "ul.ui-entityselector-list" );
		            	$(lists).each(function(){
		            		console.log("Running foreach");
							var listLength = $(this).children().length;
							// check if suggestions-special-menu
							var suggestionsSpecial = $(this).find("div.suggestions-special");
							//console.log(suggestionsSpecial);
							//console.log("suggestionsSpecial.length="+suggestionsSpecial.length);
							if (listLength > 0 && suggestionsSpecial.length === 0) {
								console.log(`Found suitable list with ${listLength} elements`);
								// remove earlier appends to this list
								$(this).find(".create-new-entity").remove();
								var inputValue = $(textarea).val().trim();
								if (inputValue != "") {
									console.log("Value longer than 0 chars");
									console.log("Running checkNotFound");
						    		// First check if we got a not-found list
						    		var found = checkNotFound(textarea);
						    		console.log("found was:"+found);
					            	if (!found) {
					            		// Append to bottom of suggester list
					            		appendLi(this, textarea);
								}}
				            	// Stop the loop
				            	willEntitySelectorListUpdate = false;
							} else if (suggestionsSpecial.length > 0){
								console.log("Skipping suggestion-special list with length:"+listLength);
							}
		            	});
		            } else {
		            	console.log("Could not find any input textarea");
		            }
		        // Timeout in miliseconds
				}, 250);
			}
		});
	}

	$( function () {
		mw.hook( 'wikibase.entityPage.entityLoaded' ).add( init );
	} );

}( jQuery, mediaWiki ) );