mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-24 10:48:42 +00:00
Merge "Table of contents widget"
This commit is contained in:
commit
fbdc55dc78
|
@ -370,6 +370,7 @@ class VisualEditorHooks {
|
|||
$wgVisualEditorBrowserBlacklist,
|
||||
$wgVisualEditorSupportedSkins,
|
||||
$wgVisualEditorShowBetaWelcome,
|
||||
$wgVisualEditorEnableTocWidget,
|
||||
$wgVisualEditorPreferenceModules;
|
||||
|
||||
$vars['wgVisualEditorConfig'] = array(
|
||||
|
@ -387,6 +388,7 @@ class VisualEditorHooks {
|
|||
'tabPosition' => $wgVisualEditorTabPosition,
|
||||
'tabMessages' => $wgVisualEditorTabMessages,
|
||||
'showBetaWelcome' => $wgVisualEditorShowBetaWelcome,
|
||||
'enableTocWidget' => $wgVisualEditorEnableTocWidget
|
||||
);
|
||||
|
||||
foreach ( $wgVisualEditorPreferenceModules as $pref => $module ) {
|
||||
|
|
|
@ -584,6 +584,8 @@ $wgResourceModules += array(
|
|||
'modules/ve-mw/ui/ve.ui.MWDialog.js',
|
||||
|
||||
'modules/ve-mw/ui/widgets/ve.ui.MWTitleInputWidget.js',
|
||||
'modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js',
|
||||
'modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js',
|
||||
|
||||
'modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js',
|
||||
'modules/ve-mw/ui/dialogs/ve.ui.MWBetaWelcomeDialog.js',
|
||||
|
@ -657,7 +659,7 @@ $wgResourceModules += array(
|
|||
'visualeditor-viewpage-savewarning',
|
||||
'visualeditor-wikitext-warning-title',
|
||||
'visualeditor-window-title',
|
||||
|
||||
'toc',
|
||||
// Only used if FancyCaptcha is installed and triggered on save
|
||||
'captcha-label',
|
||||
'fancycaptcha-edit',
|
||||
|
@ -1171,6 +1173,9 @@ $wgVisualEditorSerializationCacheTimeout = 3600;
|
|||
// Namespaces to enable VisualEditor in
|
||||
$wgVisualEditorNamespaces = array_merge( $wgContentNamespaces, array( NS_USER ) );
|
||||
|
||||
// Whether to enable the (experimental for now) TOC widget
|
||||
$wgVisualEditorEnableTocWidget = false;
|
||||
|
||||
// List of skins VisualEditor integration supports
|
||||
$wgVisualEditorSupportedSkins = array( 'vector', 'apex', 'monobook', 'minerva' );
|
||||
|
||||
|
|
|
@ -27,6 +27,30 @@ OO.inheritClass( ve.ce.MWHeadingNode, ve.ce.HeadingNode );
|
|||
|
||||
ve.ce.MWHeadingNode.static.name = 'mwHeading';
|
||||
|
||||
/* Methods */
|
||||
|
||||
ve.ce.MWHeadingNode.prototype.onSetup = function () {
|
||||
// Parent method
|
||||
ve.ce.HeadingNode.prototype.onSetup.call( this );
|
||||
|
||||
// Make reference to the surface
|
||||
this.surface = this.root.getSurface().getSurface();
|
||||
this.rebuildToc();
|
||||
};
|
||||
|
||||
ve.ce.MWHeadingNode.prototype.onTeardown = function () {
|
||||
// Parent method
|
||||
ve.ce.HeadingNode.prototype.onTeardown.call( this );
|
||||
|
||||
this.rebuildToc();
|
||||
};
|
||||
|
||||
ve.ce.MWHeadingNode.prototype.rebuildToc = function () {
|
||||
if ( this.surface.mwTocWidget ) {
|
||||
this.surface.mwTocWidget.rebuild();
|
||||
}
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
||||
ve.ce.nodeFactory.register( ve.ce.MWHeadingNode );
|
||||
|
|
|
@ -346,6 +346,9 @@ ve.init.mw.ViewPageTarget.prototype.onSurfaceReady = function () {
|
|||
'history': 'updateToolbarSaveButtonState'
|
||||
} );
|
||||
this.surface.setPasteRules( this.constructor.static.pasteRules );
|
||||
if ( mw.config.get( 'wgVisualEditorConfig' ).enableTocWidget ) {
|
||||
this.surface.mwTocWidget = new ve.ui.MWTocWidget( this.surface );
|
||||
}
|
||||
|
||||
// Update UI
|
||||
this.transformPageTitle();
|
||||
|
@ -1233,7 +1236,7 @@ ve.init.mw.ViewPageTarget.prototype.mutePageContent = function () {
|
|||
* @method
|
||||
*/
|
||||
ve.init.mw.ViewPageTarget.prototype.hidePageContent = function () {
|
||||
$( '#bodyContent > :visible:not(#siteSub)' )
|
||||
$( '#bodyContent > :visible:not(#siteSub,.ve-ui-mwTocWidget)' )
|
||||
.addClass( 've-init-mw-viewPageTarget-content' )
|
||||
.hide();
|
||||
};
|
||||
|
@ -1247,7 +1250,7 @@ ve.init.mw.ViewPageTarget.prototype.showTableOfContents = function () {
|
|||
var $toc = $( '#toc' ),
|
||||
$wrap = $toc.parent();
|
||||
if ( $wrap.data( 've.hideTableOfContents' ) ) {
|
||||
$wrap.slideDown( function () {
|
||||
$wrap.show( function () {
|
||||
$toc.unwrap();
|
||||
} );
|
||||
}
|
||||
|
@ -1263,7 +1266,7 @@ ve.init.mw.ViewPageTarget.prototype.hideTableOfContents = function () {
|
|||
.wrap( '<div>' )
|
||||
.parent()
|
||||
.data( 've.hideTableOfContents', true )
|
||||
.slideUp();
|
||||
.hide();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -379,3 +379,27 @@
|
|||
.ve-ui-mwParameterResultWidget-description {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* ve.ui.MWTocWidget */
|
||||
|
||||
.ve-ui-mwTocWidget {
|
||||
/* Margin to mock the standard appearance of TOC */
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
.ve-ui-mwTocWidget .toctoggle {
|
||||
margin: 0.25em;
|
||||
}
|
||||
.ve-ui-mwTocWidget .toctoggle:before {
|
||||
content: ' [';
|
||||
}
|
||||
.ve-ui-mwTocWidget .toctoggle:after {
|
||||
content: '] ';
|
||||
}
|
||||
|
||||
.ve-ui-mwTocWidget .tocnumber:after {
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.ve-ui-mwTocWidget a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
88
modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js
Normal file
88
modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*!
|
||||
* VisualEditor UserInterface MWTocItemWidget class.
|
||||
*
|
||||
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an item an item for the MWTocWidget
|
||||
*
|
||||
* @class
|
||||
* @extends OO.ui.Widget
|
||||
* @mixins OO.ui.GroupElement
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} config TOC Item configuration
|
||||
* @cfg {ve.ce.Node} node ContentEditable node
|
||||
* @cfg {ve.ui.MWTocItemWidget} parent Parent toc item
|
||||
* @cfg {string} sectionPrefix TOC item section number
|
||||
* @cfg {number} tocLevel Depth level of the TOC item
|
||||
* @cfg {number} tocIndex Running count of TOC items
|
||||
*
|
||||
*/
|
||||
ve.ui.MWTocItemWidget = function VeCeMWTocItemWidget ( config ) {
|
||||
// Parent constructor
|
||||
OO.ui.Widget.call( this, config );
|
||||
|
||||
// Mixin Constructor
|
||||
OO.ui.GroupElement.call( this, this.$( '<ul>' ), config );
|
||||
|
||||
config = config || {};
|
||||
|
||||
// Properties
|
||||
this.node = config.node || null;
|
||||
this.parent = config.parent;
|
||||
this.sectionPrefix = config.sectionPrefix;
|
||||
this.tocLevel = config.tocLevel;
|
||||
this.tocIndex = config.tocIndex;
|
||||
|
||||
// Allows toc items to be optionally associated to a node.
|
||||
// For the case of the zero level parent item.
|
||||
if ( this.node ) {
|
||||
this.$tocNumber = this.$( '<span>' ).addClass( 'tocnumber' )
|
||||
.text( this.sectionPrefix );
|
||||
this.$tocText = this.$( '<span>' ).addClass( 'toctext' )
|
||||
.text( this.node.$element.text() );
|
||||
this.$element
|
||||
.addClass( 'toclevel-' + this.tocLevel )
|
||||
.addClass( 'tocsection-' + this.tocIndex )
|
||||
.append( this.$( '<a>' ).append( this.$tocNumber, this.$tocText ) );
|
||||
|
||||
// Monitor node events
|
||||
this.node.model.connect( this, { 'update': 'onUpdate' } );
|
||||
}
|
||||
this.$element.append( this.$group );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ui.MWTocItemWidget, OO.ui.Widget );
|
||||
|
||||
OO.mixinClass( ve.ui.MWTocItemWidget, OO.ui.GroupElement );
|
||||
|
||||
/* Static Properties */
|
||||
|
||||
ve.ui.MWTocItemWidget.static.tagName = 'li';
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Updates the text of the toc item
|
||||
*
|
||||
*/
|
||||
ve.ui.MWTocItemWidget.prototype.onUpdate = function () {
|
||||
// Timeout needed to let the dom element actually update
|
||||
setTimeout( ve.bind( function () {
|
||||
this.$tocText.text( this.node.$element.text() );
|
||||
}, this ), 0 );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes this toc item from its parent
|
||||
*
|
||||
*/
|
||||
ve.ui.MWTocItemWidget.prototype.remove = function () {
|
||||
this.node.model.disconnect( this );
|
||||
this.parent.removeItems( [this] );
|
||||
};
|
271
modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js
Normal file
271
modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js
Normal file
|
@ -0,0 +1,271 @@
|
|||
/*!
|
||||
* VisualEditor UserInterface MWTocWidget class.
|
||||
*
|
||||
* @copyright 2011-2014 VisualEditor Team and others; 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
|
||||
OO.ui.Widget.call( this, config );
|
||||
|
||||
// Properties
|
||||
this.surface = surface;
|
||||
this.doc = surface.getModel().getDocument();
|
||||
this.metaList = surface.getModel().metaList;
|
||||
// Topic level 0 lives inside of a toc item
|
||||
this.topics = new ve.ui.MWTocItemWidget();
|
||||
// Place for a cloned previous toc to live while rebuilding.
|
||||
this.$tempTopics = this.$( '<ul>' );
|
||||
// Section keyed item map
|
||||
this.items = {};
|
||||
this.initialized = false;
|
||||
// Page settings cache
|
||||
this.mwTOCForce = false;
|
||||
this.mwTOCDisable = false;
|
||||
|
||||
// TODO: fix i18n
|
||||
this.toggle = {
|
||||
'hideMsg': ve.msg( 'hidetoc' ),
|
||||
'showMsg': ve.msg( 'showtoc' ),
|
||||
'$link': this.$( '<a class="internal" id="togglelink"></a>' ).text( ve.msg( 'hidetoc' ) ),
|
||||
'open': true
|
||||
};
|
||||
this.$element.addClass( 'toc ve-ui-mwTocWidget' ).append(
|
||||
this.$( '<div>' ).attr( 'id', 'toctitle' ).append(
|
||||
this.$( '<h2>' ).text( ve.msg( 'toc' ) ),
|
||||
this.$( '<span>' ).addClass( 'toctoggle' ).append( this.toggle.$link )
|
||||
),
|
||||
this.topics.$group, this.$tempTopics
|
||||
);
|
||||
// Place in bodyContent element, which is close to where the TOC normally lives in the dom
|
||||
// Integration ignores hiding the TOC widget, though continues to hide the real page TOC
|
||||
$( '#bodyContent' ).append( this.$element );
|
||||
|
||||
this.toggle.$link.on( 'click', ve.bind( function () {
|
||||
if ( this.toggle.open ) {
|
||||
this.toggle.$link.text( this.toggle.showMsg );
|
||||
this.toggle.open = false;
|
||||
} else {
|
||||
this.toggle.$link.text( this.toggle.hideMsg );
|
||||
this.toggle.open = true;
|
||||
}
|
||||
this.topics.$group.add( this.$tempTopics ).slideToggle();
|
||||
}, this ) );
|
||||
|
||||
this.metaList.connect( this, {
|
||||
'insert': 'onMetaListInsert',
|
||||
'remove': 'onMetaListRemove'
|
||||
} );
|
||||
|
||||
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.MWTOCForceMetaItem ) {
|
||||
// show
|
||||
this.mwTOCForce = true;
|
||||
} else if ( metaItem instanceof ve.dm.MWTOCDisableMetaItem ) {
|
||||
// hide
|
||||
this.mwTOCDisable = true;
|
||||
}
|
||||
this.hideOrShow();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.MWTOCForceMetaItem ) {
|
||||
this.mwTOCForce = false;
|
||||
} else if ( metaItem instanceof ve.dm.MWTOCDisableMetaItem ) {
|
||||
this.mwTOCDisable = false;
|
||||
}
|
||||
this.hideOrShow();
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize TOC based on the presense of magic words
|
||||
*/
|
||||
ve.ui.MWTocWidget.prototype.initFromMetaList = function () {
|
||||
var i = 0,
|
||||
items = this.metaList.getItemsInGroup( 'mwTOC' ),
|
||||
len = items.length;
|
||||
if ( len > 0 ) {
|
||||
for ( ; i < len; i++ ) {
|
||||
if ( items[i] instanceof ve.dm.MWTOCForceMetaItem ) {
|
||||
this.mwTOCForce = true;
|
||||
}
|
||||
// Needs testing
|
||||
if ( items[i] instanceof ve.dm.MWTOCDisableMetaItem ) {
|
||||
this.mwTOCDisable = true;
|
||||
}
|
||||
}
|
||||
this.hideOrShow();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides or shows the TOC based on page and default settings
|
||||
*/
|
||||
ve.ui.MWTocWidget.prototype.hideOrShow = function () {
|
||||
// In MediaWiki if __FORCETOC__ is anywhere TOC is always displayed
|
||||
// ... Even if there is a __NOTOC__ in the article
|
||||
if ( !this.mwTOCDisable && ( this.mwTOCForce || this.topics.items.length >= 3 ) ) {
|
||||
this.$element.show();
|
||||
} else {
|
||||
this.$element.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rebuild TOC on ve.ce.MWHeadingNode teardown or setup
|
||||
* Rebuilds on both teardown and setup of a node, so rebuild is debounced
|
||||
*/
|
||||
ve.ui.MWTocWidget.prototype.rebuild = ve.debounce( function () {
|
||||
var item;
|
||||
// Only rebuild when initialized
|
||||
if ( this.surface.mwTocWidget.initialized ) {
|
||||
this.$tempTopics.append( this.topics.$group.children().clone() );
|
||||
for ( item in this.items ) {
|
||||
this.items[item].remove();
|
||||
delete this.items[item];
|
||||
}
|
||||
this.items = {};
|
||||
// Build after transactions
|
||||
setTimeout( ve.bind( function () {
|
||||
this.build();
|
||||
this.$tempTopics.empty();
|
||||
}, this ), 0 );
|
||||
}
|
||||
}, 0 );
|
||||
|
||||
/**
|
||||
* Build TOC from mwHeading dm nodes
|
||||
*/
|
||||
ve.ui.MWTocWidget.prototype.build = function () {
|
||||
var nodes = this.doc.selectNodes( new ve.Range( 0, this.doc.getDocumentNode().getLength() ), 'leaves' ),
|
||||
i = 0,
|
||||
headingLevel = 0,
|
||||
previousHeadingNode = null,
|
||||
previousHeadingLevel = 0,
|
||||
parentHeadingLevel = 0,
|
||||
levelSkipped = false,
|
||||
tocNumber = 0,
|
||||
tocLevel = 0,
|
||||
tocSection = 0,
|
||||
tocIndex = 0,
|
||||
sectionPrefix = [],
|
||||
parentSectionArray,
|
||||
key,
|
||||
parent,
|
||||
config,
|
||||
headingOuterRange,
|
||||
ceNode;
|
||||
for ( ; i < nodes.length; i++ ) {
|
||||
if ( nodes[i].node.parent === previousHeadingNode ) {
|
||||
// Duplicate heading
|
||||
continue;
|
||||
}
|
||||
if ( nodes[i].node.parent.getType() === 'mwHeading' ) {
|
||||
tocIndex++;
|
||||
headingLevel = nodes[i].node.parent.getAttribute( 'level' );
|
||||
// MW TOC Generation
|
||||
// The first heading will always be be a zero level topic, even heading levels > 2
|
||||
// If heading level is 1 then it is definitely a zero level topic
|
||||
// If heading level is 2 then it is a zero level topic, unless a child of a 1 level
|
||||
// If heading went up and skipped a number, the following headings of the skipped number are in the same level
|
||||
if ( this.topics.items.length === 0 || headingLevel === 1 || ( headingLevel === 2 && parentHeadingLevel !== 1 ) ) {
|
||||
tocSection++;
|
||||
sectionPrefix = [ tocSection ];
|
||||
tocLevel = 0;
|
||||
// reset t
|
||||
levelSkipped = false;
|
||||
parent = this.topics;
|
||||
parentHeadingLevel = headingLevel;
|
||||
} else {
|
||||
// If previously skipped a level, place this heading in the same level as the previous higher one
|
||||
if ( headingLevel === previousHeadingLevel || headingLevel < previousHeadingLevel && levelSkipped ) {
|
||||
tocNumber++;
|
||||
sectionPrefix.pop();
|
||||
sectionPrefix.push( tocNumber );
|
||||
// Only remove the flag if the heading level has dropped but we skipped to a higher number previously
|
||||
if ( headingLevel < previousHeadingLevel ) {
|
||||
levelSkipped = false;
|
||||
}
|
||||
} else {
|
||||
tocNumber = 1;
|
||||
// Heading not the same as before
|
||||
if ( headingLevel > previousHeadingLevel ) {
|
||||
// Did we skip a level? Flag in case we drop down a number
|
||||
if ( headingLevel - previousHeadingLevel > 1 ) {
|
||||
levelSkipped = true;
|
||||
}
|
||||
tocLevel++;
|
||||
sectionPrefix.push( tocNumber );
|
||||
// Step to lower level unless we are at 1
|
||||
} else if ( headingLevel < previousHeadingLevel && tocLevel !== 1 ) {
|
||||
tocLevel--;
|
||||
sectionPrefix.pop();
|
||||
tocNumber = sectionPrefix[sectionPrefix.length - 1] + 1;
|
||||
sectionPrefix.pop();
|
||||
sectionPrefix.push( tocNumber );
|
||||
}
|
||||
}
|
||||
}
|
||||
// Determine parent
|
||||
parentSectionArray = sectionPrefix.slice( 0 );
|
||||
parentSectionArray.pop();
|
||||
if ( parentSectionArray.length > 0 ) {
|
||||
key = parentSectionArray.join( '.' );
|
||||
parent = this.items[key];
|
||||
} else {
|
||||
// Topic level is zero
|
||||
parent = this.topics;
|
||||
}
|
||||
// TODO: Cleanup config generation, merge local vars into config object
|
||||
// Get CE node for the heading
|
||||
headingOuterRange = nodes[i].nodeOuterRange;
|
||||
ceNode = this.surface.getView().getDocument().getNodeFromOffset( headingOuterRange.end );
|
||||
config = {
|
||||
'node': ceNode,
|
||||
'tocIndex': tocIndex,
|
||||
'parent': parent,
|
||||
'tocLevel': tocLevel,
|
||||
'tocSection': tocSection,
|
||||
'sectionPrefix': sectionPrefix.join( '.' ),
|
||||
'insertIndex': sectionPrefix[sectionPrefix.length - 1]
|
||||
};
|
||||
// Add item
|
||||
this.items[sectionPrefix.join( '.' )] = new ve.ui.MWTocItemWidget( config );
|
||||
config.parent.addItems( [this.items[sectionPrefix.join( '.' )]], config.insertIndex );
|
||||
previousHeadingLevel = headingLevel;
|
||||
previousHeadingNode = nodes[i].node.parent;
|
||||
}
|
||||
}
|
||||
this.initialized = true;
|
||||
this.hideOrShow();
|
||||
};
|
Loading…
Reference in a new issue