/** * 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 = {}; /* Create document model object with the linear model */ this.documentModel = ve.dm.Document ( data ); this.surfaceModel = new ve.dm.Surface( this.documentModel ); // Setup VE DOM Skeleton this.setupBaseElements(); // Setup Surface View this.setupSurfaceView(); // Setup toolbars based on this.options this.setupToolbars(); // Setup various toolbar modes and panels this.setupModes(); // Registration ve.instances.push( this ); console.log (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() { 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 ); }; 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; };