User:Luca.favorido/linkypop.js
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)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* LinkyPop - From zero, to property hero!
* _ _ _ _____
* | | (_) | | | __ \
* | | _ _ __ | | ___ _| |__) ___ _ __
* | | | | '_ \| |/ | | | | ___/ _ \| '_ \
* | |____| | | | | <| |_| | | | (_) | |_) |
* |______|_|_| |_|_|\_\\__, |_| \___/| .__/
* __/ | | |
* |___/ |_|
* Easily *search an identifier* on an external site.
*
* Sample use cases:
*
* 1. You're adding an ORCID for a researcher. As soon as you type "ORCID" in
* the property input, an icon with a lens will be clickable.
* You click it, a new tab opens with the ORCID site looking for the name
* of the researcher. You copy the URL and paste it to Wikidata. Bang!
*
* 2. You're creating the element for a movie.
* * You type "IMDB" and the button to search immediately appears!
* * You type "TMDB" and the button to search immediately appears!
* * ...and so on.
*
*
* This gadget is still under test and development, so please write me for
* every issue you encounter or idea to evolve the tool! I'll be glad!
*
* <<< Special thanks to [User:Bargioni] >>>
*
* How to install: Copy and paste the following text to your page common.js.
* mw.loader.load('//www.wikidata.org/w/index.php?title=User:Luca.favorido/lookup_test.js&action=raw&ctype=text/javascript');
*/
(function(){ // scope for the script
const gadgetName = "LinkyPop";
const wgUserLanguage = mw.config.get( 'wgUserLanguage' );
const searchIconUrl = "//wikidata.org/w/skins/Vector/resources/skins.vector.styles.legacy/images/search.svg";
const entityUrl = (entityId) => `//www.wikidata.org/wiki/Special:EntityData/${entityId}.json`;
const propertyUrl = (propId) => `//www.wikidata.org/wiki/Property:${propId}`;
const propertyRegex = /[Property:](P\d+)/gm;
const log = (...props) => { console.log(`[${gadgetName}]`, ...props); };
// Constants from Wikidata
const searchUrlPropId = "P4354";
const sourceUrlPropId = "P1896";
/**
* Get property codes and labels, cache them in the localstorage dict
* PropertyName => PropertyID
*/
const setPropertiesAssociations = () => {
$("ul.ui-entityselector-list li a").each((i,e) => {
const matches = e.href.matchAll(propertyRegex);
const $propertyNode = $(e).find(".ui-entityselector-label").contents();
const text = $propertyNode == null || $propertyNode.get(0) == null ? '' : $propertyNode.get(0).nodeValue;
for (const match of matches) {
localStorage.setItem(`[${gadgetName}].[${text}]`, match[1]);
}
});
};
/**
* Parse all the key-values of property ID and name and save them into the
* local storage. e.g. ("MyIdentifier" : "P12345")
* When adding a new property, we have the property name, look for it in the
* local storage and return the property ID
* @param {Object} $fatherElem The JQuery obj where the property is read
* @returns {string} propertyId the identifier of the prop P12345
*/
const getPropertyValue = ($fatherElem) => {
const $selectedInput = $fatherElem.find("input.ui-entityselector-input-recognized");
const selectedProperty = $selectedInput.val();
// log('selectedProperty', selectedProperty);
const pValue = localStorage.getItem(`[${gadgetName}].[${selectedProperty}]`);
// log('pValue', pValue);
if (pValue != null) return pValue;
return localStorage.getItem(`[${gadgetName}].[${selectedProperty}]`);
};
/**
* Show a warning message below the $fatherElem
* @param {Object} $fatherElem The JQuery obj where the warning is added
* @param {string} message A message to display in the first line
* @param {string} suggestion A message to display in the second line
*/
const showWarning = ($fatherElem, message, suggestion) => {
const $placeToPutParent = $fatherElem.find(".wikibase-toolbar-button-lookup").parent();
const areThereOtherWarnings = Boolean($placeToPutParent.find(".lookup-warning").length);
if (areThereOtherWarnings) return;
const warningMessage = `<div class='lookup-warning' style='background: beige; padding: 0 4px;'>${message}<br />${suggestion}</div>`;
$placeToPutParent.append(warningMessage);
};
/**
* Handle cases when the property doesn't have the search URL
* e.g. Scopus identifier doesn't have a search URL.
* Display a message, and a link that help to add the search URL to the property
* statements.
* @param {Object} $fatherElem The JQuery obj where the warning is added
* @param {string} propId The ID of the property (e.g. P12345)
*/
const propertyDoesntHaveSearchUrl = ($fatherElem, propId) => {
const message = `Cannot search this element because the selected property ${propId} doesn't have any search URL associated (${searchUrlPropId}).`;
const suggestion = `You can <a href='${propertyUrl(propId)}#${searchUrlPropId}' target='_blank'>add one</a> if you wish!`; // TODO clear cache on add one click
showWarning($fatherElem, message, suggestion);
};
/**
* Given a property ID, look for it, and save in the cache details about it
* @param {string} propId The ID of the property (e.g. P12345)
* @return {Promise<void>} The property cache is populated, you can access it
*/
const investigateAboutProperty = (propId) => {
const property = localStorage.getItem(`[${gadgetName}].[${propId}]`);
// log(property);
if (property != null) { return Promise.resolve(); }
return fetch(entityUrl(propId))
.then(body => body.json())
.then(jsonResponse => {
const claims = jsonResponse.entities[propId].claims;
const isExternalIdentifier = jsonResponse.entities[propId].datatype === "external-id";
// log('isExternalIdentifier', isExternalIdentifier);
const searchProp = claims[searchUrlPropId];
const hasSearchUrl = searchProp != null && !["novalue","somevalue"].includes(searchProp[0].mainsnak.snaktype);
// log('searchProp', searchProp, '\n', 'hasSearchUrl', hasSearchUrl);
const searchUrl = hasSearchUrl ? searchProp[0].mainsnak.datavalue.value : null;
const sourceUrl = claims[sourceUrlPropId] != null && !["novalue","somevalue"].includes(claims[sourceUrlPropId][0].mainsnak.snaktype)
? claims[sourceUrlPropId][0].mainsnak.datavalue.value : null;
localStorage.setItem(`[${gadgetName}].[${propId}]`,
JSON.stringify({
isExternalIdentifier, hasSearchUrl, searchUrl, sourceUrl,
})
);
});
};
/**
* Quick and dirty way to get name of the element...
* TODO: I might use the JSON labels
* @return {string} The title of the element
*/
const getElementTitle = () => {
const pageTitle = $(".wikibase-title-label").text();
if (pageTitle != null) return pageTitle;
$(".wikibase-labelview-text").each((i,e) => { return $(e).text(); });
};
/**
* Add the UI control to lookup the identifier
* @param {Object} $placeToPutUi The JQuery obj where the UI elements are added
* @param {string} replacedSearchUrl The URL to follow when btn is clicked
* @param {string} onClick The action that must happen when btn is clicked
*/
const addControl = ($placeToPutUi, replacedSearchUrl, onClick) => {
// log($placeToPutUi);
const lookupButton = document.createElement("span");
lookupButton.classList.add("wikibase-toolbar-button-lookup","wikibase-toolbarbutton","wikibase-toolbar-item","wikibase-toolbar-button");
lookupButton.style.cssText = "float:right;position:relative;margin-right:12px;left:8px";
const a = document.createElement('a');
a.title = `${gadgetName} - Click to start searching this property`;
a.target = '_blank';
a.href = replacedSearchUrl;
a.onclick = onClick;
a.appendChild(getIcon());
lookupButton.appendChild(a);
// check another time if we must really insert the control, in case of async tasks
const alreadyInserted = Boolean($placeToPutUi.find(".wikibase-toolbar-button-lookup").length);
if(alreadyInserted) { return; }
$placeToPutUi.append(lookupButton);
$placeToPutUi.append("<div class='wikibase-toolbar-button-lookup' style='clear: both;'></div>");
};
/**
* Get the image icon for the UI
* @return {Element} The image icon for the UI
*/
const getIcon = () => {
const img = document.createElement('img');
img.src = searchIconUrl;
img.width = "16"; img.height = "16";
return img;
}
/**
* Starting point,
* If it's not a Wikidata item, do nothing 8-)
* On page load, add a mutation observer that catches the event of a new
* property added. On this event, bind a callback to add UI control.
* TODO: handle the edit tag case? //mw.hook('wikibase.statement.startEditing').add(log);
*/
mw.hook('wikibase.entityPage.entityLoaded').add((page) => {
// log(page);
if ($('.wikibase-entityview-main').length && typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(function(mutationList, observer) {
// log('some mutations on DOM', mutationList); // This log can be very spammy
const $newElemDiv = $("#mw-content-text div.listview-item.wikibase-statementgroupview.wb-new");
if ($newElemDiv.length) {
// log('you\'re inserting a new property to this element');
// Look for property names and IDs and cache them
setPropertiesAssociations();
$('input.ui-entityselector-input').each(function() {
const $container = $(this).closest(".wikibase-statementgroupview");
// log('$container',$container);
if(!$container.length){ return; }
$placeToPutUi = $container.find(".wikibase-snakview-property-container");
const alreadyInserted = Boolean($placeToPutUi.find(".wikibase-toolbar-button-lookup").length);
// log(alreadyInserted ? "UI control inserted" : "UI control NOT inserted YET");
const inputRecognized = Boolean($container.find(".ui-entityselector-input-recognized").length);
// log(inputRecognized ? 'property input recognized' : 'property still empty');
if (inputRecognized && !alreadyInserted) {
const propId = getPropertyValue($container);
// log("selectedProperty", propId);
investigateAboutProperty(propId)
.then(() => {
const property = JSON.parse(localStorage.getItem(`[${gadgetName}].[${propId}]`));
const replacedSearchUrl = property.hasSearchUrl ? property.searchUrl.replace("$1", getElementTitle()) : '';
const onClick = property.hasSearchUrl ? () => {} : (e) => { e.preventDefault(); propertyDoesntHaveSearchUrl($placeToPutUi, propId); };
if (property.isExternalIdentifier) addControl($placeToPutUi, replacedSearchUrl, onClick);
});
// TODO: catch issues like network error
}
});
}
});
const observeNode = $('.wikibase-entityview-main')[0];
if (!observeNode) return;
observer.observe(observeNode, {
childList: true,
attributes: false, // We don't care about attributes
subtree: true
});
}
});
})(); // end of scope