mediawiki-extensions-Visual.../modules/ve/ve.Surface.js
Timo Tijhof 077e21867e Kranitor #3: jQuerlyfornication ft. The Cascaders
* 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
2012-07-28 13:05:57 -07:00

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> &#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>' );
}
}
};
};