Twinkle mobile test adapter v3: diagnostic banner
Twinkle mobile test adapter v4: pre-create p-cactions in overflow menu
Line 1: Line 1:
// Twinkle mobile TEST adapter v3 - DIAGNOSTIC version, personal test only
// Twinkle mobile TEST adapter v4 - personal test only
// Page: User:Bharatwiki Socrates/minerva.js (Minerva skin only)
// Page: User:Bharatwiki Socrates/minerva.js (Minerva skin only)
// Purpose: v1 loaded the files but no menu was visible; v2 could not find
// Root cause (found in Twinkle's own code + v3 census): on any skin
// any Twinkle menu. v3 does not guess: it shows a fixed on-screen log
// other than Vector/Timeless, Twinkle does NOT build its own menu.
// banner that records every stage and, after Twinkle initializes, a DOM
// It sends every module link to a portlet called "p-cactions", which
// census (does #p-cactions exist? does #p-twinkle exist? how many
// Minerva does not render, so the links go nowhere.
// twinkle-* items are in the page, and are they visible?).
// v4 fix: BEFORE loading Twinkle, create a real #p-cactions element
// The banner stays until tapped. All strings in English.
// inside Minerva's overflow (More) menu list (ul#p-tb), as a "Twinkle"
// item with a collapsible sub-list. Twinkle's links then land in that
// sub-list, where they can be seen and tapped.
// All user-facing strings in English.


( function () {
( function () {
Line 16: Line 19:
}
}
window.twinkleMobileTestLoaded = true;
window.twinkleMobileTestLoaded = true;
var banner = document.createElement( 'div' );
banner.id = 'twinkle-diag-banner';
banner.style.cssText = 'position:fixed;left:0;right:0;bottom:0;' +
'z-index:99999;background:#202122;color:#fff;font-size:12px;' +
'font-family:monospace;padding:8px;max-height:45vh;overflow:auto;';
banner.title = 'Tap to close';
banner.addEventListener( 'click', function () {
banner.remove();
} );
function log( msg ) {
var line = document.createElement( 'div' );
line.textContent = msg;
banner.appendChild( line );
}
function mountBanner() {
if ( document.body && !banner.parentNode ) {
document.body.appendChild( banner );
}
}
if ( document.body ) {
mountBanner();
} else {
document.addEventListener( 'DOMContentLoaded', mountBanner );
}
log( '[v3] adapter start, skin=' + mw.config.get( 'skin' ) );


var indexPhp = mw.util.wikiScript( 'index' );
var indexPhp = mw.util.wikiScript( 'index' );
Line 63: Line 40:
'.morebits-dialog { max-width: 96vw !important; }' +
'.morebits-dialog { max-width: 96vw !important; }' +
'.morebits-dialog-content { overflow-x: auto; }' +
'.morebits-dialog-content { overflow-x: auto; }' +
'.select2-container { max-width: 90vw; }'
'.select2-container { max-width: 90vw; }' +
'#p-cactions .twinkle-mobile-sub { list-style: none;' +
' margin: 0; padding: 0 0 0 16px; }' +
'#p-cactions .twinkle-mobile-sub li a { display: block;' +
' padding: 10px 8px; color: #202122; text-decoration: none; }' +
'#twinkle-mobile-panel { position: fixed; top: 56px; left: 2vw;' +
' width: 96vw; max-height: 70vh; overflow-y: auto; z-index: 999;' +
' background: #fff; border: 1px solid #a2a9b1; border-radius: 4px;' +
' box-shadow: 0 4px 16px rgba(0,0,0,0.3); padding: 6px 0; }' +
'#twinkle-mobile-panel ul { list-style: none; margin: 0;' +
' padding: 0; }' +
'#twinkle-mobile-panel li a { display: block;' +
' padding: 10px 16px; color: #202122; text-decoration: none; }'
);
);
rawCss( 'MediaWiki:Gadget-Twinkle.css' );
rawCss( 'MediaWiki:Gadget-Twinkle.css' );
Line 69: Line 58:
rawCss( 'MediaWiki:Gadget-select2.min.css' );
rawCss( 'MediaWiki:Gadget-select2.min.css' );


function describe( el ) {
function createPortlet() {
if ( !el ) {
if ( document.getElementById( 'p-cactions' ) ) {
return 'ABSENT';
return true;
}
var overflow = document.querySelector( 'ul#p-tb' ) ||
document.querySelector( '.page-actions-overflow-list' );
if ( overflow ) {
var li = document.createElement( 'li' );
li.className = 'toggle-list-item';
li.id = 'p-cactions';
var head = document.createElement( 'a' );
head.className = 'toggle-list-item__anchor';
head.href = '#';
var label = document.createElement( 'span' );
label.className = 'toggle-list-item__label';
label.textContent = 'Twinkle';
head.appendChild( label );
var sub = document.createElement( 'ul' );
sub.className = 'twinkle-mobile-sub';
sub.style.display = 'none';
head.addEventListener( 'click', function ( e ) {
e.preventDefault();
sub.style.display =
sub.style.display === 'none' ? 'block' : 'none';
} );
li.appendChild( head );
li.appendChild( sub );
overflow.appendChild( li );
return true;
}
}
var cs = window.getComputedStyle( el );
// Fallback for pages without an overflow menu:
var r = el.getBoundingClientRect();
// a panel that Twinkle links can fill, opened by a TW button.
return 'present, tag=' + el.tagName +
var panel = document.createElement( 'div' );
', li=' + el.querySelectorAll( 'li' ).length +
panel.id = 'twinkle-mobile-panel';
', display=' + cs.display +
panel.style.display = 'none';
', visibility=' + cs.visibility +
var wrap = document.createElement( 'div' );
', size=' + Math.round( r.width ) + 'x' +
wrap.id = 'p-cactions';
Math.round( r.height ) +
var list = document.createElement( 'ul' );
', parent=' + ( el.parentNode ? ( el.parentNode.id ||
wrap.appendChild( list );
el.parentNode.className || el.parentNode.tagName ) : '?' );
panel.appendChild( wrap );
document.body.appendChild( panel );
var btn = document.createElement( 'button' );
btn.textContent = 'TW';
btn.style.cssText = 'position:fixed;right:12px;bottom:64px;' +
'z-index:998;padding:10px 14px;border-radius:20px;' +
'background:#202122;color:#fff;border:0;';
btn.addEventListener( 'click', function () {
panel.style.display =
panel.style.display === 'none' ? 'block' : 'none';
} );
document.body.appendChild( btn );
return true;
}
}
function census( tag ) {
 
log( '--- census ' + tag + ' ---' );
function checkResult( tries ) {
log( 'window.Twinkle: ' + typeof window.Twinkle );
var landed = document.querySelectorAll(
log( '#p-cactions: ' +
'#p-cactions li[id^="twinkle-"]' ).length;
describe( document.getElementById( 'p-cactions' ) ) );
if ( landed ) {
log( '#p-twinkle: ' +
mw.notify( 'Twinkle is ready: open the More menu and tap ' +
describe( document.getElementById( 'p-twinkle' ) ) );
'Twinkle (' + landed + ' options)', { type: 'info' } );
log( '[id^="twinkle-"] count: ' +
return;
document.querySelectorAll( '[id^="twinkle-"]' ).length );
log( 'li[id^="ca-"] twinkle-ish: ' +
document.querySelectorAll( 'li[id^="ca-twinkle"]' ).length );
var items = document.querySelectorAll( '[id^="twinkle-"]' );
var names = [];
for ( var i = 0; i < items.length && i < 8; i++ ) {
names.push( items[ i ].id );
}
}
if ( names.length ) {
if ( tries > 0 ) {
log( 'sample ids: ' + names.join( ', ' ) );
setTimeout( function () { checkResult( tries - 1 ); }, 2000 );
return;
}
}
mw.notify( 'Twinkle loaded but its links did not arrive (v4)',
{ type: 'error' } );
}
function start() {
createPortlet();
mw.loader.using( [
'mediawiki.util', 'mediawiki.api', 'mediawiki.storage',
'mediawiki.Title', 'mediawiki.user', 'jquery.ui'
] ).then( function () {
return loadScript( 'MediaWiki:Gadget-select2.min.js' );
} ).then( function () {
return loadScript( 'MediaWiki:Gadget-morebits.js' );
} ).then( function () {
return loadScript( 'MediaWiki:Gadget-Twinkle.js' );
} ).then( function () {
checkResult( 10 );
} ).catch( function ( err ) {
mw.notify( 'Twinkle mobile test load failed: ' + err,
{ type: 'error' } );
} );
}
}


mw.loader.using( [
if ( document.readyState === 'loading' ) {
'mediawiki.util', 'mediawiki.api', 'mediawiki.storage',
document.addEventListener( 'DOMContentLoaded', start );
'mediawiki.Title', 'mediawiki.user', 'jquery.ui'
} else {
] ).then( function () {
start();
log( '[v3] core modules loaded (incl. jquery.ui)' );
}
return loadScript( 'MediaWiki:Gadget-select2.min.js' );
} ).then( function () {
log( '[v3] select2 loaded' );
return loadScript( 'MediaWiki:Gadget-morebits.js' );
} ).then( function () {
log( '[v3] morebits loaded' );
return loadScript( 'MediaWiki:Gadget-Twinkle.js' );
} ).then( function () {
log( '[v3] Twinkle.js loaded, waiting for its init...' );
var n = 0;
var timer = setInterval( function () {
n++;
census( 't+' + ( n * 3 ) + 's' );
if ( document.querySelectorAll( '[id^="twinkle-"]' ).length ||
n >= 5 ) {
clearInterval( timer );
log( '[v3] census done' );
}
}, 3000 );
} ).catch( function ( err ) {
log( '[v3] LOAD FAILED: ' + err );
} );
}() );
}() );

Revision as of 05:10, 1 November 2026

// Twinkle mobile TEST adapter v4 - personal test only
// Page: User:Bharatwiki Socrates/minerva.js (Minerva skin only)
// Root cause (found in Twinkle's own code + v3 census): on any skin
// other than Vector/Timeless, Twinkle does NOT build its own menu.
// It sends every module link to a portlet called "p-cactions", which
// Minerva does not render, so the links go nowhere.
// v4 fix: BEFORE loading Twinkle, create a real #p-cactions element
// inside Minerva's overflow (More) menu list (ul#p-tb), as a "Twinkle"
// item with a collapsible sub-list. Twinkle's links then land in that
// sub-list, where they can be seen and tapped.
// All user-facing strings in English.

( function () {
	if ( mw.config.get( 'skin' ) !== 'minerva' ) {
		return;
	}
	if ( window.twinkleMobileTestLoaded ) {
		return;
	}
	window.twinkleMobileTestLoaded = true;

	var indexPhp = mw.util.wikiScript( 'index' );
	function rawUrl( title ) {
		return indexPhp + '?title=' + encodeURIComponent( title ) +
			'&action=raw&ctype=text/javascript';
	}
	function rawCss( title ) {
		var link = document.createElement( 'link' );
		link.rel = 'stylesheet';
		link.href = indexPhp + '?title=' + encodeURIComponent( title ) +
			'&action=raw&ctype=text/css';
		document.head.appendChild( link );
	}
	function loadScript( title ) {
		return $.getScript( rawUrl( title ) );
	}

	mw.util.addCSS(
		'.ui-dialog { max-width: 96vw !important; left: 2vw !important; }' +
		'.morebits-dialog { max-width: 96vw !important; }' +
		'.morebits-dialog-content { overflow-x: auto; }' +
		'.select2-container { max-width: 90vw; }' +
		'#p-cactions .twinkle-mobile-sub { list-style: none;' +
		' margin: 0; padding: 0 0 0 16px; }' +
		'#p-cactions .twinkle-mobile-sub li a { display: block;' +
		' padding: 10px 8px; color: #202122; text-decoration: none; }' +
		'#twinkle-mobile-panel { position: fixed; top: 56px; left: 2vw;' +
		' width: 96vw; max-height: 70vh; overflow-y: auto; z-index: 999;' +
		' background: #fff; border: 1px solid #a2a9b1; border-radius: 4px;' +
		' box-shadow: 0 4px 16px rgba(0,0,0,0.3); padding: 6px 0; }' +
		'#twinkle-mobile-panel ul { list-style: none; margin: 0;' +
		' padding: 0; }' +
		'#twinkle-mobile-panel li a { display: block;' +
		' padding: 10px 16px; color: #202122; text-decoration: none; }'
	);
	rawCss( 'MediaWiki:Gadget-Twinkle.css' );
	rawCss( 'MediaWiki:Gadget-morebits.css' );
	rawCss( 'MediaWiki:Gadget-select2.min.css' );

	function createPortlet() {
		if ( document.getElementById( 'p-cactions' ) ) {
			return true;
		}
		var overflow = document.querySelector( 'ul#p-tb' ) ||
			document.querySelector( '.page-actions-overflow-list' );
		if ( overflow ) {
			var li = document.createElement( 'li' );
			li.className = 'toggle-list-item';
			li.id = 'p-cactions';
			var head = document.createElement( 'a' );
			head.className = 'toggle-list-item__anchor';
			head.href = '#';
			var label = document.createElement( 'span' );
			label.className = 'toggle-list-item__label';
			label.textContent = 'Twinkle';
			head.appendChild( label );
			var sub = document.createElement( 'ul' );
			sub.className = 'twinkle-mobile-sub';
			sub.style.display = 'none';
			head.addEventListener( 'click', function ( e ) {
				e.preventDefault();
				sub.style.display =
					sub.style.display === 'none' ? 'block' : 'none';
			} );
			li.appendChild( head );
			li.appendChild( sub );
			overflow.appendChild( li );
			return true;
		}
		// Fallback for pages without an overflow menu:
		// a panel that Twinkle links can fill, opened by a TW button.
		var panel = document.createElement( 'div' );
		panel.id = 'twinkle-mobile-panel';
		panel.style.display = 'none';
		var wrap = document.createElement( 'div' );
		wrap.id = 'p-cactions';
		var list = document.createElement( 'ul' );
		wrap.appendChild( list );
		panel.appendChild( wrap );
		document.body.appendChild( panel );
		var btn = document.createElement( 'button' );
		btn.textContent = 'TW';
		btn.style.cssText = 'position:fixed;right:12px;bottom:64px;' +
			'z-index:998;padding:10px 14px;border-radius:20px;' +
			'background:#202122;color:#fff;border:0;';
		btn.addEventListener( 'click', function () {
			panel.style.display =
				panel.style.display === 'none' ? 'block' : 'none';
		} );
		document.body.appendChild( btn );
		return true;
	}

	function checkResult( tries ) {
		var landed = document.querySelectorAll(
			'#p-cactions li[id^="twinkle-"]' ).length;
		if ( landed ) {
			mw.notify( 'Twinkle is ready: open the More menu and tap ' +
				'Twinkle (' + landed + ' options)', { type: 'info' } );
			return;
		}
		if ( tries > 0 ) {
			setTimeout( function () { checkResult( tries - 1 ); }, 2000 );
			return;
		}
		mw.notify( 'Twinkle loaded but its links did not arrive (v4)',
			{ type: 'error' } );
	}

	function start() {
		createPortlet();
		mw.loader.using( [
			'mediawiki.util', 'mediawiki.api', 'mediawiki.storage',
			'mediawiki.Title', 'mediawiki.user', 'jquery.ui'
		] ).then( function () {
			return loadScript( 'MediaWiki:Gadget-select2.min.js' );
		} ).then( function () {
			return loadScript( 'MediaWiki:Gadget-morebits.js' );
		} ).then( function () {
			return loadScript( 'MediaWiki:Gadget-Twinkle.js' );
		} ).then( function () {
			checkResult( 10 );
		} ).catch( function ( err ) {
			mw.notify( 'Twinkle mobile test load failed: ' + err,
				{ type: 'error' } );
		} );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', start );
	} else {
		start();
	}
}() );