mediawiki-extensions-Visual.../modules/ve/ui/tools/ve.ui.ListButtonTool.js
Timo Tijhof 88f6089952 Kranitor #1: On-boarding
'''Kranitor commits''' are commits by Krinkle with his janitor hat on.
Must never contain functional changes mixed with miscellaneous changes.

.gitignore:
 * Add .DS_Store to the ignore list so that browsing the directories
   on Mac OS X, will not add these files to the list of untracked
   files.
 * Fix missing newline at end of file

.jshintrc
 * raises -> throws
 * +module (QUnit.module)
 * remove 'Node' (as of node-jshint 1.7.2 this is now part of
   'browser:true', as it should be)

Authors:
 * Adding myself

MWExtension/VisualEditor.php
 * Fix default value of wgVisualEditorParsoidURL to not
   point to the experimental instance in WMF Labs.

Issues:
 * ve.ce.TextNode:
  - Fix TODO: Don't perform a useless clone of an already-jQuerified object.
  - Use .html() to set html content instead of encapsulating between
    two strings. This is slightly faster but more importantly safer,
    and prevents situations where the resulting jQuery collection
    actually contains 2 elements instead of 1, thus messing up
    what .contents() is iterating over.
 * ve.ce.Document.test.js
  - Fix: ReferenceError: assert is not defined
 * ve.dm.Document.test.js
  - Fix: ReferenceError: assert is not defined
 * ve.dm.Transaction.test.js
  - Fix: ReferenceError: assert is not defined
 * ve.dm.TransactionProcessor.test.js
  - Fix: ReferenceError: assert is not defined
 * ext.visualEditor.viewPageTarget
  - Missing dependency on 'mediawiki.Title'

Code conventions / Misc cleanup
 * Various JSHint warnings.
 * Whitespace
 * jQuery(): Use '<tag>' for element creation,
   use '<valid><xml/></valid>' for parsing
 * Use the default operator instead of ternary when the condition and
   first value are the same.
   x = foo ? foo : bar; -> x = foo || bar;
   Because contrary to some programming language (PHP...), in JS the
   default operator does not enforce a boolean result but returns the
   original value, hence it being called the 'default' operator, as
   opposed to the 'or' operator.
 * No need to call addClass() twice, it takes a space-separated list
   (jQuery splits by space and adds if needed)
 * Use .on( event[, selector], fn ) instead of the deprecated
   routers to it such as .bind(), .delegate() and .live().
   All these three are now built-in and fully compatible with .on()
 * Add 'XXX:' comments for suspicious code that I don't want to change
   as part of a clean up commit.
 * Remove unused variables (several var x = this; where x was not
   used anywhere, possibly from boilerplate copy/paste)
 * Follows-up Trevor's commit that converts test suites to the new
   QUnit format. Also removed the globals since we no longer use those
   any more.

Change-Id: I7e37c9bff812e371c7f65a6fd85d9e2af3e0a22f
2012-07-27 14:40:00 -07:00

226 lines
6.3 KiB
JavaScript

/**
* VisualEditor user interface ListButtonTool class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.ui.ListButtonTool object.
*
* @class
* @constructor
* @extends {ve.ui.ButtonTool}
* @param {ve.ui.Toolbar} toolbar
* @param {String} name
*/
ve.ui.ListButtonTool = function ( toolbar, name, title, data ) {
// Inheritance
ve.ui.ButtonTool.call( this, toolbar, name, title );
// Properties
this.data = data;
this.nodes = [];
};
/* Methods */
ve.ui.ListButtonTool.prototype.list = function ( nodes, style ) {
var surfaceModel = this.toolbar.getSurfaceView().getModel(),
documentModel = surfaceModel.getDocument(),
selection = surfaceModel.getSelection(),
groups = documentModel.getCoveredSiblingGroups( selection ),
previousList,
groupRange,
group,
tx, i;
for ( i = 0; i < groups.length; i++ ) {
group = groups[i];
if ( group.grandparent && group.grandparent.getType() === 'list' ) {
if ( group.grandparent !== previousList ) {
// Change the list style
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChange(
documentModel, group.grandparent.getOffset(), 'style', style
),
selection
);
// Skip this one next time
previousList = group.grandparent;
}
} else {
// Get a range that covers the whole group
groupRange = new ve.Range(
group.nodes[0].getOuterRange().start,
group.nodes[group.nodes.length - 1].getOuterRange().end
);
// Convert everything to paragraphs first
surfaceModel.change(
ve.dm.Transaction.newFromContentBranchConversion(
documentModel, groupRange, 'paragraph'
),
selection
);
// Wrap everything in a list and each content branch in a listItem
tx = ve.dm.Transaction.newFromWrap(
documentModel,
groupRange,
[],
[{ 'type': 'list', 'attributes': { 'style': style } }],
[],
[{ 'type': 'listItem' }]
);
surfaceModel.change( tx, tx.translateRange( selection ) );
}
}
};
ve.ui.ListButtonTool.prototype.unlist = function ( node ) {
/**
* Recursively prepare to unwrap all lists in a given range.
*
* This function will find all lists covered wholly or partially by the given range, as well
* as all lists inside these lists, and return their inner ranges. This means that all sublists
* will be found even if range doesn't cover them.
*
* To actually unwrap the list, feed the returned ranges to ve.dm.Transaction.newFromWrap(),
* in order.
*
* @param {ve.dm.Document} documentModel
* @param {ve.Range} range
* @returns {ve.Range[]} Array of inner ranges of lists
*/
function getUnlistRanges( documentModel, range ) {
var groups = documentModel.getCoveredSiblingGroups( range ),
// Collect ranges in an object for deduplication
unlistRanges = {},
i, j, k, group, previousList, list, listItem,
subList, endOffset = 0;
for ( i = 0; i < groups.length; i++ ) {
group = groups[i];
list = group.grandparent;
if ( list && list.getType() === 'list' && list !== previousList ) {
// Unwrap the parent list
range = list.getRange();
if ( range.end > endOffset ) {
unlistRanges[range.start + '-' + range.end] = range;
endOffset = range.end;
}
// Skip this list next time
previousList = list;
// Recursively unwrap any sublists of the list
for ( j = 0; j < list.children.length; j++ ) {
listItem = list.children[j];
if ( listItem.getType() === 'listItem' ) {
for ( k = 0; k < listItem.children.length; k++ ) {
subList = listItem.children[k];
if ( subList.getType() === 'list' ) {
// Recurse
unlistRanges = ve.extendObject( unlistRanges, getUnlistRanges(
documentModel, subList.getRange()
) );
}
}
}
}
}
}
return unlistRanges;
}
var surfaceModel = this.toolbar.getSurfaceView().getModel(),
documentModel = surfaceModel.getDocument(),
selection = surfaceModel.getSelection(),
unlistRangesObj = getUnlistRanges( documentModel, selection ),
unlistRangesArr = [],
i, j, tx;
for ( i in unlistRangesObj ) {
unlistRangesArr.push( unlistRangesObj[i] );
}
for ( i = 0; i < unlistRangesArr.length; i++ ) {
// Unwrap the range given by unlistRanges[i]
tx = ve.dm.Transaction.newFromWrap(
documentModel,
unlistRangesArr[i],
[ { 'type': 'list' } ],
[],
[ { 'type': 'listItem' } ],
[]
);
selection = tx.translateRange( selection );
surfaceModel.change( tx );
// Translate all the remaining ranges for this transaction
// TODO ideally we'd have a way to merge all these transactions into one and execute that instead
for ( j = i + 1; j < unlistRangesArr.length; j++ ) {
unlistRangesArr[j] = tx.translateRange( unlistRangesArr[j] );
}
}
// Update the selection
surfaceModel.change( null, selection );
};
ve.ui.ListButtonTool.prototype.onClick = function () {
this.toolbar.surfaceView.model.breakpoint();
if ( !this.$.hasClass( 'es-toolbarButtonTool-down' ) ) {
this.list( this.nodes, this.name );
} else {
this.unlist( this.nodes );
}
this.toolbar.surfaceView.model.breakpoint();
};
ve.ui.ListButtonTool.prototype.updateState = function ( annotations, nodes ) {
var surfaceView = this.toolbar.getSurfaceView(),
surfaceModel = surfaceView.getModel(),
doc = surfaceView.getDocument(),
selection = surfaceModel.getSelection(),
leaves = doc.selectNodes( selection, 'leaves' );
function areListItemsOfStyle( leaves, style ){
var i, listNode;
for ( i = 0; i < leaves.length; i++ ) {
listNode = leaves[i].node;
// Get the list node
while ( listNode && listNode.getType() !== 'list' ) {
listNode = listNode.getParent();
if ( listNode === null ) {
return false;
}
}
if ( listNode.getModel().getAttribute('style') !== style ) {
return false;
}
}
return true;
}
if ( areListItemsOfStyle( leaves, this.name ) ) {
this.$.addClass( 'es-toolbarButtonTool-down' );
} else {
this.$.removeClass( 'es-toolbarButtonTool-down' );
}
};
/* Registration */
ve.ui.Tool.tools.number = {
'constructor': ve.ui.ListButtonTool,
'name': 'number',
'title': ve.msg( 'visualeditor-listbutton-number-tooltip' )
};
ve.ui.Tool.tools.bullet = {
'constructor': ve.ui.ListButtonTool,
'name': 'bullet',
'title': ve.msg( 'visualeditor-listbutton-bullet-tooltip' )
};
/* Inheritance */
ve.extendClass( ve.ui.ListButtonTool, ve.ui.ButtonTool );