mediawiki-extensions-Visual.../modules/ve/ve.Surface.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

520 lines
15 KiB
JavaScript

/**
* VisualEditor Surface class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.Surface object.
*
* A surface is a top-level object which contains both a surface model and a surface view.
*
* @class
* @constructor
* @param {String} parent Selector of element to attach to
* @param {HTMLElement} html Document html
* @param {Object} options Configuration options
*/
ve.Surface = function ( parent, dom, options ) {
// Create linear model from HTML5 DOM
var data = ve.dm.converter.getDataFromDom( dom );
// Properties
this.parent = parent;
this.modes = {};
this.currentMode = null;
/* Extend VE configuration recursively */
this.options = ve.extendObject( true, {
// Default options
toolbars: {
top: {
tools: [{ 'name': 'history', 'items' : ['undo', 'redo'] },
{ 'name': 'textStyle', 'items' : ['format'] },
{ 'name': 'textStyle', 'items' : ['bold', 'italic', 'link', 'clear'] },
{ 'name': 'list', 'items' : ['number', 'bullet', 'outdent', 'indent'] }]
}
},
//TODO: i18n
modes: {
wikitext: 'Toggle wikitext view',
json: 'Toggle JSON view',
html: 'Toggle HTML view',
render: 'Toggle preview',
history: 'Toggle transaction history view',
help: 'Toggle help view'
}
}, options );
// A place to store element references
this.$base = null;
this.$surface = null;
this.toolbarWrapper = {};
/* Create document model object with the linear model */
this.documentModel = new ve.dm.Document ( data );
this.model = new ve.dm.Surface( this.documentModel );
// Setup VE DOM Skeleton
this.setupBaseElements();
this.$surface = $( '<div>' ).attr( 'class', 'es-editor' );
this.$base.find( '.es-visual' ).append( this.$surface );
/* Instantiate surface layer */
this.view = new ve.ce.Surface( $( '.es-editor' ), this.model );
// Setup toolbars based on this.options
this.setupToolbars();
// Setup various toolbar modes and panels
//this.setupModes();
// Registration
ve.instances.push( this );
// Start tracking changes
this.model.startHistoryTracking();
};
/* Methods */
ve.Surface.prototype.getModel = function () {
return this.model;
};
ve.Surface.prototype.getDocumentModel = function () {
return this.documentModel;
};
ve.Surface.prototype.getView = function () {
return this.view;
};
ve.Surface.prototype.getContext = function () {
return this.context;
};
ve.Surface.prototype.getParent = function () {
return this.parent;
};
ve.Surface.prototype.setupBaseElements = function () {
// Make new base element
this.$base = $( '<div>' )
.attr( 'class', 'es-base' )
.append(
$( '<div>' ).attr( 'class', 'es-panes' )
.append( $( '<div>' ).attr( 'class', 'es-visual' ) )
.append( $( '<div>' ).attr( 'class', 'es-panels' ) )
.append( $( '<div>' ).attr( 'style', 'clear:both' ) )
)
.append(
$( '<div>' ).attr( {
// TODO: make 'paste' in surface stateful and remove this attrib
'id': 'paste',
'class': 'paste',
'contenteditable': 'true'
} )
);
// Attach the base the the parent
$( this.getParent() ).append( this.$base );
};
ve.Surface.prototype.setupToolbars = function () {
var surface = this;
// Build each toolbar
$.each( this.options.toolbars, function ( name, config ) {
if ( config !== null ) {
if( name === 'top' ) {
// Append toolbar wrapper at the top, just above .es-panes
surface.toolbarWrapper[name] = $( '<div>' )
.attr( 'class', 'es-toolbar-wrapper' )
.append(
$( '<div>' ).attr( 'class', 'es-toolbar' )
.append(
$( '<div>' ).attr( 'class', 'es-modes' )
).append(
$( '<div>' ).attr( 'style', 'clear:both' )
).append(
$( '<div>' ).attr( 'class', 'es-toolbar-shadow' )
)
);
surface.$base.find( '.es-panes' ).before( surface.toolbarWrapper[name] );
if ( 'float' in config && config.float === true ) {
// Float top toolbar
surface.floatTopToolbar();
}
}
// Instantiate the toolbar
surface['toolbar-' + name] = new ve.ui.Toolbar(
surface.$base.find( '.es-toolbar' ), surface.view, config.tools
);
}
} );
};
/*
* This code is responsible for switching toolbar into floating mode when scrolling ( with
* keyboard or mouse ).
* TODO: Determine if this would be better in ui.toolbar vs here.
* TODO: This needs to be refactored so that it only works on the main editor top tool bar.
*/
ve.Surface.prototype.floatTopToolbar = function () {
if ( !this.toolbarWrapper.top ) {
return;
}
var $toolbarWrapper = this.toolbarWrapper.top,
$toolbar = $toolbarWrapper.find( '.es-toolbar' ),
$window = $( window );
$window.scroll( function () {
var toolbarWrapperOffset = $toolbarWrapper.offset();
var $editorDocument = $toolbarWrapper.parent()
.find('.ve-surface').find('.ve-ce-documentNode');
if ( $window.scrollTop() > toolbarWrapperOffset.top ) {
var left = toolbarWrapperOffset.left,
right = $window.width() - $toolbarWrapper.outerWidth() - left;
// If not floating, set float
if ( !$toolbarWrapper.hasClass( 'float' ) ) {
$toolbarWrapper
.css( 'height', $toolbarWrapper.height() )
.addClass( 'float' );
$toolbar.css( {
'left': left,
'right': right
} );
} else {
// Toolbar is floated
if (
// Toolbar is at or below the top of last node in the document
$window.scrollTop() + $toolbar.height() >=
$editorDocument.children( '.ve-ce-branchNode:last' ).offset().top
) {
// XXX: Use less generic class names (not "bottom" and "float")
if( !$toolbarWrapper.hasClass( 'bottom' ) ) {
$toolbarWrapper
.removeClass( 'float' )
.addClass( 'bottom' );
$toolbar.css({
'top': $window.scrollTop() + 'px',
'left': left,
'right': right
});
}
} else { // Unattach toolbar
if ( $toolbarWrapper.hasClass( 'bottom' ) ) {
$toolbarWrapper
.removeClass( 'bottom' )
.addClass( 'float' );
$toolbar.css( {
'top': 0,
'left': left,
'right': right
} );
}
}
}
} else { // Return toolbar to top position
if ( $toolbarWrapper.hasClass( 'float' ) || $toolbarWrapper.hasClass( 'bottom' ) ) {
$toolbarWrapper.css( 'height', 'auto' )
.removeClass( 'float' )
.removeClass( 'bottom' );
$toolbar.css( {
'top': 0,
'left': 0,
'right': 0
} );
}
}
} );
};
ve.Surface.prototype.setupModes = function () {
var mode, renderType, i, len,
surface = this,
activeModes = [];
// Loop through toolbar config to build modes
$.each( surface.options.toolbars, function ( name, toolbar ) {
// If toolbar has modes
if( toolbar.modes && toolbar.modes.length > 0 ) {
for( i = 0, len = toolbar.modes.length - 1; i <= len; i++ ) {
$( surface.toolbarWrapper[name] )
.find( '.es-modes' )
.append(
$( '<div>' ).attr( {
'class': 'es-modes-button es-mode-' + toolbar.modes[i],
'title': surface.options.modes[ toolbar.modes[i] ]
} )
);
// XXX: 'mode' is undefined here ?
if ( !activeModes[mode] ) {
activeModes.push( toolbar.modes[i] );
}
}
}
} );
// Build elements in #es-panels for each activeMode
if ( activeModes.length > 0 ) {
for ( mode in activeModes ) {
switch ( activeModes[mode] ) {
case 'render':
renderType = 'es-render';
break;
case 'help':
renderType = '';
break;
default:
renderType = 'es-code';
break;
}
surface.$base
.find( '.es-panels' )
.append(
$( '<div>' ).attr(
'class', 'es-panel es-panel-' + activeModes[mode] + ' ' + renderType
)
);
}
}
/*
Define this.modes
Called after bulding elements.
*/
this.defineModes();
//Bind Mode events
$.each( this.modes, function ( name, mode ) {
mode.$.click( function () {
var disable = $( this ).hasClass( 'es-modes-button-down' );
var visible = surface.$base.hasClass( 'es-showData' );
$( '.es-modes-button' ).removeClass( 'es-modes-button-down' );
$( '.es-panel' ).hide();
if ( disable ) {
if ( visible ) {
surface.$base.removeClass( 'es-showData' );
$( window ).resize();
}
surface.currentMode = null;
} else {
$( this ).addClass( 'es-modes-button-down' );
mode.$panel.show();
if ( !visible ) {
surface.$base.addClass( 'es-showData' );
$( window ).resize();
}
mode.update.call( mode );
surface.currentMode = mode;
}
} );
} );
/* Bind some surface events for modes */
this.model.on( 'transact', function () {
if ( surface.currentMode ) {
surface.currentMode.update.call( surface.currentMode );
}
} );
this.model.on( 'select', function () {
if ( surface.currentMode === surface.modes.history ) {
surface.currentMode.update.call( surface.currentMode );
}
} );
};
/*
Define modes
TODO: possibly extend this object via the config
*/
ve.Surface.prototype.defineModes = function () {
var surface = this;
this.modes = {
'wikitext': {
'$': surface.$base.find( '.es-mode-wikitext' ),
'$panel': surface.$base.find( '.es-panel-wikitext' ),
'update': function () {
this.$panel.text(
ve.dm.WikitextSerializer.stringify( surface.getDocumentModel().getPlainObject() )
);
}
},
'json': {
'$': surface.$base.find( '.es-mode-json' ),
'$panel': surface.$base.find( '.es-panel-json' ),
'update': function () {
this.$panel.text( ve.dm.JsonSerializer.stringify( surface.getDocumentModel().getPlainObject(), {
'indentWith': ' '
} ) );
}
},
'html': {
'$': surface.$base.find( '.es-mode-html' ),
'$panel': surface.$base.find( '.es-panel-html' ),
'update': function () {
this.$panel.text(
ve.dm.HtmlSerializer.stringify( surface.getDocumentModel().getPlainObject() )
);
}
},
'render': {
'$': surface.$base.find( '.es-mode-render' ),
'$panel': surface.$base.find( '.es-panel-render' ),
'update': function () {
this.$panel.html(
ve.dm.HtmlSerializer.stringify( surface.getDocumentModel().getPlainObject() )
);
}
},
'history': {
'$': surface.$base.find( '.es-mode-history' ),
'$panel': surface.$base.find( '.es-panel-history' ),
'update': function () {
var history = surface.model.getHistory(),
i = history.length,
end = Math.max( 0, i - 25 ),
j,
k,
ops,
events = '',
z = 0,
operations,
data;
while ( --i >= end ) {
z++;
operations = [];
for ( j = 0; j < history[i].stack.length; j++ ) {
ops = history[i].stack[j].getOperations().slice( 0 );
for ( k = 0; k < ops.length; k++ ) {
data = ops[k].data || ops[k].length;
if ( ve.isArray( data ) ) {
data = data[0];
if ( ve.isArray( data ) ) {
data = data[0];
}
}
if ( typeof data !== 'string' && typeof data !== 'number' ) {
data = '-';
}
ops[k] = ops[k].type.substr( 0, 3 ) + '( ' + data + ' )';
}
operations.push( '[' + ops.join( ', ' ) + ']' );
}
events += '<div' + ( z === surface.model.undoIndex ? ' class="es-panel-history-active"' : '' ) + '>' + operations.join( ', ' ) + '</div>';
}
this.$panel.html( events );
}
},
'help': {
'$': surface.$base.find( '.es-mode-help' ),
'$panel': surface.$base.find( '.es-panel-help' ),
'update': function () {
//TODO: Make this less ugly,
//HOW?: Create api to register help items so that they may be generated here.
/*jshint multistr:true */
this.$panel.html( '\
<div class="es-help-title">Keyboard Shortcuts</div>\
<div class="es-help-shortcuts-title">Clipboard</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">C</span>\
</span>\
Copy selected text\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">X</span>\
</span>\
Cut selected text\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">V</span>\
</span>\
Paste text at the cursor\
</div>\
<div class="es-help-shortcuts-title">History navigation</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">Z</span>\
</span>\
Undo\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">Y</span>\
</span>\
Redo\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">&#8679;</span> +\
<span class="es-help-key">Z</span>\
</span>\
Redo\
</div>\
<div class="es-help-shortcuts-title">Formatting</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">B</span>\
</span>\
Make selected text bold\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">I</span>\
</span>\
Make selected text italic\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#8984;</span> +\
<span class="es-help-key">K</span>\
</span>\
Make selected text a link\
</div>\
<div class="es-help-shortcuts-title">Selection</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">&#8679;</span> +\
<span class="es-help-key">Arrow</span>\
</span>\
Adjust selection\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#x2325;</span> +\
<span class="es-help-key">Arrow</span>\
</span>\
Move cursor by words or blocks\
</div>\
<div class="es-help-shortcut">\
<span class="es-help-keys">\
<span class="es-help-key">Ctrl <span class="es-help-key-or">or</span> &#x2325;</span> +\
<span class="es-help-key">&#8679;</span> +\
<span class="es-help-key">Arrow</span>\
</span>\
Adjust selection by words or blocks\
</div>' );
}
}
};
};