jenkins-bot has submitted this change and it was merged.

Change subject: Table of contents widget
......................................................................


Table of contents widget

TOC Widget is created in the mw target view class.
Adding and removing a heading rebuilds the TOC Widget based
on the the order of the page heading nodes.

TOC Widget considers TOC page settings and displays in the default manor
unless forced or disabled.

TOC Widget still needs to be finalized by being placed in the surface.
This could be a problem until we have a CE node for it to live in or
have some DM work added.  Roan and I have discussed how to go forward.

To enable the widget you must add the following to LocalSettings.php:
$wgVisualEditorEnableTocWidget = true;

Change-Id: I488cfbbdb060e50d81f51e0f757e67d0114b8936
---
M VisualEditor.hooks.php
M VisualEditor.php
M modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js
M modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
M modules/ve-mw/ui/styles/ve.ui.MWWidget.css
A modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js
A modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js
7 files changed, 421 insertions(+), 4 deletions(-)

Approvals:
  Catrope: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php
index b4310fa..275a081 100644
--- a/VisualEditor.hooks.php
+++ b/VisualEditor.hooks.php
@@ -370,6 +370,7 @@
                        $wgVisualEditorBrowserBlacklist,
                        $wgVisualEditorSupportedSkins,
                        $wgVisualEditorShowBetaWelcome,
+                       $wgVisualEditorEnableTocWidget,
                        $wgVisualEditorPreferenceModules;
 
                $vars['wgVisualEditorConfig'] = array(
@@ -387,6 +388,7 @@
                        'tabPosition' => $wgVisualEditorTabPosition,
                        'tabMessages' => $wgVisualEditorTabMessages,
                        'showBetaWelcome' => $wgVisualEditorShowBetaWelcome,
+                       'enableTocWidget' => $wgVisualEditorEnableTocWidget
                );
 
                foreach ( $wgVisualEditorPreferenceModules as $pref => $module 
) {
diff --git a/VisualEditor.php b/VisualEditor.php
index 6209798..63f9ae6 100644
--- a/VisualEditor.php
+++ b/VisualEditor.php
@@ -549,6 +549,8 @@
                        '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',
@@ -622,7 +624,7 @@
                        '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',
@@ -1127,6 +1129,9 @@
 // Namespaces to enable VisualEditor in
 $wgVisualEditorNamespaces = $wgContentNamespaces;
 
+// Whether to enable the (experimental for now) TOC widget
+$wgVisualEditorEnableTocWidget = false;
+
 // List of skins VisualEditor integration supports
 $wgVisualEditorSupportedSkins = array( 'vector', 'apex', 'monobook', 'minerva' 
);
 
diff --git a/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js 
b/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js
index 053474d..a1cada2 100644
--- a/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js
+++ b/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js
@@ -27,6 +27,30 @@
 
 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 );
diff --git a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js 
b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
index 32cca27..bf0cce9 100644
--- a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
+++ b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
@@ -325,6 +325,9 @@
                '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();
@@ -1213,7 +1216,7 @@
  * @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();
 };
@@ -1227,7 +1230,7 @@
        var $toc = $( '#toc' ),
                $wrap = $toc.parent();
        if ( $wrap.data( 've.hideTableOfContents' ) ) {
-               $wrap.slideDown( function () {
+               $wrap.show( function () {
                        $toc.unwrap();
                } );
        }
@@ -1243,7 +1246,7 @@
                .wrap( '<div>' )
                .parent()
                        .data( 've.hideTableOfContents', true )
-                       .slideUp();
+                       .hide();
 };
 
 /**
diff --git a/modules/ve-mw/ui/styles/ve.ui.MWWidget.css 
b/modules/ve-mw/ui/styles/ve.ui.MWWidget.css
index 7e51b2b..84a3e6d 100644
--- a/modules/ve-mw/ui/styles/ve.ui.MWWidget.css
+++ b/modules/ve-mw/ui/styles/ve.ui.MWWidget.css
@@ -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;
+}
diff --git a/modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js 
b/modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js
new file mode 100644
index 0000000..84e83d3
--- /dev/null
+++ b/modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js
@@ -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] );
+};
diff --git a/modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js 
b/modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js
new file mode 100644
index 0000000..ff0552f
--- /dev/null
+++ b/modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js
@@ -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();
+};

-- 
To view, visit https://gerrit.wikimedia.org/r/108945
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I488cfbbdb060e50d81f51e0f757e67d0114b8936
Gerrit-PatchSet: 17
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Robmoen <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Robmoen <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to