/**
* 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 {Array} data Document data
* @param {Object} options Configuration options
*/
ve.Surface = function( parent, data, options ) {
// 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 = {};
// Overwrite input data with example data
/*
data = [
{ 'type': 'heading', 'attributes': { 'level': 1 } },
'a',
'b',
'c',
{ 'type': '/heading' },
{ 'type': 'paragraph' },
'a',
['b', { '{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' } }],
['c', { '{"type":"textStyle/italic"}': { 'type': 'textStyle/italic' } }],
{ 'type': '/paragraph' },
{ 'type': 'paragraph' },
{ 'type': 'image', 'attributes': { 'html/src': 'http://placekitten.com/g/120/120' } },
{ 'type': '/image' },
'L',
'o',
'r',
'e',
'm',
' ',
'i',
'p',
's',
'u',
'm',
' ',
{ 'type': 'image', 'attributes': { 'html/src': 'http://placekitten.com/g/100/100' } },
{ 'type': '/image' },
' ',
'a',
'n',
'd',
{ 'type': '/paragraph' },
{ 'type': 'table' },
{ 'type': 'tableRow' },
{ 'type': 'tableCell' },
{ 'type': 'paragraph' },
['a', {
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic' },
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' }
}],
{ 'type': '/paragraph' },
{ 'type': '/tableCell' },
{ 'type': '/tableRow' },
{ 'type': '/table' },
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem', 'attributes': { 'style': 'item' } },
{ 'type': 'paragraph' },
'a',
{ 'type': '/paragraph' },
{ 'type': '/listItem' },
{ 'type': '/list' },
{ 'type': 'image', 'attributes': { 'html/src': 'http://dl.dropbox.com/u/1026938/wikia.jpeg' } },
{ 'type': '/image' },
];
*/
// Define HTML5 DOM
var HTML = $( '
Hello world! What\'s up?abc
abc
Lorem ipsum and
' );
// Create linear model from HTML5 DOM
data = ve.dm.HTMLConverter.getLinearModel( HTML[0] );
/* Create document model object with the linear model */
this.documentModel = new ve.dm.Document ( data );
this.surfaceModel = new ve.dm.Surface( this.documentModel );
// Setup VE DOM Skeleton
this.setupBaseElements();
// Setup Surface View
//this.setupSurfaceView();
this.$surface = $('').attr('class', 'es-editor');
this.$base.find('.es-visual').append( this.$surface );
/* Instantiate surface layer */
this.view = new ve.ce.Surface( $( '.es-editor' ), this.getSurfaceModel() );
//this.context = new ve.ui.Context( this.view );
// Setup toolbars based on this.options
this.setupToolbars();
// Setup various toolbar modes and panels
//this.setupModes();
// Registration
ve.instances.push( this );
};
/* Setup Methods */
ve.Surface.prototype.setupBaseElements = function() {
// Make new base element
this.$base = $('')
.attr( 'class', 'es-base' )
.append(
$('').attr('class', 'es-panes')
.append(
$('').attr('class', 'es-visual')
).append(
$('').attr('class', 'es-panels')
).append(
$('').attr('style', 'clear:both')
)
).append(
$('').attr( {
'id': 'paste', //TODO: make 'paste' in surface stateful and remove this attrib
'class': 'paste',
'contenteditable': 'true',
'style': 'height:1px;width:1px;display:none;opacity:0;position:absolute;'
})
);
// Attach the base the the parent
$( this.getParent() ).append( this.$base );
};
ve.Surface.prototype.setupSurfaceView = function() {
};
ve.Surface.prototype.setupToolbars = function() {
var _this = 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
_this.toolbarWrapper[name] = $('')
.attr('class', 'es-toolbar-wrapper')
.append(
$('').attr('class', 'es-toolbar')
.append(
$('').attr('class', 'es-modes')
).append(
$('').attr('style', 'clear:both')
).append(
$('').attr('class', 'es-toolbar-shadow')
)
);
_this.$base.find('.es-panes').before( _this.toolbarWrapper[name] );
}
// Instantiate the toolbar
_this['toolbar-' + name] = new ve.ui.Toolbar( _this.$base.find( '.es-toolbar' ), _this.view, config.tools );
}
});
// Setup sticky toolbar
this.makeMainEditorToolbarFloat();
};
/*
* 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.makeMainEditorToolbarFloat = 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();
if ( $window.scrollTop() > toolbarWrapperOffset.top ) {
if ( !$toolbarWrapper.hasClass( 'float' ) ) {
var left = toolbarWrapperOffset.left,
right = $window.width() - $toolbarWrapper.outerWidth() - left;
$toolbarWrapper.css( 'height', $toolbarWrapper.height() ).addClass( 'float' );
$toolbar.css( { 'left': left, 'right': right } );
}
} else {
if ( $toolbarWrapper.hasClass( 'float' ) ) {
$toolbarWrapper.css( 'height', 'auto' ).removeClass( 'float' );
$toolbar.css( { 'left': 0, 'right': 0 } );
}
}
} );
};
ve.Surface.prototype.setupModes = function(){
var _this = this;
var activeModes = [];
// Loop through toolbar config to build modes
$.each( _this.options.toolbars, function(name, toolbar){
//if toolbar has modes
if( toolbar.modes && toolbar.modes.length > 0 ) {
for(var i=0;i<=toolbar.modes.length -1;i++) {
$( _this.toolbarWrapper[name] )
.find('.es-modes')
.append(
$('').attr({
'class': 'es-modes-button es-mode-' + toolbar.modes[i],
'title': _this.options.modes[toolbar.modes[i]]
})
);
if( !activeModes[mode] ) {
activeModes.push( toolbar.modes[i] );
}
}
}
});
// Build elements in #es-panels for each activeMode
if ( activeModes.length > 0 ) {
for (var mode in activeModes) {
var renderType = '';
switch( activeModes[mode] ) {
case 'render':
renderType = 'es-render';
break;
case 'help':
renderType = '';
break;
default:
renderType = 'es-code';
break;
}
_this.$base
.find('.es-panels')
.append(
$('').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 = _this.$base.hasClass( 'es-showData' );
$('.es-modes-button').removeClass( 'es-modes-button-down' );
$('.es-panel').hide();
if ( disable ) {
if ( visible ) {
_this.$base.removeClass( 'es-showData' );
$( window ).resize();
}
_this.currentMode = null;
} else {
$(this).addClass( 'es-modes-button-down' );
mode.$panel.show();
if ( !visible ) {
_this.$base.addClass( 'es-showData' );
$( window ).resize();
}
mode.update.call( mode );
_this.currentMode = mode;
}
} );
} );
/* Bind some surface events for modes */
this.getSurfaceModel().on( 'transact', function() {
if ( _this.currentMode ) {
_this.currentMode.update.call( _this.currentMode );
}
} );
this.getSurfaceModel().on( 'select', function() {
if ( _this.currentMode === _this.modes.history ) {
_this.currentMode.update.call( _this.currentMode );
}
} );
};
/*
Define modes
TODO: possibly extend this object via the config
*/
ve.Surface.prototype.defineModes = function() {
var _this = this;
this.modes = {
'wikitext': {
'$': _this.$base.find( '.es-mode-wikitext' ),
'$panel': _this.$base.find( '.es-panel-wikitext' ),
'update': function() {
this.$panel.text(
ve.dm.WikitextSerializer.stringify( _this.getDocumentModel().getPlainObject() )
);
}
},
'json': {
'$': _this.$base.find( '.es-mode-json' ),
'$panel': _this.$base.find( '.es-panel-json' ),
'update': function() {
this.$panel.text( ve.dm.JsonSerializer.stringify( _this.getDocumentModel().getPlainObject(), {
'indentWith': ' '
} ) );
}
},
'html': {
'$': _this.$base.find( '.es-mode-html' ),
'$panel': _this.$base.find( '.es-panel-html' ),
'update': function() {
this.$panel.text(
ve.dm.HtmlSerializer.stringify( _this.getDocumentModel().getPlainObject() )
);
}
},
'render': {
'$': _this.$base.find( '.es-mode-render' ),
'$panel': _this.$base.find( '.es-panel-render' ),
'update': function() {
this.$panel.html(
ve.dm.HtmlSerializer.stringify( _this.getDocumentModel().getPlainObject() )
);
}
},
'history': {
'$': _this.$base.find( '.es-mode-history' ),
'$panel': _this.$base.find( '.es-panel-history' ),
'update': function() {
var history = _this.getSurfaceModel().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 += '' + operations.join(', ') + '
';
}
this.$panel.html( events );
}
},
'help': {
'$': _this.$base.find( '.es-mode-help' ),
'$panel': _this.$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('\
Keyboard Shortcuts
\
Clipboard
\
\
\
Ctrl or ⌘ +\
C\
\
Copy selected text\
\
\
\
Ctrl or ⌘ +\
X\
\
Cut selected text\
\
\
\
Ctrl or ⌘ +\
V\
\
Paste text at the cursor\
\
History navigation
\
\
\
Ctrl or ⌘ +\
Z\
\
Undo\
\
\
\
Ctrl or ⌘ +\
Y\
\
Redo\
\
\
\
Ctrl or ⌘ +\
⇧ +\
Z\
\
Redo\
\
Formatting
\
\
\
Ctrl or ⌘ +\
B\
\
Make selected text bold\
\
\
\
Ctrl or ⌘ +\
I\
\
Make selected text italic\
\
\
\
Ctrl or ⌘ +\
K\
\
Make selected text a link\
\
Selection
\
\
\
⇧ +\
Arrow\
\
Adjust selection\
\
\
\
Ctrl or ⌥ +\
Arrow\
\
Move cursor by words or blocks\
\
\
\
Ctrl or ⌥ +\
⇧ +\
Arrow\
\
Adjust selection by words or blocks\
');
}
}
};
};
/* Get Methods */
ve.Surface.prototype.getSurfaceModel = function() {
return this.surfaceModel;
};
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;
};