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

Reply via email to