mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 00:30:44 +00:00
077e21867e
* Classicifation (JS) Use addClass instead of attr( 'class' ) whenever possible. addClass will manipulate the properties directly instead of (re-)setting an attribute which (most) browsers then sync with the properties. Difference between: elem.className and elem.setAttribute( 'class', .. ); Just like .checked, .value, .disabled and other interactive properties, the HTML attributes should only be used for initial values from the html document. When in javascript, only set properties. Attributes are either ignored or slow. * Styling (JS) Use .css() instead of attr( 'style' ). Again, setting properties instead of attributes is much faster, easier and safer. And this way it takes care of cross-browser issues where applicable, and less prone to error due to dealing with key-value pairs instead of css strings. Difference between: elem.style.foo = 'bar'; and elem.setAttribute( 'style', 'foo: bar;' ); * Finding (JS) Use .find( 'foo bar' ) instead of .find( 'foo' ).find( 'bar' ). It is CSS! * Vendor prefixes (CSS) It is important to always list newer (standards-compliant) versions *after* the older/prefixed variants. See also http://css-tricks.com/ordering-css3-properties/ So the following three: -webkit-gradient (Chrome, Safari 4) -webkit-linear-gradient (Chrome 10, Safari 5+) linear-gradient (CSS3 standard) ... must be in that order. Notes: - "-moz-opacity" is from before Mozilla 1.7 (Firefox < 0.8) Has not been renamed to "opacity" since Firefox 0.9. - Removed redundant "-moz-opacity" - Added "filter: alpha(opacity=**);" where missing - Fixed order of css3 properties (old to new) - Add standardized css3 versions where missing (some 'border-radius' groups didn't have the non-prefixed version) - Spacing - @embed - Shorten hex colors where possible (#dddddd -> #ddd) $ ack '#([0-9a-f])\1{5}' --css $ ack '#([0-9a-f])\1{2};' --css Change-Id: I386fedb9058c2567fd0af5f55291e9859a53329d
522 lines
15 KiB
JavaScript
522 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>' ).addClass( '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>' )
|
|
.addClass( 'es-base' )
|
|
.append(
|
|
$( '<div>' ).addClass( 'es-panes' )
|
|
.append( $( '<div>' ).addClass( 'es-visual' ) )
|
|
.append( $( '<div>' ).addClass( 'es-panels' ) )
|
|
.append( $( '<div>' ).css( '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>' )
|
|
.addClass( 'es-toolbar-wrapper' )
|
|
.append(
|
|
$( '<div>' ).addClass( 'es-toolbar' )
|
|
.append(
|
|
$( '<div>' ).addClass( 'es-modes' )
|
|
).append(
|
|
$( '<div>' ).css( 'clear', 'both' )
|
|
).append(
|
|
$( '<div>' ).addClass( '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 .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> ⌘</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> ⌘</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> ⌘</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> ⌘</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> ⌘</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> ⌘</span> +\
|
|
<span class="es-help-key">⇧</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> ⌘</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> ⌘</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> ⌘</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">⇧</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> ⌥</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> ⌥</span> +\
|
|
<span class="es-help-key">⇧</span> +\
|
|
<span class="es-help-key">Arrow</span>\
|
|
</span>\
|
|
Adjust selection by words or blocks\
|
|
</div>' );
|
|
}
|
|
}
|
|
};
|
|
};
|