jenkins-bot has submitted this change and it was merged. Change subject: Rewrite TOCWidget based on Linker::generateTOC ......................................................................
Rewrite TOCWidget based on Linker::generateTOC Use the new node cache to find headings. Change-Id: I5eb75c5db5ca466fd6f16a57c693c2a4458cff7c --- M extension.json M modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js M modules/ve-mw/dm/nodes/ve.dm.MWHeadingNode.js M modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js D modules/ve-mw/ui/styles/widgets/ve.ui.MWTocWidget.css D modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js M modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js 7 files changed, 122 insertions(+), 293 deletions(-) Approvals: Jforrester: Looks good to me, approved jenkins-bot: Verified diff --git a/extension.json b/extension.json index 5a51d3d..7a1e53e 100644 --- a/extension.json +++ b/extension.json @@ -1131,7 +1131,6 @@ "modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js", "modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js", "modules/ve-mw/ui/widgets/ve.ui.MWTargetWidget.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.MWExtensionDialog.js", "modules/ve-mw/ui/dialogs/ve.ui.MWExtensionPreviewDialog.js", @@ -1156,7 +1155,6 @@ "modules/ve-mw/ui/styles/elements/ve.ui.MWExpandableErrorElement.css", "modules/ve-mw/ui/styles/tools/ve.ui.MWPopupTool.css", "modules/ve-mw/ui/styles/widgets/ve.ui.MWAceEditorWidget.css", - "modules/ve-mw/ui/styles/widgets/ve.ui.MWTocWidget.css", "modules/ve-mw/ui/styles/tools/ve.ui.MWEducationPopupTool.css" ], "skinStyles": { diff --git a/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js b/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js index 8154c21..6ff5bb7 100644 --- a/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js +++ b/modules/ve-mw/ce/nodes/ve.ce.MWHeadingNode.js @@ -14,9 +14,12 @@ * @param {ve.dm.MWHeadingNode} model Model to observe * @param {Object} [config] Configuration options */ -ve.ce.MWHeadingNode = function VeCeMWHeadingNode( model, config ) { +ve.ce.MWHeadingNode = function VeCeMWHeadingNode() { // Parent constructor - ve.ce.HeadingNode.call( this, model, config ); + ve.ce.MWHeadingNode.super.apply( this, arguments ); + + // Events + this.model.connect( this, { update: 'onUpdate' } ); }; /* Inheritance */ @@ -45,9 +48,24 @@ this.rebuildToc(); }; +ve.ce.MWHeadingNode.prototype.onUpdate = function () { + var surface = this.surface, + node = this; + + if ( surface && surface.mwTocWidget ) { + surface.getModel().getDocument().once( 'transact', function () { + surface.mwTocWidget.updateNode( node ); + } ); + } +}; + ve.ce.MWHeadingNode.prototype.rebuildToc = function () { - if ( this.surface && this.surface.mwTocWidget ) { - this.surface.mwTocWidget.rebuild(); + var surface = this.surface; + + if ( surface && surface.mwTocWidget ) { + surface.getModel().getDocument().once( 'transact', function () { + surface.mwTocWidget.rebuild(); + } ); } }; diff --git a/modules/ve-mw/dm/nodes/ve.dm.MWHeadingNode.js b/modules/ve-mw/dm/nodes/ve.dm.MWHeadingNode.js index 709aacc..f124425 100644 --- a/modules/ve-mw/dm/nodes/ve.dm.MWHeadingNode.js +++ b/modules/ve-mw/dm/nodes/ve.dm.MWHeadingNode.js @@ -17,7 +17,7 @@ */ ve.dm.MWHeadingNode = function VeDmMWHeadingNode() { // Parent constructor - ve.dm.HeadingNode.apply( this, arguments ); + ve.dm.MWHeadingNode.super.apply( this, arguments ); }; /* Inheritance */ diff --git a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js index 99db24a..f4cb219 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js @@ -707,7 +707,8 @@ * @inheritdoc */ ve.init.mw.DesktopArticleTarget.prototype.surfaceReady = function () { - var surfaceReadyTime = ve.now(), + var surface = this.getSurface(), + surfaceReadyTime = ve.now(), target = this; if ( !this.activating ) { @@ -720,18 +721,19 @@ // TODO: mwTocWidget should probably live in a ve.ui.MWSurface subclass if ( mw.config.get( 'wgVisualEditorConfig' ).enableTocWidget ) { - this.getSurface().mwTocWidget = new ve.ui.MWTocWidget( this.getSurface() ); + surface.mwTocWidget = new ve.ui.MWTocWidget( this.getSurface() ); + surface.$element.before( surface.mwTocWidget.$element ); } // Track how long it takes for the first transaction to happen - this.surface.getModel().getDocument().once( 'transact', function () { + surface.getModel().getDocument().once( 'transact', function () { ve.track( 'mwtiming.behavior.firstTransaction', { duration: ve.now() - surfaceReadyTime, targetName: target.constructor.static.trackingName } ); } ); - this.getSurface().getModel().getMetaList().connect( this, { + surface.getModel().getMetaList().connect( this, { insert: 'onMetaItemInserted', remove: 'onMetaItemRemoved' } ); @@ -1022,9 +1024,6 @@ // Update UI promises.push( this.teardownToolbar() ); this.restoreDocumentTitle(); - if ( this.getSurface().mwTocWidget ) { - this.getSurface().mwTocWidget.teardown(); - } if ( this.saveDialog ) { if ( this.saveDialog.isOpened() ) { @@ -1036,9 +1035,14 @@ } return $.when.apply( null, promises ).then( function () { + var surface; // Destroy surface while ( target.surfaces.length ) { - target.surfaces.pop().destroy(); + surface = target.surfaces.pop(); + surface.destroy(); + if ( surface.mwTocWidget ) { + surface.mwTocWidget.$element.remove(); + } } target.active = false; } ); diff --git a/modules/ve-mw/ui/styles/widgets/ve.ui.MWTocWidget.css b/modules/ve-mw/ui/styles/widgets/ve.ui.MWTocWidget.css deleted file mode 100644 index 816438a..0000000 --- a/modules/ve-mw/ui/styles/widgets/ve.ui.MWTocWidget.css +++ /dev/null @@ -1,28 +0,0 @@ -/*! - * VisualEditor MediaWiki UserInterface MWTocWidget styles. - * - * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt - * @license The MIT License (MIT); see LICENSE.txt - */ - -.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 deleted file mode 100644 index 435d9d2..0000000 --- a/modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js +++ /dev/null @@ -1,89 +0,0 @@ -/*! - * VisualEditor UserInterface MWTocItemWidget class. - * - * @copyright 2011-2016 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.mixin.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 VeUiMWTocItemWidget( config ) { - // Parent constructor - OO.ui.Widget.call( this, config ); - - // Mixin Constructor - OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: $( '<ul>' ) } ) ); - - 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 = $( '<span>' ).addClass( 'tocnumber' ) - .text( this.sectionPrefix ); - this.$tocText = $( '<span>' ).addClass( 'toctext' ) - .text( this.node.$element.text() ); - this.$element - .addClass( 'toclevel-' + this.tocLevel ) - .addClass( 'tocsection-' + this.tocIndex ) - .append( $( '<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.mixin.GroupElement ); - -/* Static Properties */ - -ve.ui.MWTocItemWidget.static.tagName = 'li'; - -/* Methods */ - -/** - * Updates the text of the toc item - * - */ -ve.ui.MWTocItemWidget.prototype.onUpdate = function () { - var widget = this; - // Timeout needed to let the dom element actually update - setTimeout( function () { - widget.$tocText.text( widget.node.$element.text() ); - } ); -}; - -/** - * 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 index aa23472..98e069c 100644 --- a/modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js +++ b/modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js @@ -16,7 +16,6 @@ * @param {Object} [config] Configuration options */ ve.ui.MWTocWidget = function VeUiMWTocWidget( surface, config ) { - var widget = this; // Parent constructor OO.ui.Widget.call( this, config ); @@ -26,46 +25,24 @@ 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 = $( '<ul>' ); - // Section keyed item map - this.items = {}; + this.rootLength = 0; this.initialized = false; // Page settings cache this.mwTOCForce = false; this.mwTOCDisable = false; - // TODO: fix i18n - this.tocToggle = { - hideMsg: ve.msg( 'hidetoc' ), - showMsg: ve.msg( 'showtoc' ), - $link: $( '<a class="internal" id="togglelink"></a>' ).text( ve.msg( 'hidetoc' ) ), - open: true - }; + this.$tocList = $( '<ul>' ); this.$element.addClass( 'toc ve-ui-mwTocWidget' ).append( - $( '<div>' ).attr( 'id', 'toctitle' ).append( - $( '<h2>' ).text( ve.msg( 'toc' ) ), - $( '<span>' ).addClass( 'toctoggle' ).append( this.tocToggle.$link ) + $( '<div>' ).addClass( 'toctitle' ).append( + $( '<h2>' ).text( ve.msg( 'toc' ) ) ), - this.topics.$group, this.$tempTopics + this.$tocList ); - // 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.tocToggle.$link.on( 'click', function () { - if ( widget.tocToggle.open ) { - widget.tocToggle.$link.text( widget.tocToggle.showMsg ); - widget.tocToggle.open = false; - } else { - widget.tocToggle.$link.text( widget.tocToggle.hideMsg ); - widget.tocToggle.open = true; - } - // FIXME: We should really use CSS here - widget.topics.$group.add( widget.$tempTopics ).slideToggle(); - } ); + // Setup toggle link + mw.hook( 'wikipage.content' ).fire( this.$element ); + // Events this.metaList.connect( this, { insert: 'onMetaListInsert', remove: 'onMetaListRemove' @@ -93,7 +70,7 @@ // hide this.mwTOCDisable = true; } - this.hideOrShow(); + this.updateVisibility(); }; /** @@ -107,7 +84,7 @@ } else if ( metaItem instanceof ve.dm.MWTOCDisableMetaItem ) { this.mwTOCDisable = false; } - this.hideOrShow(); + this.updateVisibility(); }; /** @@ -127,160 +104,109 @@ this.mwTOCDisable = true; } } - this.hideOrShow(); + this.updateVisibility(); } }; /** * Hides or shows the TOC based on page and default settings */ -ve.ui.MWTocWidget.prototype.hideOrShow = function () { +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.topics.items.length >= 3 ) ); + 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 rebuild is debounced */ ve.ui.MWTocWidget.prototype.rebuild = ve.debounce( function () { - var widget = this; - // Only rebuild when initialized - if ( this.surface.mwTocWidget.initialized ) { - this.$tempTopics.append( this.topics.$group.children().clone() ); - this.teardownItems(); - // Build after transactions - setTimeout( function () { - widget.build(); - widget.$tempTopics.empty(); - }, 0 ); + if ( this.initialized ) { + // Wait for transactions to process + this.build(); } -}, 0 ); +} ); /** - * Teardown all of the TOC items + * Update the text content of a specific heading node + * + * @param {ve.ce.MWHeadingNode} viewNode Heading node */ -ve.ui.MWTocWidget.prototype.teardownItems = function () { - var item; - for ( item in this.items ) { - this.items[ item ].remove(); - delete this.items[ item ]; +ve.ui.MWTocWidget.prototype.updateNode = function ( viewNode ) { + if ( viewNode.$tocText ) { + viewNode.$tocText.text( viewNode.$element.text() ); } - this.items = {}; -}; - -/** - * Teardown the widget and remove it from the dom - */ -ve.ui.MWTocWidget.prototype.teardown = function () { - this.teardownItems(); - this.$element.remove(); }; /** * Build TOC from mwHeading dm nodes + * + * Based on generateTOC in Linker.php */ 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().getBranchNodeFromOffset( 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; - } + var i, l, level, levelDiff, tocNumber, modelNode, viewNode, + $list, $text, $item, $link, + $newTocList = $( '<ul>' ), + nodes = this.doc.getNodesByType( 'mwHeading', true ), + documentView = this.surface.getView().getDocument(), + lastLevel = 0, + stack = []; + + function getItemIndex( $list, n ) { + return $list.children( 'li' ).length + ( n === stack.length - 1 ? 1 : 0 ); } + + function linkClickHandler( heading ) { + ve.init.target.goToHeading( heading ); + 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 ); + } else { + $list = $newTocList; + } + stack.push( $list ); + } else if ( level < lastLevel ) { + levelDiff = lastLevel - level; + while ( levelDiff > 0 && stack.length > 1 ) { + stack.pop(); + levelDiff--; + } + } + + tocNumber = stack.map( getItemIndex ).join( '.' ); + viewNode = documentView.getBranchNodeFromOffset( modelNode.getRange().start ); + $item = $( '<li>' ).addClass( 'toclevel-' + stack.length ).addClass( 'tocsection-' + ( i + 1 ) ); + $link = $( '<a href="#">' ).append( '<span class="tocnumber">' + tocNumber + '</span> ' ); + $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.replaceWith( $newTocList ); + this.$tocList = $newTocList; + + if ( nodes.length ) { + this.rootLength = stack[ 0 ].children().length; + } else { + this.rootLength = 0; + } + this.initialized = true; - this.hideOrShow(); + this.updateVisibility(); }; -- To view, visit https://gerrit.wikimedia.org/r/297693 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I5eb75c5db5ca466fd6f16a57c693c2a4458cff7c Gerrit-PatchSet: 11 Gerrit-Project: mediawiki/extensions/VisualEditor Gerrit-Branch: master Gerrit-Owner: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Alex Monk <a...@wikimedia.org> Gerrit-Reviewer: DLynch <dly...@wikimedia.org> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits