close
Jump to content

User:Jon Harald Søby/senseItemLabel.js

From Wikidata

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.
/**
 * Add item labels based on lexemes
 *
 * When adding P5137 (item for this sense) to lexemes, add buttons
 * to add the lexeme's lemma as a label for that item, if there is
 * no label for the item in that language, or if the label is
 * different.
 *
 * @version 1.2.0 (2025-06-18)
 * @author Jon Harald Søby
 */
mw.hook( 'wikibase.entityPage.entityLoaded' ).add( ( e ) => {
	'use strict';

	if ( e.type !== 'lexeme' ) {
		return;
	}

	mw.loader.using( [
		'mediawiki.api',
		'mediawiki.language',
		'mediawiki.util',
		'oojs',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui.styles.icons-interactions',
		'wikibase.WikibaseContentLanguages'
	]).then( async function() {

		const api = new mw.Api();

		await api.loadMessagesIfMissing( [ 'postedit-confirmation-published', 'wikibase-label-empty' ] );

		mw.util.addCSS( `
			.userjs-senseitemlabel-language {
				font-family: monospace, monospace;
			}
			.userjs-senseitemlabel-nolabel {
				color: var(--color-placeholder, #72777d);
				font-style: italic;
			}
			.userjs-senseitemlabel-oldlabel {
				color: var(--color-subtle, #54595d);
			}
			.userjs-senseitemlabel-newlabel {
				font-weight: bold;
			}`.replace( /\t\t\t/g, '' ) ); // Replace tabs for prettier output

		const ARROW = document.querySelector( 'html' ).getAttribute( 'dir' ) === 'ltr' ? '→' : '←';
		const ITEMFORSENSE = 'P5137';
		const userLanguage = mw.config.get( 'wgUserLanguage' );
		const validTermLanguages = wb.WikibaseContentLanguages.getTermLanguages()._languageCodes;

		async function lookUpLabels( items, languages ) {
			return api.get( {
				action: 'wbgetentities',
				format: 'json',
				formatversion: 2,
				ids: items,
				props: [ 'labels' ],
				languages: languages
			} );
		}

		function addLabel( item, language, oldLabel, newLabel ) {
			let query = {
				action: 'wbsetlabel',
				id: item,
				format: 'json',
				formatversion: 2,
				language: language,
				value: newLabel,
				summary: `Add label from [[Lexeme:${e.id}|${e.id}]] ([[User:Jon Harald Søby/senseItemLabel.js|senseItemLabel.js]])`
			};
			if ( oldLabel && oldLabel.toLowerCase() !== newLabel.toLowerCase() ) {
				let blob = {
					labels: {
						[ language ]: {
							language: language,
							value: newLabel
						}
					},
					aliases: {
						[ language ]: [
							{
								language: language,
								value: oldLabel,
								add: ''
							}
						]
					}
				};
				query.action = 'wbeditentity';
				query.data = JSON.stringify( blob );
				delete query.language;
				delete query.value;
			}

			return api.postWithEditToken( query );
		}

		function addLabelButton( sense, item, language, oldLabel, newLabel, disabled, icon, flags ) {
			const button = new OO.ui.ButtonWidget( {
				icon: icon,
				label: 'Add label',
				invisibleLabel: true,
				flags: flags,
				disabled: disabled
			} );
			button.on( 'click', () => {
				addLabel( item, language, oldLabel, newLabel ).then( ( res ) => {
					console.log( 'res', res );
					button.setIcon( 'check' );
					button.setDisabled( true );
					if ( language === userLanguage ) {
						const aElements = document.querySelectorAll( '[data-property-id=' + ITEMFORSENSE + '] a[title=' + item + ']' );
						aElements.forEach( ( aElement ) => {
							aElement.textContent = newLabel;
							aElement.removeAttribute( 'lang' );
							aElement.parentNode.querySelector( '.wb-language-fallback-indicator' ).remove();
						} );
					}
					mw.notify( mw.message( 'postedit-confirmation-published', mw.user, res.entity.lastrevid ).parseDom(), { type: 'success' } );
				});
			} );
			return button;
		}

		function buildLabelRow( sense, item, language, newLabel, oldLabel ) {
			const rowElement = document.createElement( 'tr' );

			const languageCell = document.createElement( 'td' );
			languageCell.classList.add( 'userjs-senseitemlabel-language' );
			languageCell.textContent = language;

			const buttonCell = document.createElement( 'td' );
			let buttonIcon = 'add';
			let buttonDisabled = false;
			let buttonFlags = [ 'primary', 'progressive' ];
			if ( !validTermLanguages.includes( language ) ) {
				buttonIcon = 'cancel';
				buttonDisabled = true;
				buttonFlags = [];
			}
			if ( newLabel === oldLabel ) {
				buttonIcon = 'check';
				buttonDisabled = true;
				buttonFlags = [];
			}
			let button = addLabelButton( sense, item, language, oldLabel, newLabel, buttonDisabled, buttonIcon, buttonFlags );
			buttonCell.appendChild( button.$element[ 0 ] );

			const labelCell = document.createElement( 'td' );
			labelCell.classList.add( 'userjs-senseitemlabel-label' );
			const oldLabelSpan = document.createElement( 'span' );
			oldLabelSpan.classList.add( 'userjs-senseitemlabel-oldlabel' );

			const newLabelSpan = document.createElement( 'span' );
			newLabelSpan.classList.add( 'userjs-senseitemlabel-newlabel' );
			newLabelSpan.textContent = newLabel;

			if ( oldLabel && oldLabel === newLabel ) {
				labelCell.append( newLabelSpan );
			} else if ( oldLabel ) {
				oldLabelSpan.textContent = oldLabel;
				labelCell.setAttribute( 'lang', mw.language.bcp47( language ) );
				labelCell.append( oldLabelSpan, ` ${ARROW} `, newLabelSpan );
			} else {
				oldLabelSpan.textContent = mw.msg( 'wikibase-label-empty' );
				oldLabelSpan.classList.add( 'userjs-senseitemlabel-nolabel' );
				oldLabelSpan.setAttribute( 'lang', mw.language.bcp47( userLanguage ) );
				newLabelSpan.setAttribute( 'lang', mw.language.bcp47( language ) );
				labelCell.append( oldLabelSpan, ` ${ARROW} `, newLabelSpan );
			}

			rowElement.append( languageCell, buttonCell, labelCell );

			return rowElement;
		}

		function buildLabelTable( sense, item, lemmas, existingLabels ) {
			const tableElement = document.createElement( 'table' );
			tableElement.classList.add( 'userjs-senseitemlabel-table' );
			let buttonIcon = 'add';
			let buttonDisabled = false;
			for ( const language in lemmas ) {
				const rowElement = buildLabelRow( sense, item, language, lemmas[ language ].value, existingLabels[ language ] );
				tableElement.appendChild( rowElement );
			}
			return tableElement;
		}

		const lemmas = e.lemmas;
		const senses = e.senses;

		const senseItems = {};

		for ( const sense of senses ) {
			if ( ITEMFORSENSE in sense.claims ) {
				for ( const item of sense.claims[ ITEMFORSENSE ] ) {
					senseItems[ item.mainsnak.datavalue.value.id ] = {};
				}
			}
		}

		const labelLookup = await lookUpLabels( Object.keys( senseItems ), Object.keys( lemmas ) );
		for ( const entity in labelLookup.entities ) {
			for ( const labelLang in labelLookup.entities[ entity ].labels ) {
				senseItems[ entity ][ labelLang ] = labelLookup.entities[ entity ].labels[ labelLang ].value;
			}
		}

		const senseItemStatementGroups = document.querySelectorAll( '[data-property-id=' + ITEMFORSENSE + ']' );
		senseItemStatementGroups.forEach( ( senseElement ) => {
			const senseId = senseElement.id.split( '-' )[ 0 ];
			const senseItemStatements = senseElement.querySelectorAll( '.wikibase-statementlistview-listview .wikibase-statementview' );

			senseItemStatements.forEach( ( statementElement ) => {
				const qid = statementElement.querySelector( '.wikibase-snakview-value a' ).getAttribute( 'title' );
				const indicatorElement = statementElement.querySelector( '.wikibase-snakview-indicators' );

				const table = buildLabelTable( senseId, qid, lemmas, senseItems[ qid ] );

				indicatorElement.appendChild( table );
			} );
		} );

		mw.hook( 'wikibase.statement.saved' ).add( async function ( entity, statement, oldStatement, newStatement ) {
			const propertyId = newStatement._claim._mainSnak._propertyId;

			if ( propertyId !== ITEMFORSENSE ) {
				return;
			}

			const guid = newStatement._claim._guid;
			const senseId = guid.split( '$' )[ 0 ].split( '-' )[ 1 ];
			const qid = newStatement._claim._mainSnak._value._serialization;
			const specialSenseItems = { [ qid ]: {} };

			const specialLabelLookup = await lookUpLabels( [ qid ], Object.keys( lemmas ) );
			for ( const labelLang in specialLabelLookup.entities[ qid ].labels ) {
				specialSenseItems[ qid ][ labelLang ] = specialLabelLookup.entities[ qid ].labels[ labelLang ].value;
			}

			const table = buildLabelTable( senseId, qid, lemmas, specialSenseItems[ qid ] );

			// Have to use [id=] instead of # because of JavaScript quirk related to the
			// dollar sign in the GUID.
			const indicatorElement = document.querySelector( `[id="${guid}"] .wikibase-snakview-indicators` );
			indicatorElement.appendChild( table );
		} );
	} );
} );