2014-01-09 01:32:13 +00:00
|
|
|
/*!
|
|
|
|
* VisualEditor UserInterface MWTocWidget class.
|
|
|
|
*
|
2023-12-01 16:06:11 +00:00
|
|
|
* @copyright See AUTHORS.txt
|
2014-01-09 01:32:13 +00:00
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a ve.ui.MWTocWidget object.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @extends OO.ui.Widget
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @param {ve.ui.Surface} surface
|
|
|
|
* @param {Object} [config] Configuration options
|
|
|
|
*/
|
2014-05-15 16:12:43 +00:00
|
|
|
ve.ui.MWTocWidget = function VeUiMWTocWidget( surface, config ) {
|
2014-01-09 01:32:13 +00:00
|
|
|
// Parent constructor
|
2016-08-22 21:44:59 +00:00
|
|
|
ve.ui.MWTocWidget.super.call( this, config );
|
2014-01-09 01:32:13 +00:00
|
|
|
|
|
|
|
// Properties
|
|
|
|
this.surface = surface;
|
|
|
|
this.doc = surface.getModel().getDocument();
|
2024-01-26 16:26:48 +00:00
|
|
|
this.metaList = this.doc.getMetaList();
|
2014-01-09 01:32:13 +00:00
|
|
|
// Topic level 0 lives inside of a toc item
|
2016-07-06 14:16:00 +00:00
|
|
|
this.rootLength = 0;
|
2014-01-09 01:32:13 +00:00
|
|
|
this.initialized = false;
|
|
|
|
// Page settings cache
|
|
|
|
this.mwTOCForce = false;
|
|
|
|
this.mwTOCDisable = false;
|
|
|
|
|
2016-07-06 14:16:00 +00:00
|
|
|
this.$tocList = $( '<ul>' );
|
2016-07-11 20:48:02 +00:00
|
|
|
this.$element.addClass( 'toc ve-ui-mwTocWidget ve-ce-focusableNode' ).append(
|
2016-07-06 14:16:00 +00:00
|
|
|
$( '<div>' ).addClass( 'toctitle' ).append(
|
|
|
|
$( '<h2>' ).text( ve.msg( 'toc' ) )
|
2014-01-09 01:32:13 +00:00
|
|
|
),
|
2016-07-06 14:16:00 +00:00
|
|
|
this.$tocList
|
2016-07-11 20:48:02 +00:00
|
|
|
).prop( 'contentEditable', 'false' );
|
2014-01-09 01:32:13 +00:00
|
|
|
|
2016-07-06 14:16:00 +00:00
|
|
|
// Setup toggle link
|
|
|
|
mw.hook( 'wikipage.content' ).fire( this.$element );
|
2014-01-09 01:32:13 +00:00
|
|
|
|
2016-07-06 14:16:00 +00:00
|
|
|
// Events
|
2014-01-09 01:32:13 +00:00
|
|
|
this.metaList.connect( this, {
|
2014-08-22 20:50:48 +00:00
|
|
|
insert: 'onMetaListInsert',
|
|
|
|
remove: 'onMetaListRemove'
|
2014-01-09 01:32:13 +00:00
|
|
|
} );
|
|
|
|
|
2018-06-10 15:00:37 +00:00
|
|
|
this.buildDebounced = ve.debounce( this.build.bind( this ) );
|
|
|
|
|
2014-01-09 01:32:13 +00:00
|
|
|
this.initFromMetaList();
|
|
|
|
this.build();
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Inheritance */
|
|
|
|
|
|
|
|
OO.inheritClass( ve.ui.MWTocWidget, OO.ui.Widget );
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to MetaList insert event to set TOC display options
|
|
|
|
*
|
|
|
|
* @param {ve.dm.MetaItem} metaItem
|
|
|
|
*/
|
2016-06-07 16:17:02 +00:00
|
|
|
ve.ui.MWTocWidget.prototype.onMetaListInsert = function ( metaItem ) {
|
2014-01-09 01:32:13 +00:00
|
|
|
// Responsible for adding UI components
|
2017-06-22 22:15:57 +00:00
|
|
|
if ( metaItem instanceof ve.dm.MWTOCMetaItem ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const property = metaItem.getAttribute( 'property' );
|
2017-06-22 22:15:57 +00:00
|
|
|
if ( property === 'mw:PageProp/forcetoc' ) {
|
|
|
|
this.mwTOCForce = true;
|
|
|
|
} else if ( property === 'mw:PageProp/notoc' ) {
|
|
|
|
this.mwTOCDisable = true;
|
|
|
|
}
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
2016-07-06 14:16:00 +00:00
|
|
|
this.updateVisibility();
|
2014-01-09 01:32:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to MetaList insert event to set TOC display options
|
|
|
|
*
|
|
|
|
* @param {ve.dm.MetaItem} metaItem
|
|
|
|
*/
|
|
|
|
ve.ui.MWTocWidget.prototype.onMetaListRemove = function ( metaItem ) {
|
2017-06-22 22:15:57 +00:00
|
|
|
if ( metaItem instanceof ve.dm.MWTOCMetaItem ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
const property = metaItem.getAttribute( 'property' );
|
2017-06-22 22:15:57 +00:00
|
|
|
if ( property === 'mw:PageProp/forcetoc' ) {
|
|
|
|
this.mwTOCForce = false;
|
|
|
|
} else if ( property === 'mw:PageProp/notoc' ) {
|
|
|
|
this.mwTOCDisable = false;
|
|
|
|
}
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
2016-07-06 14:16:00 +00:00
|
|
|
this.updateVisibility();
|
2014-01-09 01:32:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-12-09 16:47:13 +00:00
|
|
|
* Initialize TOC based on the presence of magic words
|
2014-01-09 01:32:13 +00:00
|
|
|
*/
|
|
|
|
ve.ui.MWTocWidget.prototype.initFromMetaList = function () {
|
2024-07-08 14:27:07 +00:00
|
|
|
const items = this.metaList.getItemsInGroup( 'mwTOC' );
|
|
|
|
if ( items.length === 0 ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for ( let i = 0; i < items.length; i++ ) {
|
|
|
|
if ( items[ i ] instanceof ve.dm.MWTOCMetaItem ) {
|
|
|
|
const property = items[ i ].getAttribute( 'property' );
|
|
|
|
if ( property === 'mw:PageProp/forcetoc' ) {
|
|
|
|
this.mwTOCForce = true;
|
|
|
|
}
|
|
|
|
if ( property === 'mw:PageProp/notoc' ) {
|
|
|
|
this.mwTOCDisable = true;
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-08 14:27:07 +00:00
|
|
|
this.updateVisibility();
|
2014-01-09 01:32:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Hides or shows the TOC based on page and default settings
|
|
|
|
*/
|
2016-07-06 14:16:00 +00:00
|
|
|
ve.ui.MWTocWidget.prototype.updateVisibility = function () {
|
2024-04-30 21:32:03 +00:00
|
|
|
// In MediaWiki if `__FORCETOC__` is anywhere TOC is always displayed
|
|
|
|
// ... Even if there is a `__NOTOC__` in the article
|
2016-07-06 14:16:00 +00:00
|
|
|
this.toggle( !this.mwTOCDisable && ( this.mwTOCForce || this.rootLength >= 3 ) );
|
2014-01-09 01:32:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rebuild TOC on ve.ce.MWHeadingNode teardown or setup
|
2016-07-06 14:16:00 +00:00
|
|
|
*
|
2018-06-10 15:00:37 +00:00
|
|
|
* Rebuilds on both teardown and setup of a node, so build is debounced
|
2014-01-09 01:32:13 +00:00
|
|
|
*/
|
2018-06-10 15:00:37 +00:00
|
|
|
ve.ui.MWTocWidget.prototype.rebuild = function () {
|
2016-07-06 14:16:00 +00:00
|
|
|
if ( this.initialized ) {
|
|
|
|
// Wait for transactions to process
|
2018-06-10 15:00:37 +00:00
|
|
|
this.buildDebounced();
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
2018-06-10 15:00:37 +00:00
|
|
|
};
|
2014-01-09 01:32:13 +00:00
|
|
|
|
2014-03-19 22:51:23 +00:00
|
|
|
/**
|
2016-07-06 14:16:00 +00:00
|
|
|
* Update the text content of a specific heading node
|
|
|
|
*
|
|
|
|
* @param {ve.ce.MWHeadingNode} viewNode Heading node
|
2014-03-19 22:51:23 +00:00
|
|
|
*/
|
2016-07-06 14:16:00 +00:00
|
|
|
ve.ui.MWTocWidget.prototype.updateNode = function ( viewNode ) {
|
|
|
|
if ( viewNode.$tocText ) {
|
|
|
|
viewNode.$tocText.text( viewNode.$element.text() );
|
2014-03-19 22:51:23 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-01-09 01:32:13 +00:00
|
|
|
/**
|
|
|
|
* Build TOC from mwHeading dm nodes
|
2016-07-06 14:16:00 +00:00
|
|
|
*
|
|
|
|
* Based on generateTOC in Linker.php
|
2014-01-09 01:32:13 +00:00
|
|
|
*/
|
|
|
|
ve.ui.MWTocWidget.prototype.build = function () {
|
2024-05-21 16:40:36 +00:00
|
|
|
const $newTocList = $( '<ul>' ),
|
2016-07-06 14:16:00 +00:00
|
|
|
nodes = this.doc.getNodesByType( 'mwHeading', true ),
|
2016-07-11 20:48:02 +00:00
|
|
|
surfaceView = this.surface.getView(),
|
|
|
|
documentView = surfaceView.getDocument(),
|
|
|
|
stack = [],
|
2022-12-12 19:49:26 +00:00
|
|
|
url = new URL( location.href );
|
2016-07-06 14:16:00 +00:00
|
|
|
|
2020-08-18 12:16:49 +00:00
|
|
|
function getItemIndex( $el, n ) {
|
|
|
|
return $el.children( 'li' ).length + ( n === stack.length - 1 ? 1 : 0 );
|
2016-07-06 14:16:00 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 13:21:26 +00:00
|
|
|
function linkClickHandler( /* heading */ ) {
|
2016-07-11 20:48:02 +00:00
|
|
|
surfaceView.focus();
|
2019-02-13 13:21:26 +00:00
|
|
|
// TODO: Impement heading scroll
|
2016-07-06 14:16:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-05-21 16:40:36 +00:00
|
|
|
let lastLevel = 0;
|
2024-05-21 14:22:56 +00:00
|
|
|
for ( let i = 0, l = nodes.length; i < l; i++ ) {
|
|
|
|
const modelNode = nodes[ i ];
|
|
|
|
const level = modelNode.getAttribute( 'level' );
|
2016-07-06 14:16:00 +00:00
|
|
|
|
|
|
|
if ( level > lastLevel ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
let $list;
|
2016-07-06 14:16:00 +00:00
|
|
|
if ( stack.length ) {
|
|
|
|
$list = $( '<ul>' );
|
|
|
|
stack[ stack.length - 1 ].children().last().append( $list );
|
2014-01-09 01:32:13 +00:00
|
|
|
} else {
|
2016-07-06 14:16:00 +00:00
|
|
|
$list = $newTocList;
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
2016-07-06 14:16:00 +00:00
|
|
|
stack.push( $list );
|
|
|
|
} else if ( level < lastLevel ) {
|
2024-05-21 14:22:56 +00:00
|
|
|
let levelDiff = lastLevel - level;
|
2016-07-06 14:16:00 +00:00
|
|
|
while ( levelDiff > 0 && stack.length > 1 ) {
|
|
|
|
stack.pop();
|
|
|
|
levelDiff--;
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
|
|
|
}
|
2016-07-06 14:16:00 +00:00
|
|
|
|
2024-05-21 14:22:56 +00:00
|
|
|
const tocNumber = stack.map( getItemIndex ).join( '.' );
|
|
|
|
const viewNode = documentView.getBranchNodeFromOffset( modelNode.getRange().start );
|
2022-12-12 19:49:26 +00:00
|
|
|
url.searchParams.set( 'section', ( i + 1 ).toString() );
|
2020-04-09 13:33:54 +00:00
|
|
|
// The following classes are used here:
|
|
|
|
// * toclevel-1, toclevel-2, ...
|
|
|
|
// * tocsection-1, tocsection-2, ...
|
2024-05-21 14:22:56 +00:00
|
|
|
const $item = $( '<li>' ).addClass( 'toclevel-' + stack.length ).addClass( 'tocsection-' + ( i + 1 ) );
|
|
|
|
const $link = $( '<a>' ).attr( 'href', url.toString() ).append(
|
2021-10-25 15:51:29 +00:00
|
|
|
$( '<span>' ).addClass( 'tocnumber' ).text( tocNumber )
|
|
|
|
);
|
2024-05-21 14:22:56 +00:00
|
|
|
const $text = $( '<span>' ).addClass( 'toctext' );
|
2016-07-06 14:16:00 +00:00
|
|
|
|
|
|
|
viewNode.$tocText = $text;
|
|
|
|
this.updateNode( viewNode );
|
|
|
|
|
|
|
|
stack[ stack.length - 1 ].append( $item.append( $link.append( $text ) ) );
|
|
|
|
$link.on( 'click', linkClickHandler.bind( this, viewNode ) );
|
|
|
|
|
|
|
|
lastLevel = level;
|
|
|
|
}
|
|
|
|
|
2016-07-11 20:48:02 +00:00
|
|
|
this.$tocList.empty().append( $newTocList.children() );
|
2016-07-06 14:16:00 +00:00
|
|
|
|
|
|
|
if ( nodes.length ) {
|
2016-07-11 20:48:02 +00:00
|
|
|
this.rootLength = this.$tocList.children().length;
|
2024-05-21 14:22:56 +00:00
|
|
|
const tocBeforeNode = documentView.getBranchNodeFromOffset( nodes[ 0 ].getRange().start );
|
2016-07-11 20:48:02 +00:00
|
|
|
tocBeforeNode.$element.before( this.$element );
|
2016-07-06 14:16:00 +00:00
|
|
|
} else {
|
|
|
|
this.rootLength = 0;
|
2014-01-09 01:32:13 +00:00
|
|
|
}
|
2016-07-06 14:16:00 +00:00
|
|
|
|
2014-01-09 01:32:13 +00:00
|
|
|
this.initialized = true;
|
2016-07-06 14:16:00 +00:00
|
|
|
this.updateVisibility();
|
2014-01-09 01:32:13 +00:00
|
|
|
};
|