mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 10:35:48 +00:00
b11bbed7a6
See CODING.md for how to run it. Mistakes fixed: * Warning: Unknown type function -> Function * Warning: Unknown type DOMElement -> HTMLElement * Warning: Unknown type DOM Node -> HTMLElement * Warning: Unknown type Integer -> Mixed * Warning: Unknown type Command -> ve.Command * Warning: Unknown type any -> number * Warning: Unknown type ve.Transaction -> ve.dm.Transaction * Warning: Unknown type ve.dm.AnnotationSet -> ve.AnnotationSet * Warning: Unknown type false -> boolean * Warning: Unknown type ve.dm.AlienNode ve.dm doesn't have a generic AlienNode like ve.ce -> Unknown type ve.dm.AlienInlineNode|ve.dm.AlienBlockNode * Warning: Unknown type ve.ve.Surface -> ve.ce.Surface * ve.example.lookupNode: -> Last @param should be @return * ve.dm.Transaction.prototype.pushReplace: -> @param {Array] should be @param {Array} * Warning: ve.BranchNode.js:27: {@link ve.Node#hasChildren} links to non-existing member -> (removed) * Warning: ve.LeafNode.js:21: {@link ve.Node#hasChildren} links to non-existing member -> (removed) Differences fixed: * Variadic arguments are like @param {Type...} [name] instead of @param {Type} [name...] * Convert all file headers from /** to /*! because JSDuck tries to parse all /** blocks and fails to parse with all sorts of errors for "Global property", "Unnamed property", and "Duplicate property". Find: \/\*\*([^@]+)(@copyright) Replace: /*!$1$2 * Indented blocks are considered code examples. A few methods had documentation with numbered lists that were indented, which have now been updated to not be intended. * The free-form text descriptions are parsed with Markdown, which requires lists to be separated from paragraphs by an empty line. And we should use `backticks` instead of {braces} for inline code in text paragraphs. * Doc blocks for classes and their constructor have to be in the correct order (@constructor, @param, @return must be before @class, @abstract, @extends etc.) * `@extends Class` must not have Class {wrapped} * @throws must start with a {Type} * @example means something else. It is used for an inline demo iframe, not code block. For that simply indent with spaces. * @member means something else. Non-function properties are marked with @property, not @member. * To create a link to a class or member, in most cases the name is enough to create a link. E.g. Foo, Foo.bar, Foo.bar#quux, where a hash stands for "instance member", so Foo.bar#quux, links to Foo.bar.prototype.quux (the is not supported, as "prototype" is considered an implementation detail, it only indexes class name and method name). If the magic linker doesn't work for some case, the verbose syntax is {@link #target label}. * @property can't have sub-properties (nested @param and @return values are supported, only @static @property can't be nested). We only have one case of this, which can be worked around by moving those in a new virtual class. The code is unaltered (only moved down so that it isn't with the scope of the main @class block). ve.dm.TransactionProcessor.processors. New: * @mixins: Classes mixed into the current class. * @event: Events that can be emitted by a class. These are also inherited by subclasses. (+ @param, @return and @preventable). So ve.Node#event-attach is inherited to ve.dm.BreakNode, just like @method is. * @singleton: Plain objects such as ve, ve.dm, ve.ce were missing documentation causing a tree error. Documented those as a JSDuck singleton, which they but just weren't documented yet. NB: Members of @singleton don't need @static (if present, triggers a compiler warning). * @chainable: Shorthand for "@return this". We were using "@return {classname}" which is ambiguous (returns the same instance or another instance?), @chainable is specifically for "@return this". Creates proper labels in the generated HTML pages. Removed: * @mixin: (not to be confused with @mixins). Not supported by JSDuck. Every class is standalone anyway. Where needed marked them @class + @abstract instead. Change-Id: I6a7c9e8ee8f995731bc205d666167874eb2ebe23
1320 lines
35 KiB
JavaScript
1320 lines
35 KiB
JavaScript
/*global rangy */
|
||
|
||
/*!
|
||
* VisualEditor content editable Surface class.
|
||
*
|
||
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
||
* @license The MIT License (MIT); see LICENSE.txt
|
||
*/
|
||
|
||
/**
|
||
* ContentEditable surface.
|
||
*
|
||
* @class
|
||
* @extends ve.EventEmitter
|
||
* @constructor
|
||
* @param {jQuery} $container
|
||
* @param {ve.dm.Surface} model Model to observe
|
||
* @param {ve.Surface} surface The surface of this view
|
||
*/
|
||
ve.ce.Surface = function VeCeSurface( $container, model, surface ) {
|
||
// Parent constructor
|
||
ve.EventEmitter.call( this );
|
||
|
||
// Properties
|
||
this.surface = surface;
|
||
this.inIme = false;
|
||
this.model = model;
|
||
this.documentView = new ve.ce.Document( model.getDocument(), this );
|
||
this.surfaceObserver = new ve.ce.SurfaceObserver( this.documentView );
|
||
this.selectionTimeout = null;
|
||
this.$ = $container;
|
||
this.$document = $( document );
|
||
this.clipboard = {};
|
||
this.renderingEnabled = true;
|
||
this.dragging = false;
|
||
this.selecting = false;
|
||
this.$phantoms = $( '<div class="ve-ce-phantoms">' );
|
||
this.pasting = false;
|
||
|
||
// Events
|
||
this.surfaceObserver.addListenerMethods(
|
||
this, { 'contentChange': 'onContentChange', 'selectionChange': 'onSelectionChange' }
|
||
);
|
||
this.model.addListenerMethods(
|
||
this, { 'change': 'onChange', 'lock': 'onLock', 'unlock': 'onUnlock' }
|
||
);
|
||
this.documentView.getDocumentNode().$.on( {
|
||
'focus': ve.bind( this.documentOnFocus, this ),
|
||
'blur': ve.bind( this.documentOnBlur, this )
|
||
} );
|
||
this.$.on( {
|
||
'cut': ve.bind( this.onCut, this ),
|
||
'copy': ve.bind( this.onCopy, this ),
|
||
'paste': ve.bind( this.onPaste, this ),
|
||
'dragover drop': function ( e ) {
|
||
// Prevent content drag & drop
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
} );
|
||
if ( $.browser.msie ) {
|
||
this.$.on( 'beforepaste', ve.bind( this.onPaste, this ) );
|
||
}
|
||
|
||
// Initialization
|
||
rangy.init();
|
||
ve.ce.Surface.clearLocalStorage();
|
||
this.$.append( this.documentView.getDocumentNode().$ );
|
||
this.$.append( this.$phantoms );
|
||
};
|
||
|
||
/* Inheritance */
|
||
|
||
ve.inheritClass( ve.ce.Surface, ve.EventEmitter );
|
||
|
||
/* Static Members */
|
||
|
||
ve.ce.Surface.static = {};
|
||
|
||
ve.ce.Surface.static.$phantomTemplate = $( '<div class="ve-ce-phantom" draggable="false"></div>' )
|
||
.attr( 'title', ve.msg ( 'visualeditor-aliennode-tooltip' ) );
|
||
|
||
/* Methods */
|
||
|
||
/**
|
||
* Destroy the surface, removing all DOM elements.
|
||
*
|
||
* @method
|
||
* @returns {ve.ui.Context} Context user interface
|
||
*/
|
||
ve.ce.Surface.prototype.destroy = function () {
|
||
this.$.remove();
|
||
};
|
||
|
||
/**
|
||
* Disables editing.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.disable = function () {
|
||
this.documentView.getDocumentNode().disable();
|
||
};
|
||
|
||
/**
|
||
* Enables editing.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.enable = function () {
|
||
this.documentView.getDocumentNode().enable();
|
||
};
|
||
|
||
/**
|
||
* Handles insertion of content.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.handleInsertion = function () {
|
||
var slug, data, range, annotations, insertionAnnotations, placeholder,
|
||
selection = this.model.getSelection();
|
||
|
||
// Handles removing expanded selection before inserting new text
|
||
if ( !selection.isCollapsed() ) {
|
||
// Pull annotations from the first character in the selection
|
||
annotations = this.model.documentModel.getAnnotationsFromRange(
|
||
new ve.Range( selection.start, selection.start + 1 )
|
||
);
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromRemoval( this.documentView.model, selection ),
|
||
new ve.Range( selection.start )
|
||
);
|
||
this.surfaceObserver.clear();
|
||
selection = this.model.getSelection();
|
||
this.model.setInsertionAnnotations( annotations );
|
||
}
|
||
insertionAnnotations = this.model.getInsertionAnnotations() || new ve.AnnotationSet();
|
||
if ( selection.isCollapsed() ) {
|
||
slug = this.documentView.getSlugAtOffset( selection.start );
|
||
// Is this a slug or are the annotations to the left different than the insertion
|
||
// annotations?
|
||
if (
|
||
slug || (
|
||
selection.start > 0 &&
|
||
!ve.compareObjects (
|
||
this.model.getDocument().getAnnotationsFromOffset( selection.start - 1 ),
|
||
insertionAnnotations
|
||
)
|
||
)
|
||
) {
|
||
placeholder = '\u2659';
|
||
if ( !insertionAnnotations.isEmpty() ) {
|
||
placeholder = [placeholder, insertionAnnotations];
|
||
}
|
||
// is this a slug and if so, is this a block slug?
|
||
if ( slug && ve.dm.Document.isStructuralOffset(
|
||
this.documentView.model.data, selection.start
|
||
) ) {
|
||
range = new ve.Range( selection.start + 1, selection.start + 2 );
|
||
data = [{ 'type' : 'paragraph' }, placeholder, { 'type' : '/paragraph' }];
|
||
} else {
|
||
range = new ve.Range( selection.start, selection.start + 1 );
|
||
data = [placeholder];
|
||
}
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromInsertion(
|
||
this.documentView.model, selection.start, data
|
||
),
|
||
range
|
||
);
|
||
this.surfaceObserver.clear();
|
||
}
|
||
}
|
||
|
||
this.surfaceObserver.stop( true );
|
||
};
|
||
|
||
/**
|
||
* Responds to 'contentChange' events emitted in {ve.ce.SurfaceObserver.prototype.poll}.
|
||
*
|
||
* @method
|
||
* @param {HTMLElement} node DOM node the change occured in
|
||
* @param {Object} previous Old data
|
||
* @param {Object} previous.text Old plain text content
|
||
* @param {Object} previous.hash Old DOM hash
|
||
* @param {Object} previous.range Old selection
|
||
* @param {Object} next New data
|
||
* @param {Object} next.text New plain text content
|
||
* @param {Object} next.hash New DOM hash
|
||
* @param {Object} next.range New selection
|
||
*/
|
||
ve.ce.Surface.prototype.onContentChange = function ( node, previous, next ) {
|
||
var data, range, len, annotations, offsetDiff, lengthDiff, sameLeadingAndTrailing,
|
||
previousStart, nextStart, newRange,
|
||
fromLeft = 0,
|
||
fromRight = 0,
|
||
nodeOffset = node.getModel().getOffset();
|
||
|
||
if ( previous.range && next.range ) {
|
||
offsetDiff = ( previous.range.isCollapsed() && next.range.isCollapsed() ) ?
|
||
next.range.start - previous.range.start : null;
|
||
lengthDiff = next.text.length - previous.text.length;
|
||
previousStart = previous.range.start - nodeOffset - 1;
|
||
nextStart = next.range.start - nodeOffset - 1;
|
||
sameLeadingAndTrailing = offsetDiff !== null && (
|
||
// TODO: rewrite to static method with tests
|
||
(
|
||
lengthDiff > 0 &&
|
||
previous.text.substring( 0, previousStart ) ===
|
||
next.text.substring( 0, previousStart ) &&
|
||
previous.text.substring( previousStart ) ===
|
||
next.text.substring( nextStart )
|
||
) ||
|
||
(
|
||
lengthDiff < 0 &&
|
||
previous.text.substring( 0, nextStart ) ===
|
||
next.text.substring( 0, nextStart ) &&
|
||
previous.text.substring( previousStart - lengthDiff + offsetDiff) ===
|
||
next.text.substring( nextStart )
|
||
)
|
||
);
|
||
|
||
// Simple insertion
|
||
if ( lengthDiff > 0 && offsetDiff === lengthDiff /* && sameLeadingAndTrailing */) {
|
||
data = next.text.substring(
|
||
previous.range.start - nodeOffset - 1,
|
||
next.range.start - nodeOffset - 1
|
||
).split( '' );
|
||
// Apply insertion annotations
|
||
annotations = this.model.getInsertionAnnotations();
|
||
if ( annotations instanceof ve.AnnotationSet ) {
|
||
ve.dm.Document.addAnnotationsToData( data, this.model.getInsertionAnnotations() );
|
||
}
|
||
this.disableRendering();
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromInsertion(
|
||
this.documentView.model, previous.range.start, data
|
||
),
|
||
next.range
|
||
);
|
||
this.enableRendering();
|
||
return;
|
||
}
|
||
|
||
// Simple deletion
|
||
if ( ( offsetDiff === 0 || offsetDiff === lengthDiff ) && sameLeadingAndTrailing ) {
|
||
if ( offsetDiff === 0 ) {
|
||
range = new ve.Range( next.range.start, next.range.start - lengthDiff );
|
||
} else {
|
||
range = new ve.Range( next.range.start, previous.range.start );
|
||
}
|
||
this.disableRendering();
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromRemoval( this.documentView.model, range ),
|
||
next.range
|
||
);
|
||
this.enableRendering();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Complex change
|
||
|
||
len = Math.min( previous.text.length, next.text.length );
|
||
// Count same characters from left
|
||
while ( fromLeft < len && previous.text[fromLeft] === next.text[fromLeft] ) {
|
||
++fromLeft;
|
||
}
|
||
// Count same characters from right
|
||
while (
|
||
fromRight < len - fromLeft &&
|
||
previous.text[previous.text.length - 1 - fromRight] ===
|
||
next.text[next.text.length - 1 - fromRight]
|
||
) {
|
||
++fromRight;
|
||
}
|
||
data = next.text.substring( fromLeft, next.text.length - fromRight ).split( '' );
|
||
// Get annotations to the left of new content and apply
|
||
annotations =
|
||
this.model.getDocument().getAnnotationsFromOffset( nodeOffset + 1 + fromLeft );
|
||
if ( annotations.getLength() ) {
|
||
ve.dm.Document.addAnnotationsToData( data, annotations );
|
||
}
|
||
newRange = next.range;
|
||
if ( newRange.isCollapsed() ) {
|
||
newRange = new ve.Range( this.getNearestCorrectOffset( newRange.start, 1 ) );
|
||
}
|
||
if ( data.length > 0 ) {
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromInsertion(
|
||
this.documentView.model, nodeOffset + 1 + fromLeft, data
|
||
),
|
||
newRange
|
||
);
|
||
}
|
||
if ( fromLeft + fromRight < previous.text.length ) {
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromRemoval(
|
||
this.documentView.model,
|
||
new ve.Range(
|
||
data.length + nodeOffset + 1 + fromLeft,
|
||
data.length + nodeOffset + 1 + previous.text.length - fromRight
|
||
)
|
||
),
|
||
newRange
|
||
);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to 'selectionChange' events emitted in {ve.ce.SurfaceObserver.prototype.poll}.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.onSelectionChange = function ( oldRange, newRange ) {
|
||
this.disableRendering();
|
||
this.model.change( null, newRange );
|
||
this.enableRendering();
|
||
};
|
||
|
||
/**
|
||
* Responds to surface lock events.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.onLock = function () {
|
||
this.surfaceObserver.stop();
|
||
};
|
||
|
||
/**
|
||
* Responds to surface lock events.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.onUnlock = function () {
|
||
this.surfaceObserver.clear( this.model.getSelection() );
|
||
this.surfaceObserver.start();
|
||
};
|
||
|
||
/**
|
||
* Responds to document focus events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.documentOnFocus = function () {
|
||
this.$document.off( '.ve-ce-Surface' );
|
||
this.$document.on( {
|
||
'keydown.ve-ce-Surface': ve.bind( this.onKeyDown, this ),
|
||
'keyup.ve-ce-Surface': ve.bind( this.onKeyUp, this ),
|
||
'keypress.ve-ce-Surface': ve.bind( this.onKeyPress, this ),
|
||
'mousedown.ve-ce-Surface': ve.bind( this.onMouseDown, this ),
|
||
'mouseup.ve-ce-Surface': ve.bind( this.onMouseUp, this ),
|
||
'mousemove.ve-ce-Surface': ve.bind( this.onMouseMove, this ),
|
||
'compositionstart.ve-ce-Surface': ve.bind( this.onCompositionStart, this ),
|
||
'compositionend.ve-ce-Surface': ve.bind( this.onCompositionEnd, this ),
|
||
} );
|
||
this.surfaceObserver.start( true );
|
||
};
|
||
|
||
ve.ce.Surface.prototype.onCompositionStart = function () {
|
||
if ( $.browser.msie === true ) {
|
||
return;
|
||
}
|
||
this.inIme = true;
|
||
this.handleInsertion();
|
||
};
|
||
|
||
ve.ce.Surface.prototype.onCompositionEnd = function () {
|
||
this.inIme = false;
|
||
this.surfaceObserver.start();
|
||
};
|
||
|
||
/**
|
||
* Responds to document blur events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.documentOnBlur = function () {
|
||
this.$document.off( '.ve-ce-Surface' );
|
||
this.surfaceObserver.stop( true );
|
||
};
|
||
|
||
/**
|
||
* Responds to document mouse down events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onMouseDown = function ( e ) {
|
||
// Remember the mouse is down
|
||
this.dragging = true;
|
||
|
||
// Old code to figure out if user clicked inside the document or not - leave it here for now
|
||
// $( e.target ).closest( '.ve-ce-documentNode' ).length === 0
|
||
|
||
if ( e.which === 1 ) {
|
||
this.surfaceObserver.stop( true );
|
||
}
|
||
|
||
// Block / prevent triple click
|
||
if ( e.originalEvent.detail > 2 ) {
|
||
e.preventDefault();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to document mouse up events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onMouseUp = function ( e ) {
|
||
this.surfaceObserver.start();
|
||
if ( !e.shiftKey && this.selecting ) {
|
||
this.emit( 'selectionEnd' );
|
||
this.selecting = false;
|
||
}
|
||
this.dragging = false;
|
||
};
|
||
|
||
/**
|
||
* Responds to document mouse move events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onMouseMove = function () {
|
||
// Detect beginning of selection by moving mouse while dragging
|
||
if ( this.dragging && !this.selecting ) {
|
||
this.selecting = true;
|
||
this.emit( 'selectionStart' );
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to document key up events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onKeyUp = function ( e ) {
|
||
// Detect end of selecting by letting go of shift
|
||
if ( !this.dragging && this.selecting && e.keyCode === 16 ) {
|
||
this.selecting = false;
|
||
this.emit( 'selectionEnd' );
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to document key down events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onKeyDown = function ( e ) {
|
||
if ( this.inIme === true ) {
|
||
return;
|
||
}
|
||
|
||
if ( e.which === 229 && $.browser.msie === true ) {
|
||
this.inIme = true;
|
||
this.handleInsertion();
|
||
return;
|
||
}
|
||
|
||
// Detect start of selecting using shift+arrow keys
|
||
if ( !this.dragging && !this.selecting && e.shiftKey && e.keyCode >= 37 && e.keyCode <= 40 ) {
|
||
this.selecting = true;
|
||
this.emit( 'selectionStart' );
|
||
}
|
||
|
||
switch ( e.keyCode ) {
|
||
// Left arrow
|
||
case 37:
|
||
if ( this.adjustCursor( -1 ) ) {
|
||
e.preventDefault();
|
||
}
|
||
break;
|
||
// Right arrow
|
||
case 39:
|
||
if ( this.adjustCursor( 1 ) ) {
|
||
e.preventDefault();
|
||
}
|
||
break;
|
||
// Enter
|
||
case 13:
|
||
e.preventDefault();
|
||
this.handleEnter( e );
|
||
break;
|
||
// Backspace
|
||
case 8:
|
||
this.handleDelete( e, true );
|
||
this.surfaceObserver.stop( true );
|
||
this.surfaceObserver.start();
|
||
break;
|
||
// Delete
|
||
case 46:
|
||
this.handleDelete( e, false );
|
||
this.surfaceObserver.stop( true );
|
||
this.surfaceObserver.start();
|
||
break;
|
||
default:
|
||
// Execute key command if available
|
||
this.surfaceObserver.stop( true );
|
||
if ( this.surface.execute( new ve.Command( e ) ) ) {
|
||
e.preventDefault();
|
||
}
|
||
this.surfaceObserver.start();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to copy events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onCopy = function () {
|
||
var sel = rangy.getSelection(),
|
||
$frag = $( sel.getRangeAt(0).cloneContents() ),
|
||
slice = this.documentView.model.getSlice( this.model.getSelection() ),
|
||
key = '';
|
||
|
||
// Create key from text and element names
|
||
$frag.contents().each( function () {
|
||
key += this.textContent || this.nodeName;
|
||
} );
|
||
key = 've-' + key.replace( /\s/gm, '' );
|
||
|
||
// Set clipboard and localStorage
|
||
this.clipboard[key] = slice;
|
||
try {
|
||
localStorage.setItem(
|
||
key,
|
||
JSON.stringify( {
|
||
'time': new Date().getTime(),
|
||
'data': slice
|
||
} )
|
||
);
|
||
} catch ( e ) {
|
||
// Silently ignore
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to cut events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onCut = function ( e ) {
|
||
var surface = this;
|
||
|
||
this.surfaceObserver.stop();
|
||
|
||
this.onCopy( e );
|
||
|
||
setTimeout( function () {
|
||
var selection,
|
||
tx;
|
||
|
||
// We don't like how browsers cut, so let's undo it and do it ourselves.
|
||
document.execCommand( 'undo', false, false );
|
||
|
||
selection = surface.model.getSelection();
|
||
|
||
// Transact
|
||
tx = ve.dm.Transaction.newFromRemoval( surface.documentView.model, selection );
|
||
surface.model.change( tx, new ve.Range( selection.start ) );
|
||
|
||
surface.surfaceObserver.clear();
|
||
surface.surfaceObserver.start();
|
||
}, 1 );
|
||
};
|
||
|
||
/**
|
||
* Responds to paste events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onPaste = function ( e ) {
|
||
// Prevent pasting until after we are done
|
||
if ( this.pasting ) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
this.pasting = true;
|
||
|
||
var tx, scrollTop,
|
||
$window = $( window ),
|
||
view = this,
|
||
selection = this.model.getSelection();
|
||
|
||
this.surfaceObserver.stop();
|
||
|
||
// Pasting into a range? Remove first.
|
||
if ( !rangy.getSelection().isCollapsed ) {
|
||
tx = ve.dm.Transaction.newFromRemoval( view.documentView.model, selection );
|
||
view.model.change( tx );
|
||
}
|
||
|
||
// Save scroll position and change focus to "offscreen" paste target
|
||
scrollTop = $window.scrollTop();
|
||
$( '#paste' ).html( '' ).show().focus();
|
||
|
||
setTimeout( function () {
|
||
var pasteText, pasteData, tx,
|
||
key = '';
|
||
|
||
// Create key from text and element names
|
||
$( '#paste' ).hide().contents().each( function () {
|
||
key += this.textContent || this.nodeName;
|
||
} );
|
||
key = 've-' + key.replace( /\s/gm, '' );
|
||
|
||
// Get linear model from clipboard, localStorage, or create array from unknown pasted content
|
||
if ( view.clipboard[key] ) {
|
||
pasteData = view.clipboard[key];
|
||
}
|
||
/*
|
||
else if ( localStorage.getItem( key ) ) {
|
||
pasteData = localStorage.getItem( key ).data;
|
||
}
|
||
*/
|
||
else {
|
||
pasteText = $( '#paste' ).text().replace( /\n/gm, '');
|
||
pasteData = new ve.dm.DocumentSlice( pasteText.split( '' ) );
|
||
}
|
||
|
||
// Transact
|
||
try {
|
||
tx = ve.dm.Transaction.newFromInsertion(
|
||
view.documentView.model,
|
||
selection.start,
|
||
pasteData.getData()
|
||
);
|
||
} catch ( e ) {
|
||
tx = ve.dm.Transaction.newFromInsertion(
|
||
view.documentView.model,
|
||
selection.start,
|
||
pasteData.getBalancedData()
|
||
);
|
||
}
|
||
|
||
// Restore focus and scroll position
|
||
view.documentView.documentNode.$.focus();
|
||
$window.scrollTop( scrollTop );
|
||
|
||
view.model.change( tx, tx.translateRange( selection ).truncate( 0 ) );
|
||
|
||
// Allow pasting again
|
||
view.pasting = false;
|
||
}, 0 );
|
||
};
|
||
|
||
/**
|
||
* Responds to document key press events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.onKeyPress = function ( e ) {
|
||
if ( ve.ce.Surface.isShortcutKey( e ) || e.which === 13 || e.which === 8 || e.which === 0 ) {
|
||
return;
|
||
}
|
||
this.handleInsertion();
|
||
setTimeout( ve.bind( function () {
|
||
this.surfaceObserver.start();
|
||
}, this ), 0 );
|
||
};
|
||
|
||
/**
|
||
* Called from ve.dm.Surface.prototype.change.
|
||
*
|
||
* @method
|
||
* @param {ve.dm.Transaction|null} transaction
|
||
* @param {ve.Range|undefined} selection
|
||
*/
|
||
ve.ce.Surface.prototype.onChange = function ( transaction, selection ) {
|
||
if ( selection && this.isRenderingEnabled() ) {
|
||
this.showSelection( selection );
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to enter key events.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.prototype.handleEnter = function ( e ) {
|
||
var tx, outerParent, outerChildrenCount, list,
|
||
selection = this.model.getSelection(),
|
||
documentModel = this.model.getDocument(),
|
||
emptyParagraph = [{ 'type': 'paragraph' }, { 'type': '/paragraph' }],
|
||
advanceCursor = true,
|
||
node = this.documentView.getNodeFromOffset( selection.from ),
|
||
nodeModel = node.getModel(),
|
||
cursor = selection.from,
|
||
contentBranchModel = nodeModel.isContent() ? nodeModel.getParent() : nodeModel,
|
||
contentBranchModelRange = contentBranchModel.getRange(),
|
||
stack = [],
|
||
outermostNode = null;
|
||
|
||
// Stop polling while we work
|
||
this.surfaceObserver.stop();
|
||
|
||
// Handle removal first
|
||
if ( selection.from !== selection.to ) {
|
||
tx = ve.dm.Transaction.newFromRemoval( documentModel, selection );
|
||
selection = tx.translateRange( selection );
|
||
this.model.change( tx, selection );
|
||
}
|
||
|
||
// Handle insertion
|
||
if (
|
||
contentBranchModel.getType() !== 'paragraph' &&
|
||
(
|
||
cursor === contentBranchModelRange.from ||
|
||
cursor === contentBranchModelRange.to
|
||
)
|
||
) {
|
||
// If we're at the start/end of something that's not a paragraph, insert a paragraph
|
||
// before/after
|
||
if ( cursor === contentBranchModelRange.from ) {
|
||
tx = ve.dm.Transaction.newFromInsertion(
|
||
documentModel, contentBranchModel.getOuterRange().from, emptyParagraph
|
||
);
|
||
} else if ( cursor === contentBranchModelRange.to ) {
|
||
tx = ve.dm.Transaction.newFromInsertion(
|
||
documentModel, contentBranchModel.getOuterRange().to, emptyParagraph
|
||
);
|
||
}
|
||
} else {
|
||
// Split
|
||
ve.Node.traverseUpstream( node, function ( node ) {
|
||
if ( !node.canBeSplit() ) {
|
||
return false;
|
||
}
|
||
stack.splice(
|
||
stack.length / 2,
|
||
0,
|
||
{ 'type': '/' + node.type },
|
||
node.model.getClonedElement()
|
||
);
|
||
outermostNode = node;
|
||
if ( e.shiftKey ) {
|
||
return false;
|
||
} else {
|
||
return true;
|
||
}
|
||
} );
|
||
|
||
outerParent = outermostNode.getModel().getParent();
|
||
outerChildrenCount = outerParent.getChildren().length;
|
||
|
||
if (
|
||
// This is a list item
|
||
outermostNode.type === 'listItem' &&
|
||
// This is the last list item
|
||
outerParent.getChildren()[outerChildrenCount - 1] === outermostNode.getModel() &&
|
||
// There is one child
|
||
outermostNode.children.length === 1 &&
|
||
// The child is empty
|
||
node.model.length === 0
|
||
) {
|
||
// Enter was pressed in an empty list item.
|
||
list = outermostNode.getModel().getParent();
|
||
// Remove the list item
|
||
tx = ve.dm.Transaction.newFromRemoval(
|
||
documentModel, outermostNode.getModel().getOuterRange()
|
||
);
|
||
this.model.change( tx );
|
||
// Insert a paragraph
|
||
tx = ve.dm.Transaction.newFromInsertion(
|
||
documentModel, list.getOuterRange().to, emptyParagraph
|
||
);
|
||
advanceCursor = false;
|
||
} else {
|
||
// We must process the transaction first because getRelativeContentOffset can't help us
|
||
// yet
|
||
tx = ve.dm.Transaction.newFromInsertion( documentModel, selection.from, stack );
|
||
}
|
||
}
|
||
|
||
// Commit the transaction
|
||
this.model.change( tx );
|
||
|
||
// Now we can move the cursor forward
|
||
if ( advanceCursor ) {
|
||
this.model.change(
|
||
null, new ve.Range( documentModel.getRelativeContentOffset( selection.from, 1 ) )
|
||
);
|
||
} else {
|
||
this.model.change(
|
||
null, new ve.Range( documentModel.getNearestContentOffset( selection.from ) )
|
||
);
|
||
}
|
||
// Reset and resume polling
|
||
this.surfaceObserver.clear();
|
||
this.surfaceObserver.start();
|
||
};
|
||
|
||
/**
|
||
* Adjusts the cursor position in a given distance.
|
||
*
|
||
* This method only affects the selection target, preserving selections that are not collapsed and
|
||
* the direction of the selection.
|
||
*
|
||
* @method
|
||
* @param {number} adjustment Distance to adjust the cursor, can be positive or negative
|
||
* @returns {boolean} Cursor was moved
|
||
*/
|
||
ve.ce.Surface.prototype.adjustCursor = function ( adjustment ) {
|
||
// Bypass for zero-adjustment
|
||
if ( !adjustment ) {
|
||
return false;
|
||
}
|
||
var adjustedTargetOffset,
|
||
bias = adjustment > 0 ? 1 : -1,
|
||
selection = this.model.getSelection(),
|
||
targetOffset = selection.to,
|
||
documentModel = this.model.getDocument(),
|
||
relativeContentOffset = documentModel.getRelativeContentOffset( targetOffset, adjustment ),
|
||
relativeStructuralOffset = documentModel.getRelativeStructuralOffset(
|
||
targetOffset + bias, adjustment, true
|
||
);
|
||
// Check if we've moved into a slug
|
||
if ( this.hasSlugAtOffset( relativeStructuralOffset ) ) {
|
||
// Check if the relative content offset is in the opposite direction we are trying to go
|
||
if ( ( relativeContentOffset - targetOffset < 0 ? -1 : 1 ) !== bias ) {
|
||
// There's nothing past the slug we are already in, stay in it
|
||
adjustedTargetOffset = relativeStructuralOffset;
|
||
} else {
|
||
// There's a slug neaby, go into it if it's closer
|
||
adjustedTargetOffset = adjustment < 0 ?
|
||
Math.max( relativeContentOffset, relativeStructuralOffset ) :
|
||
Math.min( relativeContentOffset, relativeStructuralOffset );
|
||
}
|
||
}
|
||
// Check if we've moved a different distance than we asked for
|
||
else if ( relativeContentOffset !== targetOffset + adjustment ) {
|
||
// We can't trust the browser, move programatically
|
||
adjustedTargetOffset = relativeContentOffset;
|
||
}
|
||
// If the target changed, update the model
|
||
if ( adjustedTargetOffset ) {
|
||
this.model.change(
|
||
null,
|
||
new ve.Range(
|
||
selection.isCollapsed() ?
|
||
adjustedTargetOffset : selection.from, adjustedTargetOffset
|
||
)
|
||
);
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* Responds to backspace and delete key events.
|
||
*
|
||
* @method
|
||
* @param {boolean} Key was a backspace
|
||
*/
|
||
ve.ce.Surface.prototype.handleDelete = function ( e, backspace ) {
|
||
var sourceOffset,
|
||
targetOffset,
|
||
sourceSplitableNode,
|
||
targetSplitableNode,
|
||
tx,
|
||
cursorAt,
|
||
sourceNode,
|
||
targetNode,
|
||
sourceData,
|
||
nodeToDelete,
|
||
adjacentData,
|
||
adjacentText,
|
||
adjacentTextAfterMatch,
|
||
endOffset,
|
||
i,
|
||
selection = this.model.getSelection(),
|
||
containsInlineElements = false;
|
||
|
||
if ( selection.from === selection.to ) {
|
||
// Set source and target linmod offsets
|
||
if ( backspace ) {
|
||
sourceOffset = selection.to;
|
||
targetOffset = this.getNearestCorrectOffset( sourceOffset - 1, -1 );
|
||
|
||
// At the beginning of the document - don't do anything and preventDefault
|
||
if ( sourceOffset === targetOffset ) {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
|
||
} else {
|
||
sourceOffset = this.model.getDocument().getRelativeContentOffset( selection.to, 1 );
|
||
targetOffset = selection.to;
|
||
|
||
// At the end of the document - don't do anything and preventDefault
|
||
if ( sourceOffset <= targetOffset ) {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Set source and target nodes
|
||
sourceNode = this.documentView.getNodeFromOffset( sourceOffset, false ),
|
||
targetNode = this.documentView.getNodeFromOffset( targetOffset, false );
|
||
|
||
if ( sourceNode.type === targetNode.type ) {
|
||
sourceSplitableNode = ve.ce.Node.getSplitableNode( sourceNode );
|
||
targetSplitableNode = ve.ce.Node.getSplitableNode( targetNode );
|
||
}
|
||
//ve.log(sourceSplitableNode, targetSplitableNode);
|
||
|
||
// Save target location of cursor
|
||
cursorAt = targetOffset;
|
||
|
||
// Get text from cursor location to end of node in the proper direction
|
||
adjacentData = null;
|
||
adjacentText = '';
|
||
|
||
if ( backspace ) {
|
||
adjacentData = sourceNode.model.doc.data.slice(
|
||
sourceNode.model.getOffset() + ( sourceNode.model.isWrapped() ? 1 : 0 ) ,
|
||
sourceOffset
|
||
);
|
||
} else {
|
||
endOffset = targetNode.model.getOffset() +
|
||
targetNode.model.getLength() +
|
||
( targetNode.model.isWrapped() ? 1 : 0 );
|
||
adjacentData = targetNode.model.doc.data.slice( targetOffset, endOffset );
|
||
}
|
||
|
||
for ( i = 0; i < adjacentData.length; i++ ) {
|
||
if ( adjacentData[i].type !== undefined ) {
|
||
containsInlineElements = true;
|
||
break;
|
||
}
|
||
adjacentText += adjacentData[i][0];
|
||
}
|
||
|
||
if ( !containsInlineElements ) {
|
||
adjacentTextAfterMatch = adjacentText.match(
|
||
/[a-zA-Z\-_’'‘ÆÐƎƏƐƔIJŊŒẞÞǷȜæðǝəɛɣijŋœĸſßþƿȝĄƁÇĐƊĘĦĮƘŁØƠŞȘŢȚŦŲƯY̨Ƴąɓçđɗęħįƙłøơşșţțŧųưy̨ƴÁÀÂÄǍĂĀÃÅǺĄÆǼǢƁĆĊĈČÇĎḌĐƊÐÉÈĖÊËĚĔĒĘẸƎƏƐĠĜǦĞĢƔáàâäǎăāãåǻąæǽǣɓćċĉčçďḍđɗðéèėêëěĕēęẹǝəɛġĝǧğģɣĤḤĦIÍÌİÎÏǏĬĪĨĮỊIJĴĶƘĹĻŁĽĿʼNŃN̈ŇÑŅŊÓÒÔÖǑŎŌÕŐỌØǾƠŒĥḥħıíìiîïǐĭīĩįịijĵķƙĸĺļłľŀʼnńn̈ňñņŋóòôöǒŏōõőọøǿơœŔŘŖŚŜŠŞȘṢẞŤŢṬŦÞÚÙÛÜǓŬŪŨŰŮŲỤƯẂẀŴẄǷÝỲŶŸȲỸƳŹŻŽẒŕřŗſśŝšşșṣßťţṭŧþúùûüǔŭūũűůųụưẃẁŵẅƿýỳŷÿȳỹƴźżžẓ]/g
|
||
);
|
||
// If there are "normal" characters in the adjacent text, let the browser handle natively.
|
||
if ( adjacentTextAfterMatch !== null && adjacentTextAfterMatch.length ) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
ve.log('handleDelete programatically');
|
||
e.preventDefault();
|
||
this.surfaceObserver.stop();
|
||
|
||
if (
|
||
// Source and target are the same node
|
||
sourceNode === targetNode ||
|
||
(
|
||
// Source and target have the same parent (list items)
|
||
sourceSplitableNode !== undefined &&
|
||
sourceSplitableNode.getParent() === targetSplitableNode.getParent()
|
||
)
|
||
) {
|
||
// Simple removal
|
||
tx = ve.dm.Transaction.newFromRemoval(
|
||
this.documentView.model, new ve.Range( targetOffset, sourceOffset )
|
||
);
|
||
this.model.change( tx, new ve.Range( cursorAt ) );
|
||
} else if ( sourceNode.getType() === 'document' ) {
|
||
// Source is a slug - move the cursor somewhere useful
|
||
this.model.change( null, new ve.Range( cursorAt ) );
|
||
} else {
|
||
// Source and target are different nodes or do not share a parent, perform tricky merge
|
||
// Get the data for the source node
|
||
sourceData = this.documentView.model.getData( sourceNode.model.getRange() );
|
||
|
||
// Find the node that should be completely removed
|
||
nodeToDelete = sourceNode;
|
||
ve.Node.traverseUpstream( nodeToDelete, function ( node ) {
|
||
if ( node.getParent().children.length === 1 ) {
|
||
nodeToDelete = node.getParent();
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
} );
|
||
|
||
this.model.change(
|
||
[
|
||
// Remove source node or source node ancestor
|
||
ve.dm.Transaction.newFromRemoval(
|
||
this.documentView.model, nodeToDelete.getModel().getOuterRange()
|
||
),
|
||
// Append source data to target
|
||
ve.dm.Transaction.newFromInsertion(
|
||
this.documentView.model, targetOffset, sourceData
|
||
)
|
||
],
|
||
new ve.Range( cursorAt )
|
||
);
|
||
}
|
||
} else {
|
||
// Selection removal
|
||
ve.log('selection removal - handle programatically');
|
||
e.preventDefault();
|
||
this.model.change(
|
||
ve.dm.Transaction.newFromRemoval( this.documentView.model, selection ),
|
||
new ve.Range( selection.start )
|
||
);
|
||
}
|
||
|
||
this.surfaceObserver.clear();
|
||
this.surfaceObserver.start();
|
||
};
|
||
|
||
/**
|
||
* Shows the cursor at a given offset.
|
||
*
|
||
* @method
|
||
* @param {number} offset Offset to show cursor at
|
||
*/
|
||
ve.ce.Surface.prototype.showCursor = function ( offset ) {
|
||
this.showSelection( new ve.Range( offset ) );
|
||
};
|
||
|
||
/**
|
||
* Shows selection on a given range.
|
||
*
|
||
* @method
|
||
* @param {ve.Range} range Range to show selection on
|
||
*/
|
||
ve.ce.Surface.prototype.showSelection = function ( range ) {
|
||
var start, end,
|
||
rangySel = rangy.getSelection(),
|
||
rangyRange = rangy.createRange();
|
||
|
||
// Ensure the range we are asking to select is from and to correct offsets - failure to do so
|
||
// may cause getNodeAndOffset to throw an exception
|
||
range = new ve.Range(
|
||
this.getNearestCorrectOffset( range.start ),
|
||
this.getNearestCorrectOffset( range.end )
|
||
);
|
||
|
||
if ( range.start !== range.end ) {
|
||
start = this.getNodeAndOffset( range.start );
|
||
end = this.getNodeAndOffset( range.end );
|
||
|
||
if ( false && $.browser.msie ) {
|
||
if ( range.start === range.from ) {
|
||
if (
|
||
start.node === this.poll.rangySelection.anchorNode &&
|
||
start.offset === this.poll.rangySelection.anchorOffset &&
|
||
end.node === this.poll.rangySelection.focusNode &&
|
||
end.offset === this.poll.rangySelection.focusOffset
|
||
) {
|
||
return;
|
||
}
|
||
} else {
|
||
if (
|
||
end.node === this.poll.rangySelection.anchorNode &&
|
||
end.offset === this.poll.rangySelection.anchorOffset &&
|
||
start.node === this.poll.rangySelection.focusNode &&
|
||
start.offset === this.poll.rangySelection.focusOffset
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
rangyRange.setStart( start.node, start.offset );
|
||
rangyRange.setEnd( end.node, end.offset );
|
||
rangySel.removeAllRanges();
|
||
rangySel.addRange( rangyRange, range.start !== range.from );
|
||
} else {
|
||
start = end = this.getNodeAndOffset( range.start );
|
||
|
||
if ( false && $.browser.msie ) {
|
||
if (
|
||
start.node === this.poll.rangySelection.anchorNode &&
|
||
start.offset === this.poll.rangySelection.anchorOffset
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
rangyRange.setStart( start.node, start.offset );
|
||
rangySel.setSingleRange( rangyRange );
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Gets the nearest offset that a cursor can actually be placed at.
|
||
*
|
||
* TODO: Find a better name and a better place for this method
|
||
*
|
||
* @method
|
||
* @param {number} offset Offset to start looking at
|
||
* @param {number} [direction=-1] Direction to look in, +1 or -1
|
||
*/
|
||
ve.ce.Surface.prototype.getNearestCorrectOffset = function ( offset, direction ) {
|
||
var contentOffset, structuralOffset;
|
||
|
||
direction = direction > 0 ? 1 : -1;
|
||
if (
|
||
ve.dm.Document.isContentOffset( this.documentView.model.data, offset ) ||
|
||
this.hasSlugAtOffset( offset )
|
||
) {
|
||
return offset;
|
||
}
|
||
|
||
contentOffset = this.documentView.model.getNearestContentOffset( offset, direction );
|
||
structuralOffset = this.documentView.model.getNearestStructuralOffset( offset, direction, true );
|
||
|
||
if ( !this.hasSlugAtOffset( structuralOffset ) ) {
|
||
return contentOffset;
|
||
}
|
||
|
||
if ( direction === 1 ) {
|
||
if ( contentOffset < offset ) {
|
||
return structuralOffset;
|
||
} else {
|
||
return Math.min( contentOffset, structuralOffset );
|
||
}
|
||
} else {
|
||
if ( contentOffset > offset ) {
|
||
return structuralOffset;
|
||
} else {
|
||
return Math.max( contentOffset, structuralOffset );
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Checks if a given offset is inside a slug.
|
||
*
|
||
* TODO: Find a better name and a better place for this method - probably in a document view?
|
||
*
|
||
* @method
|
||
* @param {number} offset Offset to check for a slug at
|
||
* @returns {boolean} A slug exists at the given offset
|
||
*/
|
||
ve.ce.Surface.prototype.hasSlugAtOffset = function ( offset ) {
|
||
return !!this.documentView.getSlugAtOffset( offset );
|
||
};
|
||
|
||
/**
|
||
* Gets a DOM node and offset that can be used to place a cursor, based on a given offset.
|
||
*
|
||
* The results of this function are meant to be used with rangy.
|
||
*
|
||
* @method
|
||
* @param {number} offset Linear model offset
|
||
* @returns {Object} Object containing a node and offset property where node is an HTML element and
|
||
* offset is the position within the element
|
||
* @throws {Error} Offset could not be translated to a DOM element and offset
|
||
*/
|
||
ve.ce.Surface.prototype.getNodeAndOffset = function ( offset ) {
|
||
var node, startOffset, current, stack, item, $item, length,
|
||
slug = this.documentView.getSlugAtOffset( offset );
|
||
if ( slug ) {
|
||
return { node: slug[0].childNodes[0], offset: 0 };
|
||
}
|
||
node = this.documentView.getNodeFromOffset( offset );
|
||
startOffset = this.documentView.getDocumentNode().getOffsetFromNode( node ) +
|
||
( ( node.isWrapped() ) ? 1 : 0 );
|
||
current = [node.$.contents(), 0];
|
||
stack = [current];
|
||
|
||
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 new Error( 'Offset could not be translated to a DOM element and offset: ' + offset );
|
||
};
|
||
|
||
/**
|
||
* Gets the coordinates of the selection anchor.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.getSelectionRect = function () {
|
||
var rangySel = rangy.getSelection();
|
||
return {
|
||
start: rangySel.getStartDocumentPos(),
|
||
end: rangySel.getEndDocumentPos()
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Tests if the modifier key for keyboard shortcuts is pressed.
|
||
*
|
||
* @method
|
||
* @param {jQuery.Event} e
|
||
*/
|
||
ve.ce.Surface.isShortcutKey = function ( e ) {
|
||
if ( e.ctrlKey || e.metaKey ) {
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* Removes localStorage keys for copy and paste after a day.
|
||
*
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.clearLocalStorage = function () {
|
||
var i, len, key, time, now,
|
||
keysToRemove = [];
|
||
|
||
for ( i = 0, len = localStorage.length; i < len; i++ ) {
|
||
key = localStorage.key( i );
|
||
|
||
if ( key.indexOf( 've-' ) !== 0 ) {
|
||
return false;
|
||
}
|
||
|
||
time = JSON.parse( localStorage.getItem( key ) ).time;
|
||
now = new Date().getTime();
|
||
|
||
// Offset: 24 days (in miliseconds)
|
||
if ( now - time > ( 24 * 3600 * 1000 ) ) {
|
||
// Don't remove keys while iterating. Store them for later removal.
|
||
keysToRemove.push( key );
|
||
}
|
||
}
|
||
|
||
$.each( keysToRemove, function ( i, val ) {
|
||
localStorage.removeItem( val );
|
||
} );
|
||
};
|
||
|
||
/**
|
||
* Gets the surface model.
|
||
*
|
||
* @method
|
||
* @returns {ve.dm.Surface} Surface model
|
||
*/
|
||
ve.ce.Surface.prototype.getModel = function () {
|
||
return this.model;
|
||
};
|
||
|
||
/**
|
||
* Gets the document view.
|
||
*
|
||
* @method
|
||
* @returns {ve.ce.Document} Document view
|
||
*/
|
||
ve.ce.Surface.prototype.getDocument = function () {
|
||
return this.documentView;
|
||
};
|
||
|
||
/**
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.enableRendering = function () {
|
||
this.renderingEnabled = true;
|
||
};
|
||
|
||
/**
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.disableRendering = function () {
|
||
this.renderingEnabled = false;
|
||
};
|
||
|
||
/**
|
||
* @method
|
||
*/
|
||
ve.ce.Surface.prototype.isRenderingEnabled = function () {
|
||
return this.renderingEnabled;
|
||
};
|