Divec has uploaded a new change for review.
https://gerrit.wikimedia.org/r/125937
Change subject: POC DONTMERGE: Unicorn
......................................................................
POC DONTMERGE: Unicorn
When the cursor is pre-annotated, immediately insert correct inline tags, with
the cursor placed inside. Stop the browser collapsing them and/or moving the
cursor out of them, by inserting a temporary img tag: a "unicorn". Remove the
unicorn at a later refresh when it is no longer needed.
TODO:
* Make unicorn disappearance timelier
* Handle left-arrow
* Make invisible (for production)
* Tests
* Testing
ve.ce.ContentBranchNode.js
* getRenderedContents: include a unicorn where pre-annotation requires it
ve.ce.Surface.hs
* onInsertionAnnotationsChange: re-render selected content branch node
ve.ce.Document.js
* getNodeAndOffset: account for unicorns
ve.ce.js
* nextCursorOffset: return DOM cursor position after unicorn
* getOffsetFromElementNode: account for unicorns
* Minimal 1px GIF data URL (for production)
* Small visible GIF data URL (for debugging)
demos/ve/demo.css
* Give spans a visible border (for debugging)
ve.ce.test.js
* test ve.ce.nextCursorOffset
Change-Id: I99b99e8551bf550547cba623b643b6317f0706da
---
M demos/ve/demo.css
M demos/ve/pages/h1.html
M modules/ve/ce/ve.ce.ContentBranchNode.js
M modules/ve/ce/ve.ce.Document.js
M modules/ve/ce/ve.ce.Surface.js
M modules/ve/ce/ve.ce.SurfaceObserver.js
M modules/ve/ce/ve.ce.js
M modules/ve/test/ce/ve.ce.test.js
8 files changed, 196 insertions(+), 5 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor
refs/changes/37/125937/1
diff --git a/demos/ve/demo.css b/demos/ve/demo.css
index 5ea8fb1..a73a0e3 100644
--- a/demos/ve/demo.css
+++ b/demos/ve/demo.css
@@ -5,6 +5,10 @@
* @license The MIT License (MIT); see LICENSE.txt
*/
+.ve-ce-documentNode span, .ve-ce-documentNode i, .ve-ce-documentNode u,
.ve-ce-documentNode b {
+ border: solid red 1px;
+}
+
body {
margin: 0;
padding: 0;
diff --git a/demos/ve/pages/h1.html b/demos/ve/pages/h1.html
index e9386a9..96435e1 100644
--- a/demos/ve/pages/h1.html
+++ b/demos/ve/pages/h1.html
@@ -1 +1 @@
-<h1>abcdefg</h1>
+<p>abcdefg</p>
diff --git a/modules/ve/ce/ve.ce.ContentBranchNode.js
b/modules/ve/ce/ve.ce.ContentBranchNode.js
index 8d55edb..b7e427b 100644
--- a/modules/ve/ce/ve.ce.ContentBranchNode.js
+++ b/modules/ve/ce/ve.ce.ContentBranchNode.js
@@ -88,10 +88,12 @@
* @method
*/
ve.ce.ContentBranchNode.prototype.onChildUpdate = function ( transaction ) {
+ console.log( 'CBN onChildUpdate ', transaction );
if ( transaction === null || transaction === this.lastTransaction ) {
this.lastTransaction = transaction;
return;
}
+ console.log( 'CBN onChildUpdate 2' );
this.renderContents();
};
@@ -122,7 +124,9 @@
* @returns {HTMLElement} Wrapper containing rendered contents
*/
ve.ce.ContentBranchNode.prototype.getRenderedContents = function () {
- var i, ilen, j, jlen, item, itemAnnotations, ann, clone,
+ console.log( 'getRenderedContents' );
+ var i, ilen, j, jlen, item, itemAnnotations, ann, clone, dmSurface,
dmRange, relCursor,
+ unicorn, img, annotationsChanged,
store = this.model.doc.getStore(),
annotationStack = new ve.dm.AnnotationSet( store ),
annotatedHtml = [],
@@ -133,6 +137,7 @@
node = this;
function openAnnotation( annotation ) {
+ annotationsChanged = true;
if ( buffer !== '' ) {
current.appendChild( doc.createTextNode( buffer ) );
buffer = '';
@@ -146,6 +151,7 @@
}
function closeAnnotation() {
+ annotationsChanged = true;
if ( buffer !== '' ) {
current.appendChild( doc.createTextNode( buffer ) );
buffer = '';
@@ -159,6 +165,25 @@
annotatedHtml = annotatedHtml.concat(
this.children[i].getAnnotatedHtml() );
}
+ // If necessary, put the pre-annotation unicorn into the HTML
+ if ( this.getRoot() ) {
+ dmSurface = this.getRoot().getSurface().getModel();
+ dmRange = dmSurface.getSelection();
+ if ( dmRange && dmRange.start === dmRange.end ) {
+ // subtract 1 for CBN opening tag
+ relCursor = dmRange.start - this.getOffset() - 1;
+ } else {
+ // Not a collapsed selection; no need to worry about
pre-annotation
+ relCursor = - 1;
+ }
+ if ( 0 <= relCursor && relCursor <= annotatedHtml.length &&
annotatedHtml.length > 0 ) {
+ // Perpetual unicorn for now. TODO: decide when to do
this
+ unicorn = [{uni: 'corn'},
dmSurface.getInsertionAnnotations().storeIndexes];
+ annotatedHtml.splice( relCursor, 0, unicorn );
+ }
+ console.log( 'cursor:', dmRange.start, 'getOffset:',
this.getOffset(), 'relCursor:', relCursor, 'annotatedHtml:', annotatedHtml );
+ }
+
// Render HTML with annotations
for ( i = 0, ilen = annotatedHtml.length; i < ilen; i++ ) {
if ( ve.isArray( annotatedHtml[i] ) ) {
@@ -169,6 +194,7 @@
itemAnnotations = new ve.dm.AnnotationSet( store );
}
+ annotationsChanged = false;
ve.dm.Converter.openAndCloseAnnotations( annotationStack,
itemAnnotations,
openAnnotation, closeAnnotation
);
@@ -176,6 +202,19 @@
// Handle the actual item
if ( typeof item === 'string' ) {
buffer += item;
+ } else if ( unicorn && item === unicorn[0] ) {
+ if ( annotationsChanged ) {
+ if ( buffer !== '' ) {
+ current.appendChild(
doc.createTextNode( buffer ) );
+ buffer = '';
+ }
+ img = doc.createElement( 'img' );
+ img.className = 've-ce-unicorn';
+ //img.setAttribute( 'src', ve.ce.minImgDataUri
);
+ img.setAttribute( 'src',
ve.ce.unicornImgDataUri );
+ $( img ).data( 'dmOffset', ( this.getOffset() +
1 + i ) );
+ current.appendChild( img );
+ }
} else {
if ( buffer !== '' ) {
current.appendChild( doc.createTextNode( buffer
) );
diff --git a/modules/ve/ce/ve.ce.Document.js b/modules/ve/ce/ve.ce.Document.js
index a548b50..cb9e386 100644
--- a/modules/ve/ce/ve.ce.Document.js
+++ b/modules/ve/ce/ve.ce.Document.js
@@ -136,11 +136,43 @@
* @throws {Error} Offset could not be translated to a DOM element and offset
*/
ve.ce.Document.prototype.getNodeAndOffset = function ( offset ) {
+ var nao, n1, n2;
+ function getNext ( node ) {
+ while ( node.nextSibling === null ) {
+ node = node.parentNode;
+ if ( node === null ) {
+ return null;
+ }
+ }
+ node = node.nextSibling;
+ while ( node.firstChild ) {
+ node = node.firstChild;
+ }
+ return node;
+ }
+
+ nao = this.getNodeAndOffset2( offset );
+ n1 = nao.node;
+ n2 = getNext( n1 );
+
+ // If just before the unicorn, return the point just after it
+ if ( n1.nodeType === 3 &&
+ nao.offset === n1.data.length &&
+ n2 && n2.nodeType === 1 && n2.getAttribute( 'class' ) ===
've-ce-unicorn'
+ ) {
+ return ve.ce.nextCursorOffset( n2 );
+ } else {
+ return nao;
+ }
+};
+
+ve.ce.Document.prototype.getNodeAndOffset2 = function ( offset ) {
var node, startOffset, current, stack, item, $item, length,
slug = this.getSlugAtOffset( offset );
if ( slug ) {
return { node: slug, offset: 0 };
}
+
node = this.getNodeFromOffset( offset );
startOffset = node.getOffset() + ( ( node.isWrapped() ) ? 1 : 0 );
current = [node.$element.contents(), 0];
diff --git a/modules/ve/ce/ve.ce.Surface.js b/modules/ve/ce/ve.ce.Surface.js
index 13ef497..d0aa6ae 100644
--- a/modules/ve/ce/ve.ce.Surface.js
+++ b/modules/ve/ce/ve.ce.Surface.js
@@ -63,8 +63,13 @@
this.surfaceObserver.connect(
this, { 'contentChange': 'onContentChange', 'selectionChange':
'onSelectionChange' }
);
- this.model.connect( this,
- { 'select': 'onModelSelect', 'documentUpdate':
'onModelDocumentUpdate' }
+ this.model.connect(
+ this,
+ {
+ 'select': 'onModelSelect',
+ 'documentUpdate': 'onModelDocumentUpdate',
+ 'insertionAnnotationsChange':
'onInsertionAnnotationsChange'
+ }
);
$documentNode = this.documentView.getDocumentNode().$element;
@@ -1317,6 +1322,7 @@
// changeModelSelection is currently being called with the same
(object-identical)
// selection object (i.e. if the model is calling us back)
if ( !this.focusedNode && !this.isRenderingLocked() && selection !==
this.newModelSelection ) {
+ //if ( !this.focusedNode ) {
this.showSelection( selection );
}
@@ -1335,6 +1341,34 @@
}
// Update the state of the SurfaceObserver
this.surfaceObserver.pollOnceNoEmit();
+};
+
+ve.ce.Surface.prototype.onInsertionAnnotationsChange = function (
insertionAnnotations ) {
+ this.renderSelectedContentBranchNode();
+ //this.onModelSelect( this.surface.getModel().getSelection() );
+ this.showSelection( this.surface.getModel().getSelection() );
+ this.surfaceObserver.pollOnceNoEmit();
+};
+
+ve.ce.Surface.prototype.renderSelectedContentBranchNode = function () {
+ var dmRange, ceNode;
+ dmRange = this.model.getSelection();
+ if ( dmRange === null ) {
+ console.log( 'renderSelectedContentBranchNode: no selection' );
+ return;
+ }
+ ceNode = this.documentView.getNodeFromOffset( dmRange.start );
+ if ( ceNode === null ) {
+ console.log( 'renderSelectedContentBranchNode: no node at ' +
dmRange.start );
+ return;
+ }
+ if ( !ceNode instanceof ve.ce.ContentBranchNode ) {
+ console.log( 'renderSelectedContentBranchNode: not a content
branch node:', ceNode );
+ // not a content branch node
+ return;
+ }
+ console.log( 'renderSelectedContentBranchNode: Unicorn at ' +
dmRange.start );
+ ceNode.renderContents();
};
/**
@@ -1555,6 +1589,7 @@
* @method
*/
ve.ce.Surface.prototype.handleLeftOrRightArrowKey = function ( e ) {
+ return; // XXX Do it natively
var selection, range, direction;
// On Mac OS pressing Command (metaKey) + Left/Right is same as
pressing Home/End.
// As we are not able to handle it programmatically (because we don't
know at which offsets
@@ -1690,7 +1725,8 @@
if ( selection.isCollapsed() ) {
slug = this.documentView.getSlugAtOffset( selection.start );
// Always pawn in a slug
- if ( slug || this.needsPawn( selection, insertionAnnotations )
) {
+ //if ( slug || this.needsPawn( selection, insertionAnnotations
) ) {
+ if ( slug ) {
placeholder = '♙';
if ( !insertionAnnotations.isEmpty() ) {
placeholder = [placeholder,
insertionAnnotations.getIndexes()];
@@ -2259,6 +2295,7 @@
* @returns {boolean} Render is locked
*/
ve.ce.Surface.prototype.isRenderingLocked = function () {
+ return false; // XXX Always render for now
return this.renderLocks > 0;
};
@@ -2295,6 +2332,7 @@
if ( this.newModelSelection !== null ) {
throw new Error( 'Nested change of newModelSelection' );
}
+
this.newModelSelection = range;
try {
this.model.change( transaction, range );
diff --git a/modules/ve/ce/ve.ce.SurfaceObserver.js
b/modules/ve/ce/ve.ce.SurfaceObserver.js
index 92e7a1b..2eb56af 100644
--- a/modules/ve/ce/ve.ce.SurfaceObserver.js
+++ b/modules/ve/ce/ve.ce.SurfaceObserver.js
@@ -233,6 +233,10 @@
text = ve.ce.getDomText( node.$element[0] );
hash = ve.ce.getDomHash( node.$element[0] );
if ( this.text !== text || this.hash !== hash ) {
+ ve.log( 'oldHash:', this.hash );
+ ve.log( 'newHash:', hash );
+ ve.log( 'oldText:', this.text );
+ ve.log( 'newText:', text );
if ( emitChanges ) {
this.emit(
'contentChange',
diff --git a/modules/ve/ce/ve.ce.js b/modules/ve/ce/ve.ce.js
index 8a2a922..829406f 100644
--- a/modules/ve/ce/ve.ce.js
+++ b/modules/ve/ce/ve.ce.js
@@ -26,6 +26,14 @@
*/
ve.ce.whitespacePattern = /[\u0020\u00A0]/g;
+
+/**
+ * Data URI for minimal GIF image.
+ */
+ve.ce.minImgDataUri = 'data:image/gif;base64,R0lGODdhAQABAADcACwAAAAAAQABAAA';
+
+ve.ce.unicornImgDataUri =
'data:image/gif;base64,R0lGODlhDwATAOcAAAAAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4%2BPj8%2FP0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5%2Bfn9%2Ff4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo%2BPj5CQkJGRkZKSkpOTk5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp%2Bfn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq%2Bvr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6%2Bvr%2B%2Fv8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs%2FPz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t%2Ff3%2BDg4OHh4eLi4uPj4%2BTk5OXl5ebm5ufn5%2Bjo6Onp6erq6uvr6%2Bzs7O3t7e7u7u%2Fv7%2FDw8PHx8fLy8vPz8%2FT09PX19fb29vf39%2Fj4%2BPn5%2Bfr6%2Bvv7%2B%2Fz8%2FP39%2Ff7%2B%2Fv%2F%2F%2FywAAAAADwATAAAIogD%2FCRxIsKBBgtoo5NB3kOE%2FfzhAoCsoT6ARU%2F%2FyRGBmkI6Tf3HA%2BIpw6iCvBiQc5Lh3UCAQBjBNdPl28JAYTDAZZGETz%2BCQDhFyMjDjxyAOoTAf9FFGMB8HpDCz3CEI7UHOBi1geugzbmCgQkwY8NAjAaYDR6oGdjNCRYmoCkILzTHoqAFSM2sKKrOKtMkbegTlQGUwxE84gnEGdyACrWXLgAA7';
+
/* Static Methods */
/**
@@ -98,6 +106,8 @@
if ( nodeType === 3 || nodeType === 4 ) {
return '#';
+ } else if ( nodeType === 1 && element.getAttribute( 'class' ) ===
've-ce-unicorn' ) {
+ // do nothing
} else if ( nodeType === 1 || nodeType === 9 ) {
hash += '<' + nodeName + '>';
// Traverse its children
@@ -105,6 +115,8 @@
hash += ve.ce.getDomHash( element );
}
hash += '</' + nodeName + '>';
+ // Merge adjacent text node representations
+ hash = hash.replace( /##+/g, '#' );
}
return hash;
};
@@ -123,6 +135,27 @@
} else {
return ve.ce.getOffsetFromElementNode( domNode, domOffset );
}
+};
+
+
+/**
+ *
+ * @method
+ * @param {Node} node DOM node
+ * @returns {Object}
+ * @returns {Node} return.node
+ * @returns {number} return.offset
+ */
+ve.ce.nextCursorOffset = function ( node ) {
+ var nextNode, offset;
+ if ( node.nextSibling !== null && node.nextSibling.nodeType ===
Node.TEXT_NODE ) {
+ nextNode = node.nextSibling;
+ offset = 0;
+ } else {
+ nextNode = node.parentNode;
+ offset = 1 + Array.prototype.indexOf.call(
node.parentNode.childNodes, node );
+ }
+ return { node: nextNode, offset: offset };
};
/**
@@ -202,6 +235,12 @@
ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, direction ) {
var nodeModel, node,
$domNode = $( domNode );
+ if ( $domNode.hasClass( 've-ce-unicorn' ) ) {
+ if ( domOffset !== 0 ) {
+ throw new Error( 'Non-zero offset in unicorn' );
+ }
+ return $domNode.data( 'dmOffset' );
+ }
if ( $domNode.hasClass( 've-ce-branchNode-slug' ) ) {
if ( $domNode.prev().length ) {
diff --git a/modules/ve/test/ce/ve.ce.test.js b/modules/ve/test/ce/ve.ce.test.js
index 59ff6c1..01cdaf8 100644
--- a/modules/ve/test/ce/ve.ce.test.js
+++ b/modules/ve/test/ce/ve.ce.test.js
@@ -162,6 +162,41 @@
assert.equal( ve.ce.isShortcutKey( {} ), false, 'Not set' );
} );
+QUnit.test( 'nextCursorOffset', function ( assert ) {
+ var i, len, tests, elt, test, img, nextOffset;
+
+ function dumpnode( node ) {
+ if ( node.nodeType === 3 ) {
+ return '#' + node.data;
+ } else {
+ return node.nodeName.toLowerCase();
+ }
+ }
+
+ tests = [
+ { html: '<p>foo<img>bar</p>', expected: ['#bar', 0] },
+ { html: '<p>foo<b><i><img></i></b></p>', expected: ['i', 1] },
+ { html: '<p><b>foo</b><img>bar</p>', expected: ['#bar', 0] },
+ { html: '<p>foo<b><i><img></i></b></p>', expected: ['i', 1] },
+ { html: '<p><b>foo</b><img></p>', expected: ['p', 2] },
+ { html: '<p><img><b>foo</b></p>', expected: ['p', 1] },
+ { html: '<p><b>foo</b><img><b>bar</b></p>', expected: ['p', 2] }
+ ];
+ QUnit.expect( assert );
+ elt = ve.createDocumentFromHtml( '' ).createElement( 'div' );
+ for ( i = 0, len = tests.length; i < len; i++ ) {
+ test = tests[i];
+ elt.innerHTML = test.html;
+ img = elt.getElementsByTagName( 'img' )[0];
+ nextOffset = ve.ce.nextCursorOffset( img );
+ assert.deepEqual(
+ [dumpnode( nextOffset.node ), nextOffset.offset],
+ test.expected,
+ test.html
+ );
+ }
+} );
+
QUnit.test( 'resolveTestOffset', function ( assert ) {
var i, ilen, j, jlen, tests, test, testOffset, elt, pre, post, count,
dom;
tests = [
--
To view, visit https://gerrit.wikimedia.org/r/125937
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I99b99e8551bf550547cba623b643b6317f0706da
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits