diff --git a/extension.json b/extension.json index ed4c8c5b7..43f074536 100644 --- a/extension.json +++ b/extension.json @@ -71,6 +71,7 @@ "localBasePath": "modules/ve-cite", "remoteExtPath": "Cite/modules/ve-cite", "scripts": [ + "ve.dm.MWDocumentReferences.js", "ve.dm.MWReferenceModel.js", "ve.dm.MWReferencesListNode.js", "ve.dm.MWReferenceNode.js", diff --git a/jsdoc.json b/jsdoc.json index 7160f61e1..ca541eaf6 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -29,6 +29,7 @@ "mw.": "https://doc.wikimedia.org/mediawiki-core/master/js/{type}.html" }, "prefixMapIgnore": [ + "ve.dm.MWDocumentReferences", "ve.dm.MWReference", "ve.ce.MWReference", "ve.ui.MWReference" diff --git a/modules/ve-cite/ve.ce.MWReferencesListNode.js b/modules/ve-cite/ve.ce.MWReferencesListNode.js index a80e745d2..b3df0c0d8 100644 --- a/modules/ve-cite/ve.ce.MWReferencesListNode.js +++ b/modules/ve-cite/ve.ce.MWReferencesListNode.js @@ -27,6 +27,7 @@ ve.ce.MWReferencesListNode = function VeCeMWReferencesListNode() { this.internalList = null; this.listNode = null; this.modified = false; + this.docRefs = null; // DOM changes this.$element.addClass( 've-ce-mwReferencesListNode' ); @@ -174,6 +175,7 @@ ve.ce.MWReferencesListNode.prototype.update = function () { return; } + this.docRefs = ve.dm.MWDocumentReferences.static.refsForDoc( model.getDocument() ); const internalList = model.getDocument().internalList; const refGroup = model.getAttribute( 'refGroup' ); const listGroup = model.getAttribute( 'listGroup' ); @@ -238,9 +240,11 @@ ve.ce.MWReferencesListNode.prototype.update = function () { this.$refmsg.text( emptyText ); this.$element.append( this.$refmsg ); } else { + const groupedByParent = this.docRefs.getGroupRefsByParents( listGroup ); + const topLevelNodes = groupedByParent[ '' ] || []; this.$reflist.append( - nodes.indexOrder.map( ( index ) => this.renderListItem( - nodes, internalList, refGroup, index + topLevelNodes.map( ( node ) => this.renderListItem( + nodes, internalList, refGroup, node ) ) ); @@ -256,11 +260,12 @@ ve.ce.MWReferencesListNode.prototype.update = function () { * @param {Object} nodes Node group object, containing nodes and key order array * @param {ve.dm.InternalList} internalList Internal list * @param {string} refGroup Reference group - * @param {number} index Item index - * @return {jQuery} List item + * @param {ve.dm.MWReferenceNode} node Reference node to render as a footnote body + * @return {jQuery} Rendered list item */ -ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internalList, refGroup, index ) { - const key = internalList.keys[ index ]; +ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internalList, refGroup, node ) { + const listIndex = node.getAttribute( 'listIndex' ); + const key = internalList.keys[ listIndex ]; const keyedNodes = ( nodes.keyedNodes[ key ] || [] ) .filter( // Exclude placeholders and references defined inside the references list node @@ -271,7 +276,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal .append( this.renderBacklinks( keyedNodes, refGroup ), ' ' ); // Generate reference HTML from first item in key - const modelNode = internalList.getItemNode( index ); + const modelNode = internalList.getItemNode( listIndex ); if ( modelNode && modelNode.length ) { const refPreview = new ve.ui.MWPreviewElement( modelNode, { useView: true } ); $li.append( @@ -285,8 +290,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal // TODO: attach to the singleton click handler on the surface $li.on( 'mousedown', ( e ) => { if ( ve.isUnmodifiedLeftClick( e ) && modelNode && modelNode.length ) { - const firstNode = nodes.firstNodes[ index ]; - const items = ve.ui.contextItemFactory.getRelatedItems( [ firstNode ] ) + const items = ve.ui.contextItemFactory.getRelatedItems( [ node ] ) .filter( ( item ) => item.name !== 'mobileActions' ); if ( items.length ) { const contextItem = ve.ui.contextItemFactory.lookup( items[ 0 ].name ); @@ -296,7 +300,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal if ( command ) { const fragmentArgs = { fragment: surface.getModel() - .getLinearFragment( firstNode.getOuterRange(), true ), + .getLinearFragment( node.getOuterRange(), true ), selectFragmentOnClose: false }; const newArgs = ve.copy( command.args ); diff --git a/modules/ve-cite/ve.dm.MWDocumentReferences.js b/modules/ve-cite/ve.dm.MWDocumentReferences.js new file mode 100644 index 000000000..db62bcaa5 --- /dev/null +++ b/modules/ve-cite/ve.dm.MWDocumentReferences.js @@ -0,0 +1,68 @@ +'use strict'; + +/*! + * @copyright 2024 VisualEditor Team's Cite sub-team and others; see AUTHORS.txt + * @license MIT + */ + +/** + * A facade providing a simplified and safe interface to Cite `ref` and + * `references` tags in a document. + * + * @constructor + * @mixes OO.EventEmitter + * @param {ve.dm.Document} doc The document that reference tags will be embedded in. + */ +ve.dm.MWDocumentReferences = function VeDmMWDocumentReferences( doc ) { + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.doc = doc; +}; + +/* Inheritance */ + +OO.mixinClass( ve.dm.MWDocumentReferences, OO.EventEmitter ); + +/* Methods */ + +/** + * Singleton MWDocumentReferences for a document. + * + * @param {ve.dm.Document} doc Source document associated with the singleton + * @return {ve.dm.MWDocumentReferences} Singleton docRefs + */ +ve.dm.MWDocumentReferences.static.refsForDoc = function ( doc ) { + let docRefs = doc.getStorage( 'document-references-store' ); + if ( docRefs === undefined ) { + docRefs = new ve.dm.MWDocumentReferences( doc ); + doc.setStorage( 'document-references-store', docRefs ); + } + return docRefs; +}; + +/** + * Get all refs for a group, organized by parent ref + * + * This is appropriate when rendering a reflist organized hierarchically by + * subrefs using the `extends` feature. + * + * @param {string} groupName Filter by this group. + * @return {Object.} Mapping from parent ref + * name to a list of its subrefs. Note that the top-level refs are under the + * `null` value. + */ +ve.dm.MWDocumentReferences.prototype.getGroupRefsByParents = function ( groupName ) { + const nodeGroup = this.doc.getInternalList().getNodeGroup( groupName ); + return ( nodeGroup ? nodeGroup.indexOrder : [] ) + .reduce( ( acc, index ) => { + const node = nodeGroup.firstNodes[ index ]; + const extendsRef = node.element.attributes.extendsRef || ''; + if ( acc[ extendsRef ] === undefined ) { + acc[ extendsRef ] = []; + } + acc[ extendsRef ].push( node ); + return acc; + }, {} ); +};