http://www.mediawiki.org/wiki/Special:Code/MediaWiki/89832

Revision: 89832
Author:   tparscal
Date:     2011-06-10 16:44:47 +0000 (Fri, 10 Jun 2011)
Log Message:
-----------
* Rewrote jquery.flow-a - Now uses character and word measurement caching!
* Modified selection code to select word boundaries first, then characters, 
greatly improving accuracy
* Removed jquery.flow-b
* Renamed flow-a to jquery.flow

Modified Paths:
--------------
    trunk/parsers/wikidom/demos/surface/index.html
    trunk/parsers/wikidom/lib/jquery.editSurface.js

Added Paths:
-----------
    trunk/parsers/wikidom/lib/jquery.flow.js

Removed Paths:
-------------
    trunk/parsers/wikidom/lib/jquery.flow-a.js
    trunk/parsers/wikidom/lib/jquery.flow-b.js

Modified: trunk/parsers/wikidom/demos/surface/index.html
===================================================================
--- trunk/parsers/wikidom/demos/surface/index.html      2011-06-10 16:40:29 UTC 
(rev 89831)
+++ trunk/parsers/wikidom/demos/surface/index.html      2011-06-10 16:44:47 UTC 
(rev 89832)
@@ -13,7 +13,7 @@
                <!-- EditSurface -->
                <script type="text/javascript" 
src="../../lib/jquery.js"></script>
                <script type="text/javascript" 
src="../../lib/jquery.closestToOffset.js"></script>
-               <script type="text/javascript" 
src="../../lib/jquery.flow-a.js"></script>
+               <script type="text/javascript" 
src="../../lib/jquery.flow.js"></script>
                <script type="text/javascript" 
src="../../lib/jquery.editSurface.js"></script>
                
                <!-- Demo -->
@@ -28,7 +28,7 @@
                                        "Word wrap following hyphens is 
sometimes not desired, and can be avoided by using a so-called non-breaking 
hyphen instead of a regular hyphen. On the other hand, when using word 
processors, invisible hyphens, called soft hyphens, can also be inserted inside 
words so that word wrap can occur following the soft hyphens.",
                                        "Sometimes, word wrap is not desirable 
between words. In such cases, word wrap can usually be avoided by using a hard 
space or non-breaking space between the words, instead of regular spaces.",
                                        
"OccasionallyThereAreWordsThatAreSoLongTheyExceedTheWidthOfTheLineAndEndUpWrappingBetweenMultipleLines.",
-                               ].join( '\n\n' );
+                               ].join( ' ' );
                                $( '#es' ).editSurface( {
                                        'document': { 'blocks': [ {
                                                'type': 'paragraph',

Modified: trunk/parsers/wikidom/lib/jquery.editSurface.js
===================================================================
--- trunk/parsers/wikidom/lib/jquery.editSurface.js     2011-06-10 16:40:29 UTC 
(rev 89831)
+++ trunk/parsers/wikidom/lib/jquery.editSurface.js     2011-06-10 16:44:47 UTC 
(rev 89832)
@@ -49,7 +49,7 @@
                .mouseup( function( e ) {
                        if ( sel.active ) {
                                if ( !sel.from || !sel.to
-                                               || ( sel.from.line === 
sel.to.line && sel.from.index === sel.to.index ) ) {
+                                               || ( sel.from.line === 
sel.to.line && sel.from.char === sel.to.char ) ) {
                                        sel.from = null;
                                        sel.to = null;
                                        sel.start = null;
@@ -71,9 +71,12 @@
                                        );
                                }
                                sel.end = getCursorPosition( e.pageX, e.pageY, 
$target );
+                               //console.log( [sel.start.char, sel.end.char] );
+                               //console.log( [sel.start.word, sel.end.word] );
+                               //console.log( [sel.start.line, sel.end.line] );
                                if ( sel.start.line < sel.end.line
                                                || ( sel.start.line === 
sel.end.line
-                                                               && 
sel.start.index < sel.end.index ) ) {
+                                                               && 
sel.start.char < sel.end.char ) ) {
                                        sel.from = sel.start;
                                        sel.to = sel.end;
                                } else {
@@ -85,7 +88,6 @@
                        }
                } );
        
-       
        // Shortcuts
        var $document = $this.find( '.editSurface-document' );
        var ranges = {
@@ -100,47 +102,56 @@
                var text;
                if ( sel.from && sel.to ) {
                        if ( sel.from.line === sel.to.line ) {
-                               text = sel.from.$target.data( 'text' ).substr(
-                                       sel.from.index, sel.to.index - 
sel.from.index
+                               text = sel.from.$target.data( 'flow' 
).text.substr(
+                                       sel.from.char, sel.to.char - 
sel.from.char
                                );
                        } else {
-                               text = sel.from.$target.data( 'text' ).substr( 
sel.from.index );
+                               text = sel.from.$target.data( 'flow' 
).text.substr( sel.from.char );
                                var $sibling = sel.from.$target.next();
                                for ( var i = sel.from.line + 1; i < 
sel.to.line; i++ ) {
-                                       text += $sibling.data( 'text' )
+                                       text += $sibling.data( 'flow' ).text
                                        $sibling = $sibling.next();
                                }
-                               text += sel.to.$target.data( 'text' ).substr( 
0, sel.to.index );
+                               text += sel.to.$target.data( 'flow' 
).text.substr( 0, sel.to.char );
                        }
                }
                return text;
        }
        function getCursorPosition( x, y, $target ) {
-               var metrics = $target.data( 'metrics' );
-               var text = $target.data( 'text' );
-               var line = $target.data( 'line' );
-               if ( !$.isArray( metrics ) || metrics.length === 0 ) {
-                       throw "Missing metrics data error"
-               }
-               var to = metrics.length - 1;
-               var a;
-               var b = { 'l': 0, 'c': 0, 'r': 0 };
-               var c = x - $target.offset().left;
-               for ( var i = 0; i <= to; i++ ) {
-                       a = b;
-                       b = { 'l': a.r, 'c': a.r + ( metrics[i] / 2 ), 'r': a.r 
+ metrics[i] };
-                       if ( ( i === 0 && c <= a.l ) || ( c >= a.c && c <= b.c 
) || i === to ) {
-                               var offset = $target.offset();
-                               var height = $target.height();
-                               return {
-                                               '$target': $target,
-                                               'index': i,
-                                               'line': line,
-                                               'x': offset.left + b.l,
-                                               'top': offset.top,
-                                               'bottom': offset.top + height,
-                                               'height': height
-                               };
+               var line = $target.data( 'flow' ),
+                       offset = $target.offset(),
+                       height = $target.height(),
+                       l,
+                       r = 0,
+                       cur = x - offset.left;
+               for ( var w = 0, eol = line.metrics.length; w <= eol; w++ ) {
+                       var wi = Math.min( w, eol - 1 );
+                       l = r;
+                       r += line.metrics[wi];
+                       if ( ( w === 0 && cur <= l ) || ( cur >= l && cur <= r 
) || ( w === eol ) ) {
+                               var word = line.words[wi],
+                                       a,
+                                       b = { 'l': l, 'c': l, 'r': l };
+                               for ( var c = 0, eow = word.metrics.length; c 
<= eow; c++ ) {
+                                       a = b;
+                                       b = {
+                                               'l': a.r,
+                                               'c': a.r + ( word.metrics[c] / 
2 ),
+                                               'r': a.r + word.metrics[c]
+                                       };
+                                       if ( ( c === 0 && cur <= a.l ) || ( cur 
>= a.c && cur <= b.c ) || c === eow ) {
+                                               return {
+                                                       '$target': $target,
+                                                       'char': word.offset + 
Math.min( c, word.text.length - 1 ),
+                                                       'word': word.index,
+                                                       'line': line.index,
+                                                       'x': offset.left + ( c 
< eow ? b.l : a.l ),
+                                                       'top': offset.top,
+                                                       'bottom': offset.top + 
height,
+                                                       'height': height
+                                               };
+                                       }
+                               }
                        }
                }
        }
@@ -153,7 +164,7 @@
                if ( sel.from && sel.to ) {
                        if ( sel.from.line === sel.to.line ) {
                                // 1 line
-                               if ( sel.from.index !== sel.to.index ) {
+                               if ( sel.from.char !== sel.to.char ) {
                                        ranges.$first.show().css( {
                                                'left': sel.from.x,
                                                'top': sel.from.top,

Deleted: trunk/parsers/wikidom/lib/jquery.flow-a.js
===================================================================
--- trunk/parsers/wikidom/lib/jquery.flow-a.js  2011-06-10 16:40:29 UTC (rev 
89831)
+++ trunk/parsers/wikidom/lib/jquery.flow-a.js  2011-06-10 16:44:47 UTC (rev 
89832)
@@ -1,97 +0,0 @@
-/*
- * Flow jQuery plugin
- */
-
-$.flow = { 'cache': { 'chars': {}, 'words': {} } };
-
-$.fn.flow = function( text ) {
-       console.time( 'flow' );
-       
-       var $this = $(this);
-       var lineLimit = $this.innerWidth();
-       
-       // Wordify
-       var words = [],
-               word = { 'text': '', 'width': 0, 'metrics': [] };
-       for ( var i = 0; i < text.length; i++ ) {
-               var char = text[i];
-               // Boundary detection
-               var boundary = String( ' -\t\r\n\f' ).indexOf( char ) >= 0;
-               // Encoding
-               var charHtml = char
-                       .replace( '&', '&amp;' )
-                       .replace( ' ', '&nbsp;' )
-                       .replace( '<', '&lt;' )
-                       .replace( '>', '&gt;' )
-                       .replace( '\'', '&apos;' )
-                       .replace( '"', '&quot;' );
-        // Measurement
-               var charWidth;
-               if ( typeof $.flow.cache.chars[char] === 'undefined' ) {
-                       charWidth = $.flow.cache.chars[char] =
-                               $( '<div class="editSurface-line">' + charHtml 
+ '</div>' )
-                                       .appendTo( $this ).width();
-               } else {
-                       charWidth = $.flow.cache.chars[char];
-               }
-               // Virtual boundary
-               if ( word.width + charWidth >= lineLimit ) {
-                       words[words.length] = word;
-                       word = { 'text': '', 'width': 0, 'metrics': [] };
-               }
-               // Append
-               if ( boundary ) {
-                       if ( word.text.length ) {
-                               words[words.length] = word;
-                               word = { 'text': '', 'width': 0, 'metrics': [] 
};
-                       }
-                       words[words.length] = { 'text': char, 'width': 
charWidth, 'metrics': [charWidth] };
-               } else {
-                       word.text += char;
-                       word.width += charWidth;
-                       word.metrics[word.metrics.length] = charWidth;
-               }
-       }
-       if ( word.text.length ) {
-               words[words.length] = word;
-       }
-       
-       // Lineify
-       var lines = [],
-               line = { 'text': '', 'width': 0, 'metrics': [] };
-       for ( var i = 0; i < words.length; i++ ) {
-               var hardReturn = String( '\r\n\f' ).indexOf( words[i].text ) >= 
0;
-               if ( line.width + words[i].width > lineLimit || hardReturn ) {
-                       lines[lines.length] = line;
-                       line = { 'text': '', 'width': 0, 'metrics': [] };
-               }
-               if ( !hardReturn && ( line.width > 0 || words[i].text !== ' ' ) 
) {
-                       line.text += words[i].text;
-                       line.width += words[i].width;
-                       line.metrics = line.metrics.concat( words[i].metrics );
-               }
-       }
-       if ( line.text.length ) {
-               lines[lines.length] = line;
-       }
-       
-       // Flow
-       $this.empty();
-       for ( var i = 0; i < lines.length; i++ ) {
-               var $line = $( '<div class="editSurface-line"></div>' )
-                       .data( 'metrics', lines[i].metrics )
-                       .data( 'text', lines[i].text )
-                       .data( 'line', i );
-               if ( lines[i].text.length ) {
-                       $line.text( lines[i].text );
-               } else {
-                       $line.html( '&nbsp;' );
-                       $line.addClass( 'empty' );
-               }
-               $this.append( $line );
-       }
-
-       console.timeEnd( 'flow' );
-       
-       return $this;
-};
\ No newline at end of file

Deleted: trunk/parsers/wikidom/lib/jquery.flow-b.js
===================================================================
--- trunk/parsers/wikidom/lib/jquery.flow-b.js  2011-06-10 16:40:29 UTC (rev 
89831)
+++ trunk/parsers/wikidom/lib/jquery.flow-b.js  2011-06-10 16:44:47 UTC (rev 
89832)
@@ -1,94 +0,0 @@
-/*
- * Flow jQuery plugin
- */
-
-$.flow = { 'widthCache': {} };
-
-$.fn.flow = function( text ) {
-       console.time( 'flow' );
-       
-       function encodeHtml( c ) {
-               return c.replace( /\&/g, '&amp;' )
-                       .replace( /</g, '&lt;' )
-                       .replace( />/g, '&gt;' );
-       }
-       
-       var breakableRe = /[\s\r\n\f]/;
-       
-       var $this = $(this);
-
-       $this.empty();
-       
-       var width = $this.innerWidth();
-       var pos = 0;
-       var line = 0;
-       
-       while( pos < text.length ) {
-               var lineStartPos = pos;
-               var breakPos = pos;
-               
-               var $line = $( '<div class="editSurface-line"></div>' 
).appendTo( $this );
-               var lineElem = $line.get(0);
-               var lineText = '';
-               var lineMetrics = [];
-               var lineWidth = 0;
-               var lastLineWidth = 0;
-               
-               while ( pos < text.length && lineWidth < width ) {
-                       // Append text
-                       var c = text.charAt( pos );
-                       lineText += c;
-                       lineElem.innerHTML = encodeHtml( lineText );
-               
-                       // Get new line width from DOM
-                       lastLineWidth = lineWidth;
-                       // $.innerWidth call is expensive. Assume padding and 
border = 0 and this should be okay
-                       lineWidth = lineElem.offsetWidth; 
-                       // Push difference (character width)
-                       lineMetrics.push( lineWidth - lastLineWidth );
-
-                       if ( breakableRe( c ) ) {
-                               breakPos = pos;
-                       }
-
-                       pos++;
-               }
-       
-               if ( lineWidth >= width ) {
-                       if ( breakPos === lineStartPos ) {
-                               // There was no breakable position between the 
start of the line and here, so we
-                               // have some kind of long word. Or, the line 
width is very small. Break at the
-                               // previous character.
-                               pos -= 1;
-                               breakPos = pos;
-                       } else {
-                               // Include the breaking character in the 
previous line
-                               // TODO: How does this work with hyphens? Won't 
they be to far right?
-                               breakPos++;
-                       }
-                       // Move the position back to the last safe location
-                       pos = breakPos;
-                       // Truncate characters that won't fit
-                       lineText = text.substring( lineStartPos, breakPos );
-                       lineElem.innerHTML = encodeHtml( lineText );
-                       // Don't leave metrics from truncated characters around
-                       lineMetrics = lineMetrics.slice( 0, pos - lineStartPos 
);
-               }
-
-               $line
-                       .data( 'metrics', lineMetrics )
-                       .data( 'text', lineText )
-                       .data( 'line', line );
-               
-               if ( lineStartPos === pos ) {
-                       lineElem.innerHtml = '&nbsp;';
-               }
-               
-               line++;
-       }
-       
-       console.timeEnd( 'flow' );
-       
-       return $this;
-};
-

Added: trunk/parsers/wikidom/lib/jquery.flow.js
===================================================================
--- trunk/parsers/wikidom/lib/jquery.flow.js                            (rev 0)
+++ trunk/parsers/wikidom/lib/jquery.flow.js    2011-06-10 16:44:47 UTC (rev 
89832)
@@ -0,0 +1,185 @@
+/*
+ * Flow jQuery plugin
+ * 
+ * Each line has data in the following structure, embedded as $line.data( 
'flow' )
+ * {
+ *     index: 0,
+ *     text: 'abc 123',
+ *     metrics: [9,4,9]
+ *     width: 22,
+ *     words: [
+ *         {
+ *             text: 'abc',
+ *             html: 'abc',
+ *             metrics: [3,3,3],
+ *             width: 9,
+ *             index: 0,
+ *             offset: 0
+ *         },
+ *         {
+ *             text: ' ',
+ *             html: '&nbsp;',
+ *             metrics: [4],
+ *             width: 4,
+ *             index: 1,
+ *             offset: 3
+ *         },
+ *         {
+ *             text: '123',
+ *             html: '123',
+ *             metrics: [3,3,3]
+ *             width: 9,
+ *             index: 2,
+ *             offset: 4
+ *         }
+ *     ]
+ * }
+ */
+
+function copy( from, to ) {
+       if ( to === undefined ) {
+               to = {};
+       }
+       if ( from == null || typeof from != 'object' ) {
+               return from;
+       }
+       if ( from.constructor != Object && from.constructor != Array ) {
+               return from;
+       }
+       if ( from.constructor == Date
+                       || from.constructor == RegExp
+                       || from.constructor == Function
+                       || from.constructor == String
+                       || from.constructor == Number
+                       || from.constructor == Boolean ) {
+               return new from.constructor( from );
+       }
+       to = to || new from.constructor();
+       for ( var name in from ) {
+               to[name] = typeof to[name] == 'undefined' ? copy( from[name], 
null ) : to[name];
+       }
+       return to;
+}
+
+$.flow = {
+       'charCache': {},
+       'wordCache': {},
+       'measureWord': function( text, ruler ) {
+               if ( $.flow.wordCache[text] === undefined ) {
+                       // Cache miss
+                       var word = { 'text': text, 'html': '', 'metrics': [] };
+                       for ( var i = 0; i < text.length; i++ ) {
+                               var char = text[i],
+                                       charHtml = char
+                                               .replace( '&', '&amp;' )
+                                               .replace( ' ', '&nbsp;' )
+                                               .replace( '<', '&lt;' )
+                                               .replace( '>', '&gt;' )
+                                               .replace( '\'', '&apos;' )
+                                               .replace( '"', '&quot;' );
+                               word.html += charHtml;
+                               if ( $.flow.charCache[char] === undefined ) {
+                                       // Cache miss
+                                       ruler.innerHTML = charHtml;
+                                       word.metrics.push( 
$.flow.charCache[char] = ruler.clientWidth );
+                                       continue;
+                               }
+                               // Cache hit
+                               word.metrics.push( $.flow.charCache[char] );
+                       }
+                       ruler.innerHTML = word.html;
+                       word.width = ruler.clientWidth;
+                       $.flow.wordCache[text] = copy( word );
+                       return word;
+               }
+               // Cache hit
+               return copy( $.flow.wordCache[text] );
+       },
+       'getWords': function( text, ruler ) {
+               var words = [],
+                       bounadry = /[ \-\t\r\n\f]/,
+                       left = 0,
+                       right = 0,
+                       search = 0;
+               while ( ( search = text.substr( right ).search( bounadry ) ) >= 
0 ) {
+                       right += search;
+                       words.push( $.flow.measureWord( text.substring( left, 
right ), ruler ) );
+                       if ( right < text.length ) {
+                               words.push( $.flow.measureWord( text.substring( 
right, ++right ), ruler ) );
+                       }
+                       left = right;
+               }
+               words.push( $.flow.measureWord( text.substring( right, 
text.length ), ruler ) );
+               return words;
+       },
+       'getLines': function( words, width ) {
+               // Lineify
+               var lineCount = 0,
+                       charCount = 0,
+                       wordCount = 0,
+                       lines = [],
+                       line = {
+                               'text': '',
+                               'html': '',
+                               'width': 0,
+                               'metrics': [],
+                               'words': [],
+                               'index': lineCount
+                       };
+               for ( var i = 0; i < words.length; i++ ) {
+                       if ( line.width + words[i].width > width ) {
+                               lines.push( line );
+                               charCount = 0;
+                               wordCount = 0;
+                               lineCount++;
+                               line = {
+                                       'text': '',
+                                       'html': '',
+                                       'width': 0,
+                                       'metrics': [],
+                                       'words': [],
+                                       'index': lineCount
+                               };
+                       }
+                       words[i].index = wordCount;
+                       wordCount++;
+                       words[i].offset = charCount;
+                       charCount += words[i].text.length;
+                       line.words.push( words[i] );
+                       line.text += words[i].text;
+                       line.html += words[i].html;
+                       line.width += words[i].width;
+                       line.metrics.push( words[i].width );
+               }
+               if ( line.text.length ) {
+                       lines.push( line );
+               }
+               return lines;
+       }
+};
+
+$.fn.flow = function( text ) {
+       console.time( 'flow' );
+       
+       var $this = $(this),
+               lines = $.flow.getLines(
+                       $.flow.getWords( text, $( '<div 
class="editSurface-line"></div>' ).appendTo( $this )[0] ),
+                       $this.innerWidth()
+               );
+       
+       // Flow
+       $this.empty();
+       for ( var i = 0; i < lines.length; i++ ) {
+               var $line = $( '<div class="editSurface-line"></div>' ).data( 
'flow', lines[i] );
+               if ( lines[i].text.length === 1 && lines[1].text.match( /[ 
\-\t\r\n\f]/ ) ) {
+                       $line.html( '&nbsp;' );
+                       $line.addClass( 'empty' );
+               } else {
+                       $line.html( lines[i].html );
+               }
+               $this.append( $line );
+       }
+       
+       console.timeEnd( 'flow' );
+       return $this;
+};


Property changes on: trunk/parsers/wikidom/lib/jquery.flow.js
___________________________________________________________________
Added: svn:mime-type
   + text/plain
Added: svn:eol-style
   + native


_______________________________________________
MediaWiki-CVS mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs

Reply via email to