mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-23 11:32:51 +00:00
652e140dc5
I spent a couple minutes looking at this before I saw that we needed `this.updateVisibility()` to only fire if the loop iterated at least once. Change-Id: I7d03f73a35e3ded539898effa064dc0e14ba595f
228 lines
6.1 KiB
JavaScript
228 lines
6.1 KiB
JavaScript
/*!
|
|
* VisualEditor UserInterface MWTocWidget class.
|
|
*
|
|
* @copyright See AUTHORS.txt
|
|
* @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
|
|
*/
|
|
ve.ui.MWTocWidget = function VeUiMWTocWidget( surface, config ) {
|
|
// Parent constructor
|
|
ve.ui.MWTocWidget.super.call( this, config );
|
|
|
|
// Properties
|
|
this.surface = surface;
|
|
this.doc = surface.getModel().getDocument();
|
|
this.metaList = this.doc.getMetaList();
|
|
// Topic level 0 lives inside of a toc item
|
|
this.rootLength = 0;
|
|
this.initialized = false;
|
|
// Page settings cache
|
|
this.mwTOCForce = false;
|
|
this.mwTOCDisable = false;
|
|
|
|
this.$tocList = $( '<ul>' );
|
|
this.$element.addClass( 'toc ve-ui-mwTocWidget ve-ce-focusableNode' ).append(
|
|
$( '<div>' ).addClass( 'toctitle' ).append(
|
|
$( '<h2>' ).text( ve.msg( 'toc' ) )
|
|
),
|
|
this.$tocList
|
|
).prop( 'contentEditable', 'false' );
|
|
|
|
// Setup toggle link
|
|
mw.hook( 'wikipage.content' ).fire( this.$element );
|
|
|
|
// Events
|
|
this.metaList.connect( this, {
|
|
insert: 'onMetaListInsert',
|
|
remove: 'onMetaListRemove'
|
|
} );
|
|
|
|
this.buildDebounced = ve.debounce( this.build.bind( this ) );
|
|
|
|
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
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.onMetaListInsert = function ( metaItem ) {
|
|
// Responsible for adding UI components
|
|
if ( metaItem instanceof ve.dm.MWTOCMetaItem ) {
|
|
const property = metaItem.getAttribute( 'property' );
|
|
if ( property === 'mw:PageProp/forcetoc' ) {
|
|
this.mwTOCForce = true;
|
|
} else if ( property === 'mw:PageProp/notoc' ) {
|
|
this.mwTOCDisable = true;
|
|
}
|
|
}
|
|
this.updateVisibility();
|
|
};
|
|
|
|
/**
|
|
* Bound to MetaList insert event to set TOC display options
|
|
*
|
|
* @param {ve.dm.MetaItem} metaItem
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.onMetaListRemove = function ( metaItem ) {
|
|
if ( metaItem instanceof ve.dm.MWTOCMetaItem ) {
|
|
const property = metaItem.getAttribute( 'property' );
|
|
if ( property === 'mw:PageProp/forcetoc' ) {
|
|
this.mwTOCForce = false;
|
|
} else if ( property === 'mw:PageProp/notoc' ) {
|
|
this.mwTOCDisable = false;
|
|
}
|
|
}
|
|
this.updateVisibility();
|
|
};
|
|
|
|
/**
|
|
* Initialize TOC based on the presence of magic words
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.initFromMetaList = function () {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
this.updateVisibility();
|
|
};
|
|
|
|
/**
|
|
* Hides or shows the TOC based on page and default settings
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.updateVisibility = function () {
|
|
// In MediaWiki if `__FORCETOC__` is anywhere TOC is always displayed
|
|
// ... Even if there is a `__NOTOC__` in the article
|
|
this.toggle( !this.mwTOCDisable && ( this.mwTOCForce || this.rootLength >= 3 ) );
|
|
};
|
|
|
|
/**
|
|
* Rebuild TOC on ve.ce.MWHeadingNode teardown or setup
|
|
*
|
|
* Rebuilds on both teardown and setup of a node, so build is debounced
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.rebuild = function () {
|
|
if ( this.initialized ) {
|
|
// Wait for transactions to process
|
|
this.buildDebounced();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update the text content of a specific heading node
|
|
*
|
|
* @param {ve.ce.MWHeadingNode} viewNode Heading node
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.updateNode = function ( viewNode ) {
|
|
if ( viewNode.$tocText ) {
|
|
viewNode.$tocText.text( viewNode.$element.text() );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Build TOC from mwHeading dm nodes
|
|
*
|
|
* Based on generateTOC in Linker.php
|
|
*/
|
|
ve.ui.MWTocWidget.prototype.build = function () {
|
|
const $newTocList = $( '<ul>' ),
|
|
nodes = this.doc.getNodesByType( 'mwHeading', true ),
|
|
surfaceView = this.surface.getView(),
|
|
documentView = surfaceView.getDocument(),
|
|
stack = [],
|
|
url = new URL( location.href );
|
|
|
|
function getItemIndex( $el, n ) {
|
|
return $el.children( 'li' ).length + ( n === stack.length - 1 ? 1 : 0 );
|
|
}
|
|
|
|
function linkClickHandler( /* heading */ ) {
|
|
surfaceView.focus();
|
|
// TODO: Impement heading scroll
|
|
return false;
|
|
}
|
|
|
|
let lastLevel = 0;
|
|
for ( let i = 0, l = nodes.length; i < l; i++ ) {
|
|
const modelNode = nodes[ i ];
|
|
const level = modelNode.getAttribute( 'level' );
|
|
|
|
if ( level > lastLevel ) {
|
|
let $list;
|
|
if ( stack.length ) {
|
|
$list = $( '<ul>' );
|
|
stack[ stack.length - 1 ].children().last().append( $list );
|
|
} else {
|
|
$list = $newTocList;
|
|
}
|
|
stack.push( $list );
|
|
} else if ( level < lastLevel ) {
|
|
let levelDiff = lastLevel - level;
|
|
while ( levelDiff > 0 && stack.length > 1 ) {
|
|
stack.pop();
|
|
levelDiff--;
|
|
}
|
|
}
|
|
|
|
const tocNumber = stack.map( getItemIndex ).join( '.' );
|
|
const viewNode = documentView.getBranchNodeFromOffset( modelNode.getRange().start );
|
|
url.searchParams.set( 'section', ( i + 1 ).toString() );
|
|
// The following classes are used here:
|
|
// * toclevel-1, toclevel-2, ...
|
|
// * tocsection-1, tocsection-2, ...
|
|
const $item = $( '<li>' ).addClass( 'toclevel-' + stack.length ).addClass( 'tocsection-' + ( i + 1 ) );
|
|
const $link = $( '<a>' ).attr( 'href', url.toString() ).append(
|
|
$( '<span>' ).addClass( 'tocnumber' ).text( tocNumber )
|
|
);
|
|
const $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;
|
|
}
|
|
|
|
this.$tocList.empty().append( $newTocList.children() );
|
|
|
|
if ( nodes.length ) {
|
|
this.rootLength = this.$tocList.children().length;
|
|
const tocBeforeNode = documentView.getBranchNodeFromOffset( nodes[ 0 ].getRange().start );
|
|
tocBeforeNode.$element.before( this.$element );
|
|
} else {
|
|
this.rootLength = 0;
|
|
}
|
|
|
|
this.initialized = true;
|
|
this.updateVisibility();
|
|
};
|