MediaWiki:Gadget-term-preview.js

Z Wikisłownika – wolnego słownika wielojęzycznego
Wersja z dnia 23:05, 7 kwi 2022 autorstwa Peter Bowman (dyskusja | edycje) (mw.viewport juz nie istnieje, przeklejam usunieta implementacje z GitHuba)

Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.

  • Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
  • Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
  • Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
  • Opera: Naciśnij klawisze Ctrl+F5.
var termsStorage = {},
	reInnerCapture = /^\((.+)\)$/,
	reSplit = / *, +/,
	reMain = /(\d+(?:[-–,\d]+)?)/,
	reFull = new RegExp( mw.format( '^$1(?:\\.$1)?$', reMain.source ) ),
	reRange = /^(\d+)[-–](\d+)$/,
	reTermNum = /^\((\d+)\.(\d+)\)$/,
	reTrailingSemicolon = /[\s;]+$/,
	$contentNode = $(),
	popupCloseDelay = 150;

function parseTerms( $entryPoint, localTerms ) {
	var $currTerms, currTerm,
		$currHeader = $entryPoint,
		firstLevel = 1,
		secondLevel = 1;
	
	while ( true ) {
		$currHeader = $currHeader.next( 'p' );
		$currTerms = $currHeader.next( 'dl' );
		
		if (
			!$currHeader.length || !$currHeader.hasClass( 'fldt-znaczenia' ) ||
			!$currTerms.length
		) {
			break;
		}
		
		currTerm = {
			header: $currHeader,
			terms: [],
			valid: true
		};
		
		$currTerms.children( 'dd' ).each( function ( i, dd ) {
			var $dd = $( dd ),
				$num = $dd.find( '.term-num' ).first(),
				matches = reTermNum.exec( $num.text() );
			
			if (
				!$num.length || matches === null ||
				Number( matches[ 1 ] ) !== firstLevel ||
				Number( matches[ 2 ] ) !== secondLevel
			) {
				currTerm.valid = false;
				return false;
			}
			
			currTerm.terms.push( $dd );
			secondLevel++;
		} );
		
		localTerms.push( currTerm );
		firstLevel++;
		secondLevel = 1;
		$currHeader = $currTerms;
	}
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap
function flatMap( arr, callback, thisArg ) {
	if ( 'flatMap' in Array.prototype ) {
		return Array.prototype.flatMap.call( arr, callback, thisArg );
	} else {
		return arr.map( callback, thisArg ).reduce( function ( acc, val ) {
			return acc.concat( val );
		}, [] );
	}
}

function parseNum( text ) {
	return text.replace( reInnerCapture, '$1' ).split( reSplit ).map( function ( val ) {
		return ( reFull.exec( val ) || [] ).splice( 1 ).filter( function ( capture ) {
			return capture !== undefined;
		} ).map( function ( group ) {
			return flatMap( group.split( ',' ), function ( subgroup ) {
				return Number( subgroup ) || expandRange( subgroup ) || 0;
			} );
		} );
	} ).filter( function ( el ) {
		return el.length && el.every( function ( ell ) {
			return ell.length;
		} );
	} );
}

function expandRange( expr ) {
	var first, last,
		m = reRange.exec( expr );
	
	if ( m === null ) {
		return null;
	}
	
	first = Number( m[ 1 ] );
	last = Number( m[ 2 ] );
	
	// https://stackoverflow.com/a/20066663
	return Array.apply( null, { length: last + 1 } ).map( Number.call, Number ).slice( first );
}

function selectTermEls( nums, termsArr ) {
	return nums[ 0 ].reduce( function ( $obj, lvl ) {
		var termObj = termsArr[ lvl - 1 ];
		
		if ( !termObj || !termObj.valid ) {
			return $obj;
		}
		
		$obj = $obj.add( termObj.header );
		
		if ( nums.length === 1 ) {
			termObj.terms.forEach( function ( term ) {
				$obj = $obj.add( term );
			} );
		} else {
			nums[ 1 ].forEach( function ( lvl2 ) {
				$obj = $obj.add( termObj.terms[ lvl2 - 1 ] );
			} );
		}
		
		return $obj;
	}, $() );
}

function populateStorage( code ) {
	var selFmt = 'dl:has(dt.lang-$1.fldt-znaczenia)',
		$entryPoint = $contentNode.find( mw.format( selFmt, code ) ),
		localTerms = [];
	
	if ( $entryPoint.length ) {
		parseTerms( $entryPoint, localTerms );
	}
	
	if ( localTerms.length ) {
		termsStorage[ code ] = localTerms;
	}
	
	return localTerms;
}

function makeContent( $terms ) {
	var html = '',
		startDefinition = false;
	
	$terms.each( function ( i, el ) {
		var $el = $( el ),
			isDefinition = $el.is( 'dd' );
		
		if ( !isDefinition ) {
			if ( html !== '' && !startDefinition ) {
				html += '</dl>';
			}
			
			html += $el.get( 0 ).outerHTML;
			startDefinition = true;
		} else {
			if ( startDefinition ) {
				html += '<dl>';
				startDefinition = false;
			}
			
			html += $el.get( 0 ).outerHTML;
		}
	} );
	
	return $( '<div>' ).html( html )
		.find( '.reference' ).remove().end() // przypisy
		.find( 'dd' ).contents().filter( function () {
			return this.nodeType === Node.TEXT_NODE;
		} ).each( function () {
			var $this = $( this ),
				$next = $this.next();
				
				// szablony 'wikipedia', 'Gloger' itp. oraz poprzedzający je średnik
				if ( $next.is( '.term-preview-ignore' ) ) {
					$next.remove();
					$this.replaceWith( function () {
						return this.textContent.replace( reTrailingSemicolon, '' );
					} );
				}
		} ).end().end().end();
}

function enablePreview( $num, code ) {
	var $terms, popup, timer,
		destroyPopup = function () {
			if ( popup ) {
				popup.$element.remove();
			}
			
			popup = null;
		};
	
	if ( !( code in termsStorage ) && !populateStorage( code ).length ) {
		return;
	}
	
	$terms = parseNum( $num.text() ).reduce( function ( $acc, nums ) {
		return $acc.add( selectTermEls( nums, termsStorage[ code ] ) );
	}, $() );
	
	if ( !$terms.length ) {
		return;
	}
	
	$num.on( 'click', function ( e ) {
		$( '.term-selected' ).removeClass( 'term-selected' );
		
		if ( !isElementInViewport( $terms.eq( 0 ) ) ) {
			$terms.first().get( 0 ).scrollIntoView();
		}
		
		$terms.addClass( 'term-selected' );
		
		$( document ).one( 'click', function ( e ) {
			if ( !$( e.target ).closest( $terms ).length ) {
				 $terms.removeClass( 'term-selected' );
			}
		} );
		
		return false;
	} ).on( 'mouseenter focus', function ( e ) {
		clearTimeout( timer );
		
		if ( !popup ) {
			mw.loader.using( 'oojs-ui-core' ).done( function () {
				popup = new OO.ui.PopupWidget( {
					$content: makeContent( $terms ),
					$floatableContainer: $num,
					padded: true,
					align: 'forwards',
					classes: [ 'term-preview-container' ]
				} );
				
				popup.$element.on( 'mouseenter focus', function ( e ) {
					clearTimeout( timer );
				} ).on( 'mouseleave blur', function ( e ) {
					timer = setTimeout( destroyPopup, popupCloseDelay );
				} ).appendTo( $contentNode );
				
				popup.toggle( true );
			} );
		}
	} ).on( 'mouseleave blur', function ( e ) {
		timer = setTimeout( destroyPopup, popupCloseDelay );
	} ).addClass( 'term-lookup' );
};

// https://github.com/wikimedia/mediawiki/blob/991980fc8a9ee4363d10c6a2aa7cc228c863d856/resources/src/mediawiki.viewport.js
function isElementInViewport( $el ) {
	var offset = $el.offset(),
		rect = {
			height: $el.height(),
			width: $el.width(),
			top: offset.top,
			left: offset.left
		},
		$window = $( window ),
		scrollTop = $window.scrollTop(),
		scrollLeft = $window.scrollLeft(),
		viewportRect = {
			top: scrollTop,
			left: scrollLeft,
			right: scrollLeft + $window.width(),
			bottom: ( window.innerHeight ? window.innerHeight : $window.height() ) + scrollTop
		};

	return (
		// Top border must be above viewport's bottom
		( viewportRect.bottom >= rect.top ) &&
		// Left border must be before viewport's right border
		( viewportRect.right >= rect.left ) &&
		// Bottom border must be below viewport's top
		( viewportRect.top <= rect.top + rect.height ) &&
		// Right border must be after viewport's left border
		( viewportRect.left <= rect.left + rect.width )
	);
}

if ( mw.config.get( 'wgNamespaceNumber' ) === 0 ) {
	mw.hook( 'wikipage.content' ).add( function ( $content ) {
		var re = /lang-([a-z-]+)/;
		
		termsStorage = {};
		$contentNode = $content;
		
		$content.find( '.term-num' ).filter( function () {
			var $defs = $( this ).closest( '.fldt-znaczenia' );
			return !$defs.length || this !== this.parentElement.firstElementChild;
		} ).each( function () {
			var $num = $( this ),
				matches = $num.attr( 'class' ).match( re ),
				code = matches && matches[ 1 ];
			
			if ( code ) {
				enablePreview( $num, code );
			}
		} );
	} );
}

module.exports = { enablePreview: enablePreview };