5a920362 |
1 | /** |
2 | * This code is mostly from the old Etherpad. Please help us to comment this code. |
3 | * This helps other people to understand this code better and helps them to improve it. |
4 | * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED |
5 | */ |
6 | |
7 | /** |
8 | * Copyright 2009 Google Inc. |
9 | * |
10 | * Licensed under the Apache License, Version 2.0 (the "License"); |
11 | * you may not use this file except in compliance with the License. |
12 | * You may obtain a copy of the License at |
13 | * |
14 | * http://www.apache.org/licenses/LICENSE-2.0 |
15 | * |
16 | * Unless required by applicable law or agreed to in writing, software |
17 | * distributed under the License is distributed on an "AS-IS" BASIS, |
18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
19 | * See the License for the specific language governing permissions and |
20 | * limitations under the License. |
21 | */ |
22 | |
23 | function makeVirtualLineView(lineNode) |
24 | { |
25 | |
26 | // how much to jump forward or backward at once in a charSeeker before |
27 | // constructing a DOM node and checking the coordinates (which takes a |
28 | // significant fraction of a millisecond). From the |
29 | // coordinates and the approximate line height we can estimate how |
30 | // many lines we have moved. We risk being off if the number of lines |
31 | // we move is on the order of the line height in pixels. Fortunately, |
32 | // when the user boosts the font-size they increase both. |
33 | var maxCharIncrement = 20; |
34 | var seekerAtEnd = null; |
35 | |
36 | function getNumChars() |
37 | { |
38 | return lineNode.textContent.length; |
39 | } |
40 | |
41 | function getNumVirtualLines() |
42 | { |
43 | if (!seekerAtEnd) |
44 | { |
45 | var seeker = makeCharSeeker(); |
46 | seeker.forwardByWhile(maxCharIncrement); |
47 | seekerAtEnd = seeker; |
48 | } |
49 | return seekerAtEnd.getVirtualLine() + 1; |
50 | } |
51 | |
52 | function getVLineAndOffsetForChar(lineChar) |
53 | { |
54 | var seeker = makeCharSeeker(); |
55 | seeker.forwardByWhile(maxCharIncrement, null, lineChar); |
56 | var theLine = seeker.getVirtualLine(); |
57 | seeker.backwardByWhile(8, function() |
58 | { |
59 | return seeker.getVirtualLine() == theLine; |
60 | }); |
61 | seeker.forwardByWhile(1, function() |
62 | { |
63 | return seeker.getVirtualLine() != theLine; |
64 | }); |
65 | var lineStartChar = seeker.getOffset(); |
66 | return { |
67 | vline: theLine, |
68 | offset: (lineChar - lineStartChar) |
69 | }; |
70 | } |
71 | |
72 | function getCharForVLineAndOffset(vline, offset) |
73 | { |
74 | // returns revised vline and offset as well as absolute char index within line. |
75 | // if offset is beyond end of line, for example, will give new offset at end of line. |
76 | var seeker = makeCharSeeker(); |
77 | // go to start of line |
78 | seeker.binarySearch(function() |
79 | { |
80 | return seeker.getVirtualLine() >= vline; |
81 | }); |
82 | var lineStart = seeker.getOffset(); |
83 | var theLine = seeker.getVirtualLine(); |
84 | // go to offset, overshooting the virtual line only if offset is too large for it |
85 | seeker.forwardByWhile(maxCharIncrement, null, lineStart + offset); |
86 | // get back into line |
87 | seeker.backwardByWhile(1, function() |
88 | { |
89 | return seeker.getVirtualLine() != theLine; |
90 | }, lineStart); |
91 | var lineChar = seeker.getOffset(); |
92 | var theOffset = lineChar - lineStart; |
93 | // handle case of last virtual line; should be able to be at end of it |
94 | if (theOffset < offset && theLine == (getNumVirtualLines() - 1)) |
95 | { |
96 | var lineLen = getNumChars(); |
97 | theOffset += lineLen - lineChar; |
98 | lineChar = lineLen; |
99 | } |
100 | |
101 | return { |
102 | vline: theLine, |
103 | offset: theOffset, |
104 | lineChar: lineChar |
105 | }; |
106 | } |
107 | |
108 | return { |
109 | getNumVirtualLines: getNumVirtualLines, |
110 | getVLineAndOffsetForChar: getVLineAndOffsetForChar, |
111 | getCharForVLineAndOffset: getCharForVLineAndOffset, |
112 | makeCharSeeker: function() |
113 | { |
114 | return makeCharSeeker(); |
115 | } |
116 | }; |
117 | |
118 | function deepFirstChildTextNode(nd) |
119 | { |
120 | nd = nd.firstChild; |
121 | while (nd && nd.firstChild) nd = nd.firstChild; |
122 | if (nd.data) return nd; |
123 | return null; |
124 | } |
125 | |
126 | function makeCharSeeker( /*lineNode*/ ) |
127 | { |
128 | |
129 | function charCoords(tnode, i) |
130 | { |
131 | var container = tnode.parentNode; |
132 | |
133 | // treat space specially; a space at the end of a virtual line |
134 | // will have weird coordinates |
135 | var isSpace = (tnode.nodeValue.charAt(i) === " "); |
136 | if (isSpace) |
137 | { |
138 | if (i == 0) |
139 | { |
140 | if (container.previousSibling && deepFirstChildTextNode(container.previousSibling)) |
141 | { |
142 | tnode = deepFirstChildTextNode(container.previousSibling); |
143 | i = tnode.length - 1; |
144 | container = tnode.parentNode; |
145 | } |
146 | else |
147 | { |
148 | return { |
149 | top: container.offsetTop, |
150 | left: container.offsetLeft |
151 | }; |
152 | } |
153 | } |
154 | else |
155 | { |
156 | i--; // use previous char |
157 | } |
158 | } |
159 | |
160 | |
161 | var charWrapper = document.createElement("SPAN"); |
162 | |
163 | // wrap the character |
164 | var tnodeText = tnode.nodeValue; |
165 | var frag = document.createDocumentFragment(); |
166 | frag.appendChild(document.createTextNode(tnodeText.substring(0, i))); |
167 | charWrapper.appendChild(document.createTextNode(tnodeText.substr(i, 1))); |
168 | frag.appendChild(charWrapper); |
169 | frag.appendChild(document.createTextNode(tnodeText.substring(i + 1))); |
170 | container.replaceChild(frag, tnode); |
171 | |
172 | var result = { |
173 | top: charWrapper.offsetTop, |
174 | left: charWrapper.offsetLeft + (isSpace ? charWrapper.offsetWidth : 0), |
175 | height: charWrapper.offsetHeight |
176 | }; |
177 | |
178 | while (container.firstChild) container.removeChild(container.firstChild); |
179 | container.appendChild(tnode); |
180 | |
181 | return result; |
182 | } |
183 | |
184 | var lineText = lineNode.textContent; |
185 | var lineLength = lineText.length; |
186 | |
187 | var curNode = null; |
188 | var curChar = 0; |
189 | var curCharWithinNode = 0 |
190 | var curTop; |
191 | var curLeft; |
192 | var approxLineHeight; |
193 | var whichLine = 0; |
194 | |
195 | function nextNode() |
196 | { |
197 | var n = curNode; |
198 | if (!n) n = lineNode.firstChild; |
199 | else n = n.nextSibling; |
200 | while (n && !deepFirstChildTextNode(n)) |
201 | { |
202 | n = n.nextSibling; |
203 | } |
204 | return n; |
205 | } |
206 | |
207 | function prevNode() |
208 | { |
209 | var n = curNode; |
210 | if (!n) n = lineNode.lastChild; |
211 | else n = n.previousSibling; |
212 | while (n && !deepFirstChildTextNode(n)) |
213 | { |
214 | n = n.previousSibling; |
215 | } |
216 | return n; |
217 | } |
218 | |
219 | var seeker; |
220 | if (lineLength > 0) |
221 | { |
222 | curNode = nextNode(); |
223 | var firstCharData = charCoords(deepFirstChildTextNode(curNode), 0); |
224 | approxLineHeight = firstCharData.height; |
225 | curTop = firstCharData.top; |
226 | curLeft = firstCharData.left; |
227 | |
228 | function updateCharData(tnode, i) |
229 | { |
230 | var coords = charCoords(tnode, i); |
231 | whichLine += Math.round((coords.top - curTop) / approxLineHeight); |
232 | curTop = coords.top; |
233 | curLeft = coords.left; |
234 | } |
235 | |
236 | seeker = { |
237 | forward: function(numChars) |
238 | { |
239 | var oldChar = curChar; |
240 | var newChar = curChar + numChars; |
241 | if (newChar > (lineLength - 1)) newChar = lineLength - 1; |
242 | while (curChar < newChar) |
243 | { |
244 | var curNodeLength = deepFirstChildTextNode(curNode).length; |
245 | var toGo = curNodeLength - curCharWithinNode; |
246 | if (curChar + toGo > newChar || !nextNode()) |
247 | { |
248 | // going to next node would be too far |
249 | var n = newChar - curChar; |
250 | if (n >= toGo) n = toGo - 1; |
251 | curChar += n; |
252 | curCharWithinNode += n; |
253 | break; |
254 | } |
255 | else |
256 | { |
257 | // go to next node |
258 | curChar += toGo; |
259 | curCharWithinNode = 0; |
260 | curNode = nextNode(); |
261 | } |
262 | } |
263 | updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode); |
264 | return curChar - oldChar; |
265 | }, |
266 | backward: function(numChars) |
267 | { |
268 | var oldChar = curChar; |
269 | var newChar = curChar - numChars; |
270 | if (newChar < 0) newChar = 0; |
271 | while (curChar > newChar) |
272 | { |
273 | if (curChar - curCharWithinNode <= newChar || !prevNode()) |
274 | { |
275 | // going to prev node would be too far |
276 | var n = curChar - newChar; |
277 | if (n > curCharWithinNode) n = curCharWithinNode; |
278 | curChar -= n; |
279 | curCharWithinNode -= n; |
280 | break; |
281 | } |
282 | else |
283 | { |
284 | // go to prev node |
285 | curChar -= curCharWithinNode + 1; |
286 | curNode = prevNode(); |
287 | curCharWithinNode = deepFirstChildTextNode(curNode).length - 1; |
288 | } |
289 | } |
290 | updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode); |
291 | return oldChar - curChar; |
292 | }, |
293 | getVirtualLine: function() |
294 | { |
295 | return whichLine; |
296 | }, |
297 | getLeftCoord: function() |
298 | { |
299 | return curLeft; |
300 | } |
301 | }; |
302 | } |
303 | else |
304 | { |
305 | curLeft = lineNode.offsetLeft; |
306 | seeker = { |
307 | forward: function(numChars) |
308 | { |
309 | return 0; |
310 | }, |
311 | backward: function(numChars) |
312 | { |
313 | return 0; |
314 | }, |
315 | getVirtualLine: function() |
316 | { |
317 | return 0; |
318 | }, |
319 | getLeftCoord: function() |
320 | { |
321 | return curLeft; |
322 | } |
323 | }; |
324 | } |
325 | seeker.getOffset = function() |
326 | { |
327 | return curChar; |
328 | }; |
329 | seeker.getLineLength = function() |
330 | { |
331 | return lineLength; |
332 | }; |
333 | seeker.toString = function() |
334 | { |
335 | return "seeker[curChar: " + curChar + "(" + lineText.charAt(curChar) + "), left: " + seeker.getLeftCoord() + ", vline: " + seeker.getVirtualLine() + "]"; |
336 | }; |
337 | |
338 | function moveByWhile(isBackward, amount, optCondFunc, optCharLimit) |
339 | { |
340 | var charsMovedLast = null; |
341 | var hasCondFunc = ((typeof optCondFunc) == "function"); |
342 | var condFunc = optCondFunc; |
343 | var hasCharLimit = ((typeof optCharLimit) == "number"); |
344 | var charLimit = optCharLimit; |
345 | while (charsMovedLast !== 0 && ((!hasCondFunc) || condFunc())) |
346 | { |
347 | var toMove = amount; |
348 | if (hasCharLimit) |
349 | { |
350 | var untilLimit = (isBackward ? curChar - charLimit : charLimit - curChar); |
351 | if (untilLimit < toMove) toMove = untilLimit; |
352 | } |
353 | if (toMove < 0) break; |
354 | charsMovedLast = (isBackward ? seeker.backward(toMove) : seeker.forward(toMove)); |
355 | } |
356 | } |
357 | |
358 | seeker.forwardByWhile = function(amount, optCondFunc, optCharLimit) |
359 | { |
360 | moveByWhile(false, amount, optCondFunc, optCharLimit); |
361 | } |
362 | seeker.backwardByWhile = function(amount, optCondFunc, optCharLimit) |
363 | { |
364 | moveByWhile(true, amount, optCondFunc, optCharLimit); |
365 | } |
366 | seeker.binarySearch = function(condFunc) |
367 | { |
368 | // returns index of boundary between false chars and true chars; |
369 | // positions seeker at first true char, or else last char |
370 | var trueFunc = condFunc; |
371 | var falseFunc = function() |
372 | { |
373 | return !condFunc(); |
374 | }; |
375 | seeker.forwardByWhile(20, falseFunc); |
376 | seeker.backwardByWhile(20, trueFunc); |
377 | seeker.forwardByWhile(10, falseFunc); |
378 | seeker.backwardByWhile(5, trueFunc); |
379 | seeker.forwardByWhile(1, falseFunc); |
380 | return seeker.getOffset() + (condFunc() ? 0 : 1); |
381 | } |
382 | |
383 | return seeker; |
384 | } |
385 | |
386 | } |
387 | |
388 | exports.makeVirtualLineView = makeVirtualLineView; |