2014-01-09 01:32:13 +00:00
|
|
|
/*!
|
|
|
|
* VisualEditor UserInterface MWTocWidget class.
|
|
|
|
*
|
2020-01-08 17:13:04 +00:00
|
|
|
* @copyright 2011-2020 VisualEditor Team and others; 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-12-16 21:14:01 +00:00
|
|
|
|
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();
|
|
|
|
this.metaList = surface.getModel().metaList;
|
|
|
|
// 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 ) {
|
2017-06-22 22:15:57 +00:00
|
|
|
var property;
|
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 ) {
|
|
|
|
property = metaItem.getAttribute( 'property' );
|
|
|
|
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
|
|
|
var property;
|
|
|
|
if ( metaItem instanceof ve.dm.MWTOCMetaItem ) {
|
|
|
|
property = metaItem.getAttribute( 'property' );
|
|
|
|
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 () {
|
|
|
|
var i = 0,
|
|
|
|
items = this.metaList.getItemsInGroup( 'mwTOC' ),
|
2019-02-07 01:21:15 +00:00
|
|
|
len = items.length,
|
|
|
|
property;
|
2014-01-09 01:32:13 +00:00
|
|
|
if ( len > 0 ) {
|
|
|
|
for ( ; i < len; i++ ) {
|
2019-02-07 01:21:15 +00:00
|
|
|
if ( items[ i ] instanceof ve.dm.MWTOCMetaItem ) {
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2016-07-06 14:16:00 +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 () {
|
2014-01-09 01:32:13 +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 () {
|
2016-07-11 20:48:02 +00:00
|
|
|
var i, l, level, levelDiff, tocNumber, modelNode, viewNode, tocBeforeNode,
|
2016-07-06 14:16:00 +00:00
|
|
|
$list, $text, $item, $link,
|
|
|
|
$newTocList = $( '<ul>' ),
|
|
|
|
nodes = this.doc.getNodesByType( 'mwHeading', true ),
|
2016-07-11 20:48:02 +00:00
|
|
|
surfaceView = this.surface.getView(),
|
|
|
|
documentView = surfaceView.getDocument(),
|
2016-07-06 14:16:00 +00:00
|
|
|
lastLevel = 0,
|
2016-07-11 20:48:02 +00:00
|
|
|
stack = [],
|
|
|
|
uri = new mw.Uri();
|
2016-07-06 14:16:00 +00:00
|
|
|
|
|
|
|
function getItemIndex( $list, n ) {
|
|
|
|
return $list.children( 'li' ).length + ( n === stack.length - 1 ? 1 : 0 );
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
for ( i = 0, l = nodes.length; i < l; i++ ) {
|
|
|
|
modelNode = nodes[ i ];
|
|
|
|
level = modelNode.getAttribute( 'level' );
|
|
|
|
|
|
|
|
if ( level > lastLevel ) {
|
|
|
|
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 ) {
|
|
|
|
levelDiff = lastLevel - level;
|
|
|
|
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
|
|
|
|
|
|
|
tocNumber = stack.map( getItemIndex ).join( '.' );
|
|
|
|
viewNode = documentView.getBranchNodeFromOffset( modelNode.getRange().start );
|
2016-07-11 20:48:02 +00:00
|
|
|
uri.query.section = ( i + 1 ).toString();
|
2016-07-06 14:16:00 +00:00
|
|
|
$item = $( '<li>' ).addClass( 'toclevel-' + stack.length ).addClass( 'tocsection-' + ( i + 1 ) );
|
2016-07-11 20:48:02 +00:00
|
|
|
$link = $( '<a>' ).attr( 'href', uri )
|
|
|
|
.append( '<span class="tocnumber">' + tocNumber + '</span> ' );
|
2016-07-06 14:16:00 +00:00
|
|
|
$text = $( '<span>' ).addClass( 'toctext' );
|
|
|
|
|
|
|
|
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;
|
|
|
|
tocBeforeNode = documentView.getBranchNodeFromOffset( nodes[ 0 ].getRange().start );
|
|
|
|
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
|
|
|
};
|