r89832 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r89831‎ | r89832 | r89833 >
Date:16:44, 10 June 2011
Author:tparscal
Status:deferred
Tags:
Comment:
* 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 (modified) (history)
  • /trunk/parsers/wikidom/lib/jquery.editSurface.js (modified) (history)
  • /trunk/parsers/wikidom/lib/jquery.flow-a.js (deleted) (history)
  • /trunk/parsers/wikidom/lib/jquery.flow-b.js (deleted) (history)
  • /trunk/parsers/wikidom/lib/jquery.flow.js (added) (history)

Diff [purge]

Index: trunk/parsers/wikidom/lib/jquery.flow-b.js
@@ -1,94 +0,0 @@
2 -/*
3 - * Flow jQuery plugin
4 - */
5 -
6 -$.flow = { 'widthCache': {} };
7 -
8 -$.fn.flow = function( text ) {
9 - console.time( 'flow' );
10 -
11 - function encodeHtml( c ) {
12 - return c.replace( /\&/g, '&' )
13 - .replace( /</g, '&lt;' )
14 - .replace( />/g, '&gt;' );
15 - }
16 -
17 - var breakableRe = /[\s\r\n\f]/;
18 -
19 - var $this = $(this);
20 -
21 - $this.empty();
22 -
23 - var width = $this.innerWidth();
24 - var pos = 0;
25 - var line = 0;
26 -
27 - while( pos < text.length ) {
28 - var lineStartPos = pos;
29 - var breakPos = pos;
30 -
31 - var $line = $( '<div class="editSurface-line"></div>' ).appendTo( $this );
32 - var lineElem = $line.get(0);
33 - var lineText = '';
34 - var lineMetrics = [];
35 - var lineWidth = 0;
36 - var lastLineWidth = 0;
37 -
38 - while ( pos < text.length && lineWidth < width ) {
39 - // Append text
40 - var c = text.charAt( pos );
41 - lineText += c;
42 - lineElem.innerHTML = encodeHtml( lineText );
43 -
44 - // Get new line width from DOM
45 - lastLineWidth = lineWidth;
46 - // $.innerWidth call is expensive. Assume padding and border = 0 and this should be okay
47 - lineWidth = lineElem.offsetWidth;
48 - // Push difference (character width)
49 - lineMetrics.push( lineWidth - lastLineWidth );
50 -
51 - if ( breakableRe( c ) ) {
52 - breakPos = pos;
53 - }
54 -
55 - pos++;
56 - }
57 -
58 - if ( lineWidth >= width ) {
59 - if ( breakPos === lineStartPos ) {
60 - // There was no breakable position between the start of the line and here, so we
61 - // have some kind of long word. Or, the line width is very small. Break at the
62 - // previous character.
63 - pos -= 1;
64 - breakPos = pos;
65 - } else {
66 - // Include the breaking character in the previous line
67 - // TODO: How does this work with hyphens? Won't they be to far right?
68 - breakPos++;
69 - }
70 - // Move the position back to the last safe location
71 - pos = breakPos;
72 - // Truncate characters that won't fit
73 - lineText = text.substring( lineStartPos, breakPos );
74 - lineElem.innerHTML = encodeHtml( lineText );
75 - // Don't leave metrics from truncated characters around
76 - lineMetrics = lineMetrics.slice( 0, pos - lineStartPos );
77 - }
78 -
79 - $line
80 - .data( 'metrics', lineMetrics )
81 - .data( 'text', lineText )
82 - .data( 'line', line );
83 -
84 - if ( lineStartPos === pos ) {
85 - lineElem.innerHtml = '&nbsp;';
86 - }
87 -
88 - line++;
89 - }
90 -
91 - console.timeEnd( 'flow' );
92 -
93 - return $this;
94 -};
95 -
Index: trunk/parsers/wikidom/lib/jquery.flow-a.js
@@ -1,97 +0,0 @@
2 -/*
3 - * Flow jQuery plugin
4 - */
5 -
6 -$.flow = { 'cache': { 'chars': {}, 'words': {} } };
7 -
8 -$.fn.flow = function( text ) {
9 - console.time( 'flow' );
10 -
11 - var $this = $(this);
12 - var lineLimit = $this.innerWidth();
13 -
14 - // Wordify
15 - var words = [],
16 - word = { 'text': '', 'width': 0, 'metrics': [] };
17 - for ( var i = 0; i < text.length; i++ ) {
18 - var char = text[i];
19 - // Boundary detection
20 - var boundary = String( ' -\t\r\n\f' ).indexOf( char ) >= 0;
21 - // Encoding
22 - var charHtml = char
23 - .replace( '&', '&amp;' )
24 - .replace( ' ', '&nbsp;' )
25 - .replace( '<', '&lt;' )
26 - .replace( '>', '&gt;' )
27 - .replace( '\'', '&apos;' )
28 - .replace( '"', '&quot;' );
29 - // Measurement
30 - var charWidth;
31 - if ( typeof $.flow.cache.chars[char] === 'undefined' ) {
32 - charWidth = $.flow.cache.chars[char] =
33 - $( '<div class="editSurface-line">' + charHtml + '</div>' )
34 - .appendTo( $this ).width();
35 - } else {
36 - charWidth = $.flow.cache.chars[char];
37 - }
38 - // Virtual boundary
39 - if ( word.width + charWidth >= lineLimit ) {
40 - words[words.length] = word;
41 - word = { 'text': '', 'width': 0, 'metrics': [] };
42 - }
43 - // Append
44 - if ( boundary ) {
45 - if ( word.text.length ) {
46 - words[words.length] = word;
47 - word = { 'text': '', 'width': 0, 'metrics': [] };
48 - }
49 - words[words.length] = { 'text': char, 'width': charWidth, 'metrics': [charWidth] };
50 - } else {
51 - word.text += char;
52 - word.width += charWidth;
53 - word.metrics[word.metrics.length] = charWidth;
54 - }
55 - }
56 - if ( word.text.length ) {
57 - words[words.length] = word;
58 - }
59 -
60 - // Lineify
61 - var lines = [],
62 - line = { 'text': '', 'width': 0, 'metrics': [] };
63 - for ( var i = 0; i < words.length; i++ ) {
64 - var hardReturn = String( '\r\n\f' ).indexOf( words[i].text ) >= 0;
65 - if ( line.width + words[i].width > lineLimit || hardReturn ) {
66 - lines[lines.length] = line;
67 - line = { 'text': '', 'width': 0, 'metrics': [] };
68 - }
69 - if ( !hardReturn && ( line.width > 0 || words[i].text !== ' ' ) ) {
70 - line.text += words[i].text;
71 - line.width += words[i].width;
72 - line.metrics = line.metrics.concat( words[i].metrics );
73 - }
74 - }
75 - if ( line.text.length ) {
76 - lines[lines.length] = line;
77 - }
78 -
79 - // Flow
80 - $this.empty();
81 - for ( var i = 0; i < lines.length; i++ ) {
82 - var $line = $( '<div class="editSurface-line"></div>' )
83 - .data( 'metrics', lines[i].metrics )
84 - .data( 'text', lines[i].text )
85 - .data( 'line', i );
86 - if ( lines[i].text.length ) {
87 - $line.text( lines[i].text );
88 - } else {
89 - $line.html( '&nbsp;' );
90 - $line.addClass( 'empty' );
91 - }
92 - $this.append( $line );
93 - }
94 -
95 - console.timeEnd( 'flow' );
96 -
97 - return $this;
98 -};
\ No newline at end of file
Index: trunk/parsers/wikidom/lib/jquery.flow.js
@@ -0,0 +1,185 @@
 2+/*
 3+ * Flow jQuery plugin
 4+ *
 5+ * Each line has data in the following structure, embedded as $line.data( 'flow' )
 6+ * {
 7+ * index: 0,
 8+ * text: 'abc 123',
 9+ * metrics: [9,4,9]
 10+ * width: 22,
 11+ * words: [
 12+ * {
 13+ * text: 'abc',
 14+ * html: 'abc',
 15+ * metrics: [3,3,3],
 16+ * width: 9,
 17+ * index: 0,
 18+ * offset: 0
 19+ * },
 20+ * {
 21+ * text: ' ',
 22+ * html: '&nbsp;',
 23+ * metrics: [4],
 24+ * width: 4,
 25+ * index: 1,
 26+ * offset: 3
 27+ * },
 28+ * {
 29+ * text: '123',
 30+ * html: '123',
 31+ * metrics: [3,3,3]
 32+ * width: 9,
 33+ * index: 2,
 34+ * offset: 4
 35+ * }
 36+ * ]
 37+ * }
 38+ */
 39+
 40+function copy( from, to ) {
 41+ if ( to === undefined ) {
 42+ to = {};
 43+ }
 44+ if ( from == null || typeof from != 'object' ) {
 45+ return from;
 46+ }
 47+ if ( from.constructor != Object && from.constructor != Array ) {
 48+ return from;
 49+ }
 50+ if ( from.constructor == Date
 51+ || from.constructor == RegExp
 52+ || from.constructor == Function
 53+ || from.constructor == String
 54+ || from.constructor == Number
 55+ || from.constructor == Boolean ) {
 56+ return new from.constructor( from );
 57+ }
 58+ to = to || new from.constructor();
 59+ for ( var name in from ) {
 60+ to[name] = typeof to[name] == 'undefined' ? copy( from[name], null ) : to[name];
 61+ }
 62+ return to;
 63+}
 64+
 65+$.flow = {
 66+ 'charCache': {},
 67+ 'wordCache': {},
 68+ 'measureWord': function( text, ruler ) {
 69+ if ( $.flow.wordCache[text] === undefined ) {
 70+ // Cache miss
 71+ var word = { 'text': text, 'html': '', 'metrics': [] };
 72+ for ( var i = 0; i < text.length; i++ ) {
 73+ var char = text[i],
 74+ charHtml = char
 75+ .replace( '&', '&amp;' )
 76+ .replace( ' ', '&nbsp;' )
 77+ .replace( '<', '&lt;' )
 78+ .replace( '>', '&gt;' )
 79+ .replace( '\'', '&apos;' )
 80+ .replace( '"', '&quot;' );
 81+ word.html += charHtml;
 82+ if ( $.flow.charCache[char] === undefined ) {
 83+ // Cache miss
 84+ ruler.innerHTML = charHtml;
 85+ word.metrics.push( $.flow.charCache[char] = ruler.clientWidth );
 86+ continue;
 87+ }
 88+ // Cache hit
 89+ word.metrics.push( $.flow.charCache[char] );
 90+ }
 91+ ruler.innerHTML = word.html;
 92+ word.width = ruler.clientWidth;
 93+ $.flow.wordCache[text] = copy( word );
 94+ return word;
 95+ }
 96+ // Cache hit
 97+ return copy( $.flow.wordCache[text] );
 98+ },
 99+ 'getWords': function( text, ruler ) {
 100+ var words = [],
 101+ bounadry = /[ \-\t\r\n\f]/,
 102+ left = 0,
 103+ right = 0,
 104+ search = 0;
 105+ while ( ( search = text.substr( right ).search( bounadry ) ) >= 0 ) {
 106+ right += search;
 107+ words.push( $.flow.measureWord( text.substring( left, right ), ruler ) );
 108+ if ( right < text.length ) {
 109+ words.push( $.flow.measureWord( text.substring( right, ++right ), ruler ) );
 110+ }
 111+ left = right;
 112+ }
 113+ words.push( $.flow.measureWord( text.substring( right, text.length ), ruler ) );
 114+ return words;
 115+ },
 116+ 'getLines': function( words, width ) {
 117+ // Lineify
 118+ var lineCount = 0,
 119+ charCount = 0,
 120+ wordCount = 0,
 121+ lines = [],
 122+ line = {
 123+ 'text': '',
 124+ 'html': '',
 125+ 'width': 0,
 126+ 'metrics': [],
 127+ 'words': [],
 128+ 'index': lineCount
 129+ };
 130+ for ( var i = 0; i < words.length; i++ ) {
 131+ if ( line.width + words[i].width > width ) {
 132+ lines.push( line );
 133+ charCount = 0;
 134+ wordCount = 0;
 135+ lineCount++;
 136+ line = {
 137+ 'text': '',
 138+ 'html': '',
 139+ 'width': 0,
 140+ 'metrics': [],
 141+ 'words': [],
 142+ 'index': lineCount
 143+ };
 144+ }
 145+ words[i].index = wordCount;
 146+ wordCount++;
 147+ words[i].offset = charCount;
 148+ charCount += words[i].text.length;
 149+ line.words.push( words[i] );
 150+ line.text += words[i].text;
 151+ line.html += words[i].html;
 152+ line.width += words[i].width;
 153+ line.metrics.push( words[i].width );
 154+ }
 155+ if ( line.text.length ) {
 156+ lines.push( line );
 157+ }
 158+ return lines;
 159+ }
 160+};
 161+
 162+$.fn.flow = function( text ) {
 163+ console.time( 'flow' );
 164+
 165+ var $this = $(this),
 166+ lines = $.flow.getLines(
 167+ $.flow.getWords( text, $( '<div class="editSurface-line"></div>' ).appendTo( $this )[0] ),
 168+ $this.innerWidth()
 169+ );
 170+
 171+ // Flow
 172+ $this.empty();
 173+ for ( var i = 0; i < lines.length; i++ ) {
 174+ var $line = $( '<div class="editSurface-line"></div>' ).data( 'flow', lines[i] );
 175+ if ( lines[i].text.length === 1 && lines[1].text.match( /[ \-\t\r\n\f]/ ) ) {
 176+ $line.html( '&nbsp;' );
 177+ $line.addClass( 'empty' );
 178+ } else {
 179+ $line.html( lines[i].html );
 180+ }
 181+ $this.append( $line );
 182+ }
 183+
 184+ console.timeEnd( 'flow' );
 185+ return $this;
 186+};
Property changes on: trunk/parsers/wikidom/lib/jquery.flow.js
___________________________________________________________________
Added: svn:eol-style
1187 + native
Added: svn:mime-type
2188 + text/plain
Index: trunk/parsers/wikidom/lib/jquery.editSurface.js
@@ -49,7 +49,7 @@
5050 .mouseup( function( e ) {
5151 if ( sel.active ) {
5252 if ( !sel.from || !sel.to
53 - || ( sel.from.line === sel.to.line && sel.from.index === sel.to.index ) ) {
 53+ || ( sel.from.line === sel.to.line && sel.from.char === sel.to.char ) ) {
5454 sel.from = null;
5555 sel.to = null;
5656 sel.start = null;
@@ -71,9 +71,12 @@
7272 );
7373 }
7474 sel.end = getCursorPosition( e.pageX, e.pageY, $target );
 75+ //console.log( [sel.start.char, sel.end.char] );
 76+ //console.log( [sel.start.word, sel.end.word] );
 77+ //console.log( [sel.start.line, sel.end.line] );
7578 if ( sel.start.line < sel.end.line
7679 || ( sel.start.line === sel.end.line
77 - && sel.start.index < sel.end.index ) ) {
 80+ && sel.start.char < sel.end.char ) ) {
7881 sel.from = sel.start;
7982 sel.to = sel.end;
8083 } else {
@@ -85,7 +88,6 @@
8689 }
8790 } );
8891
89 -
9092 // Shortcuts
9193 var $document = $this.find( '.editSurface-document' );
9294 var ranges = {
@@ -100,47 +102,56 @@
101103 var text;
102104 if ( sel.from && sel.to ) {
103105 if ( sel.from.line === sel.to.line ) {
104 - text = sel.from.$target.data( 'text' ).substr(
105 - sel.from.index, sel.to.index - sel.from.index
 106+ text = sel.from.$target.data( 'flow' ).text.substr(
 107+ sel.from.char, sel.to.char - sel.from.char
106108 );
107109 } else {
108 - text = sel.from.$target.data( 'text' ).substr( sel.from.index );
 110+ text = sel.from.$target.data( 'flow' ).text.substr( sel.from.char );
109111 var $sibling = sel.from.$target.next();
110112 for ( var i = sel.from.line + 1; i < sel.to.line; i++ ) {
111 - text += $sibling.data( 'text' )
 113+ text += $sibling.data( 'flow' ).text
112114 $sibling = $sibling.next();
113115 }
114 - text += sel.to.$target.data( 'text' ).substr( 0, sel.to.index );
 116+ text += sel.to.$target.data( 'flow' ).text.substr( 0, sel.to.char );
115117 }
116118 }
117119 return text;
118120 }
119121 function getCursorPosition( x, y, $target ) {
120 - var metrics = $target.data( 'metrics' );
121 - var text = $target.data( 'text' );
122 - var line = $target.data( 'line' );
123 - if ( !$.isArray( metrics ) || metrics.length === 0 ) {
124 - throw "Missing metrics data error"
125 - }
126 - var to = metrics.length - 1;
127 - var a;
128 - var b = { 'l': 0, 'c': 0, 'r': 0 };
129 - var c = x - $target.offset().left;
130 - for ( var i = 0; i <= to; i++ ) {
131 - a = b;
132 - b = { 'l': a.r, 'c': a.r + ( metrics[i] / 2 ), 'r': a.r + metrics[i] };
133 - if ( ( i === 0 && c <= a.l ) || ( c >= a.c && c <= b.c ) || i === to ) {
134 - var offset = $target.offset();
135 - var height = $target.height();
136 - return {
137 - '$target': $target,
138 - 'index': i,
139 - 'line': line,
140 - 'x': offset.left + b.l,
141 - 'top': offset.top,
142 - 'bottom': offset.top + height,
143 - 'height': height
144 - };
 122+ var line = $target.data( 'flow' ),
 123+ offset = $target.offset(),
 124+ height = $target.height(),
 125+ l,
 126+ r = 0,
 127+ cur = x - offset.left;
 128+ for ( var w = 0, eol = line.metrics.length; w <= eol; w++ ) {
 129+ var wi = Math.min( w, eol - 1 );
 130+ l = r;
 131+ r += line.metrics[wi];
 132+ if ( ( w === 0 && cur <= l ) || ( cur >= l && cur <= r ) || ( w === eol ) ) {
 133+ var word = line.words[wi],
 134+ a,
 135+ b = { 'l': l, 'c': l, 'r': l };
 136+ for ( var c = 0, eow = word.metrics.length; c <= eow; c++ ) {
 137+ a = b;
 138+ b = {
 139+ 'l': a.r,
 140+ 'c': a.r + ( word.metrics[c] / 2 ),
 141+ 'r': a.r + word.metrics[c]
 142+ };
 143+ if ( ( c === 0 && cur <= a.l ) || ( cur >= a.c && cur <= b.c ) || c === eow ) {
 144+ return {
 145+ '$target': $target,
 146+ 'char': word.offset + Math.min( c, word.text.length - 1 ),
 147+ 'word': word.index,
 148+ 'line': line.index,
 149+ 'x': offset.left + ( c < eow ? b.l : a.l ),
 150+ 'top': offset.top,
 151+ 'bottom': offset.top + height,
 152+ 'height': height
 153+ };
 154+ }
 155+ }
145156 }
146157 }
147158 }
@@ -153,7 +164,7 @@
154165 if ( sel.from && sel.to ) {
155166 if ( sel.from.line === sel.to.line ) {
156167 // 1 line
157 - if ( sel.from.index !== sel.to.index ) {
 168+ if ( sel.from.char !== sel.to.char ) {
158169 ranges.$first.show().css( {
159170 'left': sel.from.x,
160171 'top': sel.from.top,
Index: trunk/parsers/wikidom/demos/surface/index.html
@@ -13,7 +13,7 @@
1414 <!-- EditSurface -->
1515 <script type="text/javascript" src="../../lib/jquery.js"></script>
1616 <script type="text/javascript" src="../../lib/jquery.closestToOffset.js"></script>
17 - <script type="text/javascript" src="../../lib/jquery.flow-a.js"></script>
 17+ <script type="text/javascript" src="../../lib/jquery.flow.js"></script>
1818 <script type="text/javascript" src="../../lib/jquery.editSurface.js"></script>
1919
2020 <!-- Demo -->
@@ -28,7 +28,7 @@
2929 "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.",
3030 "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.",
3131 "OccasionallyThereAreWordsThatAreSoLongTheyExceedTheWidthOfTheLineAndEndUpWrappingBetweenMultipleLines.",
32 - ].join( '\n\n' );
 32+ ].join( ' ' );
3333 $( '#es' ).editSurface( {
3434 'document': { 'blocks': [ {
3535 'type': 'paragraph',

Status & tagging log