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, '<' ) |
14 | | - .replace( />/g, '>' ); |
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 = ' '; |
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( '&', '&' ) |
24 | | - .replace( ' ', ' ' ) |
25 | | - .replace( '<', '<' ) |
26 | | - .replace( '>', '>' ) |
27 | | - .replace( '\'', ''' ) |
28 | | - .replace( '"', '"' ); |
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( ' ' ); |
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: ' ', |
| 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( '&', '&' ) |
| 76 | + .replace( ' ', ' ' ) |
| 77 | + .replace( '<', '<' ) |
| 78 | + .replace( '>', '>' ) |
| 79 | + .replace( '\'', ''' ) |
| 80 | + .replace( '"', '"' ); |
| 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( ' ' ); |
| 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 |
1 | 187 | + native |
Added: svn:mime-type |
2 | 188 | + text/plain |
Index: trunk/parsers/wikidom/lib/jquery.editSurface.js |
— | — | @@ -49,7 +49,7 @@ |
50 | 50 | .mouseup( function( e ) { |
51 | 51 | if ( sel.active ) { |
52 | 52 | 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 ) ) { |
54 | 54 | sel.from = null; |
55 | 55 | sel.to = null; |
56 | 56 | sel.start = null; |
— | — | @@ -71,9 +71,12 @@ |
72 | 72 | ); |
73 | 73 | } |
74 | 74 | 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] ); |
75 | 78 | if ( sel.start.line < sel.end.line |
76 | 79 | || ( sel.start.line === sel.end.line |
77 | | - && sel.start.index < sel.end.index ) ) { |
| 80 | + && sel.start.char < sel.end.char ) ) { |
78 | 81 | sel.from = sel.start; |
79 | 82 | sel.to = sel.end; |
80 | 83 | } else { |
— | — | @@ -85,7 +88,6 @@ |
86 | 89 | } |
87 | 90 | } ); |
88 | 91 | |
89 | | - |
90 | 92 | // Shortcuts |
91 | 93 | var $document = $this.find( '.editSurface-document' ); |
92 | 94 | var ranges = { |
— | — | @@ -100,47 +102,56 @@ |
101 | 103 | var text; |
102 | 104 | if ( sel.from && sel.to ) { |
103 | 105 | 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 |
106 | 108 | ); |
107 | 109 | } else { |
108 | | - text = sel.from.$target.data( 'text' ).substr( sel.from.index ); |
| 110 | + text = sel.from.$target.data( 'flow' ).text.substr( sel.from.char ); |
109 | 111 | var $sibling = sel.from.$target.next(); |
110 | 112 | for ( var i = sel.from.line + 1; i < sel.to.line; i++ ) { |
111 | | - text += $sibling.data( 'text' ) |
| 113 | + text += $sibling.data( 'flow' ).text |
112 | 114 | $sibling = $sibling.next(); |
113 | 115 | } |
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 ); |
115 | 117 | } |
116 | 118 | } |
117 | 119 | return text; |
118 | 120 | } |
119 | 121 | 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 | + } |
145 | 156 | } |
146 | 157 | } |
147 | 158 | } |
— | — | @@ -153,7 +164,7 @@ |
154 | 165 | if ( sel.from && sel.to ) { |
155 | 166 | if ( sel.from.line === sel.to.line ) { |
156 | 167 | // 1 line |
157 | | - if ( sel.from.index !== sel.to.index ) { |
| 168 | + if ( sel.from.char !== sel.to.char ) { |
158 | 169 | ranges.$first.show().css( { |
159 | 170 | 'left': sel.from.x, |
160 | 171 | 'top': sel.from.top, |
Index: trunk/parsers/wikidom/demos/surface/index.html |
— | — | @@ -13,7 +13,7 @@ |
14 | 14 | <!-- EditSurface --> |
15 | 15 | <script type="text/javascript" src="../../lib/jquery.js"></script> |
16 | 16 | <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> |
18 | 18 | <script type="text/javascript" src="../../lib/jquery.editSurface.js"></script> |
19 | 19 | |
20 | 20 | <!-- Demo --> |
— | — | @@ -28,7 +28,7 @@ |
29 | 29 | "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.", |
30 | 30 | "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.", |
31 | 31 | "OccasionallyThereAreWordsThatAreSoLongTheyExceedTheWidthOfTheLineAndEndUpWrappingBetweenMultipleLines.", |
32 | | - ].join( '\n\n' ); |
| 32 | + ].join( ' ' ); |
33 | 33 | $( '#es' ).editSurface( { |
34 | 34 | 'document': { 'blocks': [ { |
35 | 35 | 'type': 'paragraph', |