From 7bcb65e03690f8ab0a4fee236203f6e1ac61f641 Mon Sep 17 00:00:00 2001 From: Rob Moen Date: Thu, 3 May 2012 13:23:21 -0700 Subject: [PATCH] - Port surface constructor into dmrewrite. - Create document model object linear model instead of wikidom Change-Id: I16307c290ead3e4b29dfbd1b76759e4c19c3fc8b --- modules/ve2/ve.Surface.js | 462 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 445 insertions(+), 17 deletions(-) diff --git a/modules/ve2/ve.Surface.js b/modules/ve2/ve.Surface.js index 579069f2e0..e1ca10cccc 100644 --- a/modules/ve2/ve.Surface.js +++ b/modules/ve2/ve.Surface.js @@ -1,34 +1,454 @@ /** * 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} id Unique name of editor instance + * @param {String} parent Selector of element to attach to * @param {Array} data Document data * @param {Object} options Configuration options */ -ve.Surface = function( id, data, options ) { +ve.Surface = function( parent, data, options ) { // Properties - this.id = id; - this.options = ve.extendObject( { + this.parent = parent; + this.modes = {}; + this.currentMode = null; + /* Extend VE configuration recursively */ + this.options = ve.extendObject( true, { // Default options - }, this.options ); - - this.documentModel = ve.dm.DocumentNode.newFromPlainObject( data ); + 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 ); - //TODO: Find source of breakage when view element is not #es-editor - this.view = new ve.ce.Surface( $( '#es-editor' ), this.surfaceModel ); - this.context = new ve.ui.Context( this.view ); - - //TODO: Configure toolbar based on this.options. - this.toolbar = new ve.ui.Toolbar( $( '#es-toolbar'), this.view, options.toolbars.top ); + // 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; }; @@ -37,6 +457,14 @@ ve.Surface.prototype.getDocumentModel = function() { return this.documentModel; }; -ve.Surface.prototype.getID = function() { - return this.id; +ve.Surface.prototype.getView = function() { + return this.view; }; + +ve.Surface.prototype.getContext = function() { + return this.context; +}; + +ve.Surface.prototype.getParent = function() { + return this.parent; +}; \ No newline at end of file