/** * ContentEditable surface. * * @class * @constructor * @param model {ve.dm.Surface} Model to observe */ ve.ce.Surface = function( $container, model ) { // Inheritance ve.EventEmitter.call( this ); // Properties this.model = model; this.documentView = new ve.ce.Document( model.getDocument() ); this.contextView = new ve.ui.Context( this ); this.$ = $container; this.isMouseDown = false; this.clipboard = {}; // Events this.$.on( { 'keydown': this.proxy( this.onKeyDown ), 'mousedown': this.proxy( this.onMouseDown ), 'mouseup': this.proxy( this.onMouseUp ), 'mousemove': this.proxy( this.onMouseMove ), 'cut copy': this.proxy( this.onCutCopy ), 'beforepaste paste': this.proxy( this.onPaste ), } ); // Initialization this.$.append( this.documentView.documentNode.$ ); try { document.execCommand( "enableObjectResizing", false, false ); document.execCommand( "enableInlineTableEditing", false, false ); } catch (e) { } }; /* Methods */ ve.ce.Surface.prototype.proxy = function( func ) { var _this = this; return( function() { return func.apply( _this, arguments ); }); }; ve.ce.Surface.prototype.onKeyDown = function( e ) { var rangySel = rangy.getSelection(); var offset = this.getOffset( rangySel.anchorNode, rangySel.anchorOffset ); console.log( 'onKeyDown', 'offset', offset ); switch ( e.which ) { case 37: // left arrow key var relativeContentOffset = this.documentView.model.getRelativeContentOffset( offset, -1 ); var relativeStructuralOffset = this.documentView.model.getRelativeStructuralOffset( offset - 1, -1, true ); var relativeStructuralOffsetNode = this.documentView.documentNode.getNodeFromOffset( relativeStructuralOffset ); var hasSlug = relativeStructuralOffsetNode.hasSlugAtOffset( relativeStructuralOffset ); if ( hasSlug ) { if ( relativeContentOffset > offset ) { this.showCursor( relativeStructuralOffset ); } else { this.showCursor( Math.max( relativeContentOffset, relativeStructuralOffset ) ); } } else { this.showCursor( relativeContentOffset ); } return false; break; case 39: // right arrow key var relativeContentOffset = this.documentView.model.getRelativeContentOffset( offset, 1 ); var relativeStructuralOffset = this.documentView.model.getRelativeStructuralOffset( offset + 1, 1, true ); var relativeStructuralOffsetNode = this.documentView.documentNode.getNodeFromOffset( relativeStructuralOffset ); var hasSlug = relativeStructuralOffsetNode.hasSlugAtOffset( relativeStructuralOffset ); if ( hasSlug ) { if ( relativeContentOffset < offset ) { this.showCursor( relativeStructuralOffset ); } else { this.showCursor( Math.min( relativeContentOffset, relativeStructuralOffset ) ); } } else { this.showCursor( relativeContentOffset ); } return false; break; } }; ve.ce.Surface.prototype.onMouseDown = function( e ) { this.isMouseDown = true; var _this = this; setTimeout( function() { var rangySel = rangy.getSelection(); var offset = _this.getOffset( rangySel.anchorNode, rangySel.anchorOffset ); console.log( 'onMouseDown', 'rangySel', rangySel.anchorNode, rangySel.anchorOffset ); console.log( 'onMouseDown', 'offset', offset ); }, 0 ); var $closestLeaf = $( e.target ).closest( '.ve-ce-leafNode' ); if ( $closestLeaf.length > 0 ) { console.log( 'onMouseDown', 'closest leaf of e.target', $closestLeaf ); var closestLeafModel = $closestLeaf.data( 'node' ).getModel(); var closestLeafOffset = closestLeafModel.getRoot().getOffsetFromNode( closestLeafModel ); console.log( 'onMouseDown', 'closestLeafOffset', closestLeafOffset ); var nearestContentOffset = this.documentView.model.getNearestContentOffset( closestLeafOffset, -1 ); console.log( 'onMouseDown', 'nearestContentOffset', nearestContentOffset ); var nearestStructuralOffset = this.documentView.model.getNearestStructuralOffset( closestLeafOffset, 0, true ); console.log( 'onMouseDown', 'nearestStructuralOffset', nearestStructuralOffset ); var nearestStructuralOffsetNode = this.documentView.documentNode.getNodeFromOffset( nearestStructuralOffset ); console.log( 'onMouseDown', 'nearestStructuralOffsetNode', nearestStructuralOffsetNode ); var hasSlug = nearestStructuralOffsetNode.hasSlugAtOffset( nearestStructuralOffset ); console.log( 'onMouseDown', 'hasSlug', hasSlug ); if ( hasSlug ) { if ( nearestContentOffset > closestLeafOffset ) { this.showCursor( nearestStructuralOffset ); } else { this.showCursor( Math.max( nearestStructuralOffset, nearestContentOffset ) ); } } else { this.showCursor( nearestContentOffset ); } return false; } }; ve.ce.Surface.prototype.onMouseUp = function( e ) { this.isMouseDown = false; }; ve.ce.Surface.prototype.onMouseMove = function( e ) { if ( this.isMouseDown === true ) { //... } }; /** * @method */ ve.ce.Surface.prototype.onCutCopy = function( e ) { var _this = this, sel = rangy.getSelection(), key = sel.getRangeAt(0).toString().replace( /\s/gm, '' ); this.clipboard[key] = ve.copyArray( this.documentView.model.getData( this.getSelectionRange() ) ); if ( e.type == 'cut' ) { setTimeout( function() { // we don't like how browsers cut, so let's undo it and do it ourselves. document.execCommand('undo', false, false); var selection = _this.getSelectionRange(); // transact var tx = _this.model.getDocument().prepareRemoval( selection ); _this.autoRender = true; _this.model.transact( tx ); _this.autoRender = false; _this.clearPollData(); // place cursor _this.showCursor( selection.start ); }, 1 ); } }; /** * @method */ ve.ce.Surface.prototype.onPaste = function( e ) { var _this = this, insertionPoint = _this.getSelectionRange().start; $('#paste').html('').show().focus(); setTimeout( function() { var pasteString = $('#paste').hide().text(), key = pasteString.replace( /\s/gm, '' ), pasteData = ( _this.clipboard[key] ) ? _this.clipboard[key] : pasteString.split(''); // transact var tx = ve.dm.Transaction.newFromInsertion( _this.documentView.model, insertionPoint, pasteData ); ve.dm.TransactionProcessor.commit( _this.documentView.model, tx ); // place cursor _this.showCursor( insertionPoint + pasteData.length ); _this.documentView.documentNode.$.focus(); }, 1 ); }; /** * Gets the linear offset based on a given DOM node (DOMnode) and offset (DOMoffset) * * @method * @param DOMnode {DOM Element} DOM Element * @param DOMoffset {Integer} DOM offset within the DOM Element * @returns {Integer} Linear model offset */ ve.ce.Surface.prototype.getOffset = function( DOMnode, DOMoffset ) { if ( DOMnode.nodeType === Node.TEXT_NODE ) { var $branch = $( DOMnode ).closest( '.ve-ce-branchNode' ), current = [$branch.contents(), 0], stack = [current], offset = 0, item, $item; while ( stack.length > 0 ) { if ( current[1] >= current[0].length ) { stack.pop(); current = stack[ stack.length - 1 ]; continue; } item = current[0][current[1]]; if ( item.nodeType === Node.TEXT_NODE ) { if ( item === DOMnode ) { offset += DOMoffset; break; } else { offset += item.textContent.length; } } else if ( item.nodeType === Node.ELEMENT_NODE ) { $item = current[0].eq( current[1] ); if ( $item.hasClass( 've-ce-slug' ) ) { if ( $item.contents()[0] === DOMnode ) { break; } } else if ( $item.hasClass( 've-ce-leafNode' ) ) { offset += 2; } else if ( $item.hasClass( 've-ce-branchNode' ) ) { offset += $item.data( 'node' ).getOuterLength(); } else { stack.push( [$item.contents(), 0 ] ); current[1]++; current = stack[stack.length-1]; continue; } } current[1]++; } var branchModel = $branch.data( 'node' ).getModel(), branchOffset = branchModel.getRoot().getOffsetFromNode( branchModel ); return offset + branchOffset + ( ( branchModel.isWrapped() ) ? 1 : 0 ); } else if ( DOMnode.nodeType === Node.ELEMENT_NODE ) { if ( DOMoffset === 0 ) { throw "Not implemented"; } else { var $node = $( DOMnode ).contents().eq( DOMoffset - 1 ), nodeModel = $node.data( 'node' ).getModel(), nodeOffset = nodeModel.getRoot().getOffsetFromNode( nodeModel ); return nodeOffset + nodeModel.getOuterLength(); } } throw "Not implemented"; }; /** * Based on a given offset returns DOM node and offset that can be used to place a cursor (with rangy) * * @method * @param offset {Integer} Linear model offset */ ve.ce.Surface.prototype.getNodeAndOffset = function( offset ) { var node = this.documentView.getNodeFromOffset( offset ), startOffset = this.documentView.getDocumentNode().getOffsetFromNode( node ) + ( ( node.isWrapped() ) ? 1 : 0 ), current = [node.$.contents(), 0], stack = [current], item, $item, length; while ( stack.length > 0 ) { if ( current[1] >= current[0].length ) { stack.pop(); current = stack[ stack.length - 1 ]; continue; } item = current[0][current[1]]; if ( item.nodeType === Node.TEXT_NODE ) { length = item.textContent.length; if ( offset >= startOffset && offset <= startOffset + length ) { return { node: item, offset: offset - startOffset }; } else { startOffset += length; } } else if ( item.nodeType === Node.ELEMENT_NODE ) { $item = current[0].eq( current[1] ); if ( $item.hasClass('ve-ce-slug') ) { if ( offset === startOffset ) { return { node: $item[0], offset: 1 }; } } else if ( $item.is( '.ve-ce-branchNode, .ve-ce-leafNode' ) ) { length = $item.data( 'node' ).model.getOuterLength(); if ( offset >= startOffset && offset < startOffset + length ) { stack.push( [$item.contents(), 0] ); current[1]++; current = stack[stack.length-1]; continue; } else { startOffset += length; } } else { stack.push( [$item.contents(), 0] ); current[1]++; current = stack[stack.length-1]; continue; } } current[1]++; } throw "Not implemented"; }; /** * Gets closest BranchNode (.ve-ce-branchNode) based on a given DOM node * * @method * @param elem {DOM Element} DOM Element * @returns {jQuery} jQuery element */ ve.ce.Surface.getBranchNode = function( elem ) { var $branch = $( elem ).closest( '.ve-ce-branchNode' ); return $branch.length ? $branch : null; }; /** * @method */ ve.ce.Surface.prototype.showCursor = function( offset ) { this.showSelection( new ve.Range( offset ) ); }; /** * @method */ ve.ce.Surface.prototype.showSelection = function( range ) { var start = this.getNodeAndOffset( range.start ), stop = this.getNodeAndOffset( range.end ), rangySel = rangy.getSelection(), rangyRange = rangy.createRange(); rangyRange.setStart( start.node, start.offset ); rangyRange.setEnd( stop.node, stop.offset ); rangySel.setSingleRange( rangyRange, range.start !== range.from ); }; /** * @method * @returns {ve.Range} Current selection range */ ve.ce.Surface.prototype.getSelectionRange = function() { var rangySel = rangy.getSelection(); if ( rangySel.isCollapsed ) { var offset = this.getOffset( rangySel.anchorNode, rangySel.anchorOffset, true ); return new ve.Range( offset, offset ); } else { return new ve.Range( this.getOffset( rangySel.anchorNode, rangySel.anchorOffset, true ), this.getOffset( rangySel.focusNode, rangySel.focusOffset, true ) ); } }; /** * @method */ ve.ce.Surface.prototype.getSelectionRect = function() { var rangySel = rangy.getSelection(); return { start: rangySel.getStartDocumentPos(), end: rangySel.getEndDocumentPos() }; }; /* Inheritance */ ve.extendClass( ve.ce.Surface, ve.EventEmitter );