blob: 0763520d45c49cb5ceeaf04db1d86ebcb3df7d56 [file] [log] [blame]
rginda87b86462011-12-14 13:48:03 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
rginda8ba33642011-12-14 12:31:31 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview This class represents a single terminal screen full of text.
7 *
8 * It maintains the current cursor position and has basic methods for text
9 * insert and overwrite, and adding or removing rows from the screen.
10 *
11 * This class has no knowledge of the scrollback buffer.
12 *
13 * The number of rows on the screen is determined only by the number of rows
14 * that the caller inserts into the screen. If a caller wants to ensure a
15 * constant number of rows on the screen, it's their responsibility to remove a
16 * row for each row inserted.
17 *
18 * The screen width, in contrast, is enforced locally.
19 *
20 *
21 * In practice...
22 * - The hterm.Terminal class holds two hterm.Screen instances. One for the
23 * primary screen and one for the alternate screen.
24 *
25 * - The html.Screen class only cares that rows are HTMLElements. In the
26 * larger context of hterm, however, the rows happen to be displayed by an
27 * hterm.ScrollPort and have to follow a few rules as a result. Each
28 * row must be rooted by the custom HTML tag 'x-row', and each must have a
29 * rowIndex property that corresponds to the index of the row in the context
30 * of the scrollback buffer. These invariants are enforced by hterm.Terminal
31 * because that is the class using the hterm.Screen in the context of an
32 * hterm.ScrollPort.
33 */
34
35/**
36 * Create a new screen instance.
37 *
38 * The screen initially has no rows and a maximum column count of 0.
39 *
40 * @param {integer} opt_columnCount The maximum number of columns for this
41 * screen. See insertString() and overwriteString() for information about
42 * what happens when too many characters are added too a row. Defaults to
43 * 0 if not provided.
44 */
45hterm.Screen = function(opt_columnCount) {
46 /**
47 * Public, read-only access to the rows in this screen.
48 */
49 this.rowsArray = [];
50
51 // The max column width for this screen.
rginda87b86462011-12-14 13:48:03 -080052 this.columnCount_ = opt_columnCount || 80;
rginda8ba33642011-12-14 12:31:31 -080053
rgindaa19afe22012-01-25 15:40:22 -080054 // The current color, bold, underline and blink attributes.
55 this.textAttributes = new hterm.TextAttributes(window.document);
56
rginda87b86462011-12-14 13:48:03 -080057 // Current zero-based cursor coordinates.
58 this.cursorPosition = new hterm.RowCol(0, 0);
rginda8ba33642011-12-14 12:31:31 -080059
60 // The node containing the row that the cursor is positioned on.
61 this.cursorRowNode_ = null;
62
63 // The node containing the span of text that the cursor is positioned on.
64 this.cursorNode_ = null;
65
66 // The offset into cursorNode_ where the cursor is positioned.
67 this.cursorOffset_ = null;
68};
69
70/**
71 * Return the screen size as an hterm.Size object.
72 *
73 * @return {hterm.Size} hterm.Size object representing the current number
74 * of rows and columns in this screen.
75 */
76hterm.Screen.prototype.getSize = function() {
77 return new hterm.Size(this.columnCount_, this.rowsArray.length);
78};
79
80/**
81 * Return the current number of rows in this screen.
82 *
83 * @return {integer} The number of rows in this screen.
84 */
85hterm.Screen.prototype.getHeight = function() {
86 return this.rowsArray.length;
87};
88
89/**
90 * Return the current number of columns in this screen.
91 *
92 * @return {integer} The number of columns in this screen.
93 */
94hterm.Screen.prototype.getWidth = function() {
95 return this.columnCount_;
96};
97
98/**
99 * Set the maximum number of columns per row.
100 *
rginda8ba33642011-12-14 12:31:31 -0800101 * @param {integer} count The maximum number of columns per row.
102 */
103hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800104 this.columnCount_ = count;
105
rginda35c456b2012-02-09 17:29:05 -0800106 if (this.cursorPosition.column >= count) {
107 this.setCursorPosition(this.cursorPosition.row,
108 this.cursorPosition.column - 1);
rginda87b86462011-12-14 13:48:03 -0800109 }
rginda8ba33642011-12-14 12:31:31 -0800110};
111
112/**
113 * Remove the first row from the screen and return it.
114 *
115 * @return {HTMLElement} The first row in this screen.
116 */
117hterm.Screen.prototype.shiftRow = function() {
118 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800119};
rginda8ba33642011-12-14 12:31:31 -0800120
121/**
122 * Remove rows from the top of the screen and return them as an array.
123 *
124 * @param {integer} count The number of rows to remove.
125 * @return {Array.<HTMLElement>} The selected rows.
126 */
127hterm.Screen.prototype.shiftRows = function(count) {
128 return this.rowsArray.splice(0, count);
129};
130
131/**
132 * Insert a row at the top of the screen.
133 *
134 * @param {HTMLElement} The row to insert.
135 */
136hterm.Screen.prototype.unshiftRow = function(row) {
137 this.rowsArray.splice(0, 0, row);
138};
139
140/**
141 * Insert rows at the top of the screen.
142 *
143 * @param {Array.<HTMLElement>} The rows to insert.
144 */
145hterm.Screen.prototype.unshiftRows = function(rows) {
146 this.rowsArray.unshift.apply(this.rowsArray, rows);
147};
148
149/**
150 * Remove the last row from the screen and return it.
151 *
152 * @return {HTMLElement} The last row in this screen.
153 */
154hterm.Screen.prototype.popRow = function() {
155 return this.popRows(1)[0];
156};
157
158/**
159 * Remove rows from the bottom of the screen and return them as an array.
160 *
161 * @param {integer} count The number of rows to remove.
162 * @return {Array.<HTMLElement>} The selected rows.
163 */
164hterm.Screen.prototype.popRows = function(count) {
165 return this.rowsArray.splice(this.rowsArray.length - count, count);
166};
167
168/**
169 * Insert a row at the bottom of the screen.
170 *
171 * @param {HTMLElement} The row to insert.
172 */
173hterm.Screen.prototype.pushRow = function(row) {
174 this.rowsArray.push(row);
175};
176
177/**
178 * Insert rows at the bottom of the screen.
179 *
180 * @param {Array.<HTMLElement>} The rows to insert.
181 */
182hterm.Screen.prototype.pushRows = function(rows) {
183 rows.push.apply(this.rowsArray, rows);
184};
185
186/**
187 * Insert a row at the specified column of the screen.
188 *
189 * @param {HTMLElement} The row to insert.
190 */
191hterm.Screen.prototype.insertRow = function(index, row) {
192 this.rowsArray.splice(index, 0, row);
193};
194
195/**
196 * Insert rows at the specified column of the screen.
197 *
198 * @param {Array.<HTMLElement>} The rows to insert.
199 */
200hterm.Screen.prototype.insertRows = function(index, rows) {
201 for (var i = 0; i < rows.length; i++) {
202 this.rowsArray.splice(index + i, 0, rows[i]);
203 }
204};
205
206/**
207 * Remove a last row from the specified column of the screen and return it.
208 *
209 * @return {HTMLElement} The selected row.
210 */
211hterm.Screen.prototype.removeRow = function(index) {
212 return this.rowsArray.splice(index, 1)[0];
213};
214
215/**
216 * Remove rows from the bottom of the screen and return them as an array.
217 *
218 * @param {integer} count The number of rows to remove.
219 * @return {Array.<HTMLElement>} The selected rows.
220 */
221hterm.Screen.prototype.removeRows = function(index, count) {
222 return this.rowsArray.splice(index, count);
223};
224
225/**
226 * Invalidate the current cursor position.
227 *
rginda87b86462011-12-14 13:48:03 -0800228 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800229 * data.
230 *
231 * Attempting to insert or overwrite text while the cursor position is invalid
232 * will raise an obscure exception.
233 */
234hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800235 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800236 this.cursorRowNode_ = null;
237 this.cursorNode_ = null;
238 this.cursorOffset_ = null;
239};
240
241/**
242 * Clear the contents of a selected row.
243 *
244 * TODO: Make this clear in the current style... somehow. We can't just
245 * fill the row with spaces, since they would have potential to mess up the
246 * terminal (for example, in insert mode, they might wrap around to the next
247 * line.
248 *
249 * @param {integer} index The zero-based index to clear.
250 */
251hterm.Screen.prototype.clearRow = function(index) {
252 if (index == this.cursorPosition.row) {
253 this.clearCursorRow();
254 } else {
255 var row = this.rowsArray[index];
256 row.innerHTML = '';
257 row.appendChild(row.ownerDocument.createTextNode(''));
258 }
259};
260
261/**
262 * Clear the contents of the cursor row.
263 *
264 * TODO: Same comment as clearRow().
265 */
266hterm.Screen.prototype.clearCursorRow = function() {
267 this.cursorRowNode_.innerHTML = '';
268 var text = this.cursorRowNode_.ownerDocument.createTextNode('');
269 this.cursorRowNode_.appendChild(text);
270 this.cursorOffset_ = 0;
271 this.cursorNode_ = text;
272 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800273 this.cursorPosition.overflow = false;
rginda8ba33642011-12-14 12:31:31 -0800274};
275
276/**
277 * Relocate the cursor to a give row and column.
278 *
279 * @param {integer} row The zero based row.
280 * @param {integer} column The zero based column.
281 */
282hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda87b86462011-12-14 13:48:03 -0800283 if (row >= this.rowsArray.length) {
284 console.log('Row out of bounds: ' + row, hterm.getStack(1));
285 row = this.rowsArray.length - 1;
286 } else if (row < 0) {
287 console.log('Row out of bounds: ' + row, hterm.getStack(1));
288 row = 0;
289 }
290
291 if (column >= this.columnCount_) {
292 console.log('Column out of bounds: ' + column, hterm.getStack(1));
293 column = this.columnCount_ - 1;
294 } else if (column < 0) {
295 console.log('Column out of bounds: ' + column, hterm.getStack(1));
296 column = 0;
297 }
rginda8ba33642011-12-14 12:31:31 -0800298
rginda2312fff2012-01-05 16:20:52 -0800299 this.cursorPosition.overflow = false;
300
rginda8ba33642011-12-14 12:31:31 -0800301 var rowNode = this.rowsArray[row];
302 var node = rowNode.firstChild;
303
304 if (!node) {
305 node = rowNode.ownerDocument.createTextNode('');
306 rowNode.appendChild(node);
307 }
308
rgindaa19afe22012-01-25 15:40:22 -0800309 var currentColumn = 0;
310
rginda8ba33642011-12-14 12:31:31 -0800311 if (rowNode == this.cursorRowNode_) {
312 if (column >= this.cursorPosition.column - this.cursorOffset_) {
313 node = this.cursorNode_;
314 currentColumn = this.cursorPosition.column - this.cursorOffset_;
315 }
316 } else {
317 this.cursorRowNode_ = rowNode;
318 }
319
320 this.cursorPosition.move(row, column);
321
322 while (node) {
323 var offset = column - currentColumn;
324 var textContent = node.textContent;
325 if (!node.nextSibling || textContent.length > offset) {
326 this.cursorNode_ = node;
327 this.cursorOffset_ = offset;
328 return;
329 }
330
331 currentColumn += textContent.length;
332 node = node.nextSibling;
333 }
334};
335
336/**
rginda87b86462011-12-14 13:48:03 -0800337 * Set the provided selection object to be a caret selection at the current
338 * cursor position.
339 */
340hterm.Screen.prototype.syncSelectionCaret = function(selection) {
341 selection.collapse(this.cursorNode_, this.cursorOffset_);
342};
343
344/**
rgindaa19afe22012-01-25 15:40:22 -0800345 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800346 *
rgindaa19afe22012-01-25 15:40:22 -0800347 * For example:
348 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
349 * passing the span and an offset of 6. This would modifiy the fragment to
350 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
351 * had any attributes they would have been copied to the new span as well.
352 *
353 * The to-be-split node must have a container, so that the new node can be
354 * placed next to it.
355 *
356 * @param {HTMLNode} node The node to split.
357 * @param {integer} offset The offset into the node where the split should
358 * occur.
rginda8ba33642011-12-14 12:31:31 -0800359 */
rgindaa19afe22012-01-25 15:40:22 -0800360hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800361 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800362
rginda35c456b2012-02-09 17:29:05 -0800363 var textContent = node.textContent;
364 node.textContent = textContent.substr(0, offset);
365 afterNode.textContent = textContent.substr(offset);
rgindaa19afe22012-01-25 15:40:22 -0800366
367 node.parentNode.insertBefore(afterNode, node.nextSibling);
rginda8ba33642011-12-14 12:31:31 -0800368};
369
370/**
rgindaa19afe22012-01-25 15:40:22 -0800371 * Remove and return all content past the end of the current cursor position.
rginda8ba33642011-12-14 12:31:31 -0800372 *
rgindaa19afe22012-01-25 15:40:22 -0800373 * If necessary, the cursor's current node will be split. Everything past
374 * the end of the cursor will be returned in an array. Any empty nodes
375 * will be omitted from the result array. If the resulting array is empty,
376 * this function will return null.
rginda8ba33642011-12-14 12:31:31 -0800377 *
rgindaa19afe22012-01-25 15:40:22 -0800378 * @return {Array} An array of DOM nodes that used to appear after the cursor,
379 * or null if the cursor was already at the end of the line.
380 */
381hterm.Screen.prototype.clipAtCursor_ = function() {
382 if (this.cursorOffset_ < this.cursorNode_.textContent.length - 1)
383 this.splitNode_(this.cursorNode_, this.cursorOffset_ + 1);
384
385 var rv = null;
386 var rowNode = this.cursorRowNode_;
387 var node = this.cursorNode_.nextSibling;
388
389 while (node) {
390 var length = node.textContent.length;
391 if (length) {
392 if (rv) {
393 rv.push(node);
394 rv.characterLength += length;
395 } else {
396 rv = [node];
397 rv.characterLength = length;
398 }
399 }
400
401 rowNode.removeChild(node);
402 node = this.cursorNode_.nextSibling;
403 }
404
405 return rv;
406};
407
408/**
409 * Ensure that the current row does not overflow the current column count.
410 *
411 * If the current row is too long, it will be clipped and the overflow content
412 * will be returned as an array of DOM nodes. Otherwise this function returns
413 * null.
414 *
415 * @return {Array} An array of DOM nodes that overflowed in the current row,
416 * or null if the row did not overflow.
417 */
418hterm.Screen.prototype.maybeClipCurrentRow = function() {
419 var currentColumn = this.cursorPosition.column;
420
421 if (currentColumn >= this.columnCount_) {
422 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
423 this.cursorPosition.overflow = true;
424 return this.clipAtCursor_();
425 }
426
427 if (this.cursorRowNode_.textContent.length > this.columnCount_) {
428 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
429 var overflow = this.clipAtCursor_();
430 this.setCursorPosition(this.cursorPosition.row, currentColumn);
431 return overflow;
432 }
433
434 return null;
435};
436
437/**
438 * Insert a string at the current character position using the current
439 * text attributes.
440 *
441 * You must call maybeClipCurrentRow() after in order to check overflow.
rginda8ba33642011-12-14 12:31:31 -0800442 */
443hterm.Screen.prototype.insertString = function(str) {
rgindaa19afe22012-01-25 15:40:22 -0800444 var cursorNode = this.cursorNode_;
445 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800446
rgindaa19afe22012-01-25 15:40:22 -0800447 // We may alter the length of the string by prepending some missing
448 // whitespace, so we need to record the string length ahead of time.
449 var strLength = str.length;
rginda8ba33642011-12-14 12:31:31 -0800450
rgindaa19afe22012-01-25 15:40:22 -0800451 // No matter what, before this function exits the cursor column will have
452 // moved this much.
453 this.cursorPosition.column += strLength;
rginda8ba33642011-12-14 12:31:31 -0800454
rgindaa19afe22012-01-25 15:40:22 -0800455 // Local cache of the cursor offset.
456 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800457
rgindaa19afe22012-01-25 15:40:22 -0800458 // Reverse offset is the offset measured from the end of the string.
459 // Zero implies that the cursor is at the end of the cursor node.
460 var reverseOffset = cursorNodeText.length - offset
461
462 if (reverseOffset < 0) {
463 // A negative reverse offset means the cursor is positioned past the end
464 // of the characters on this line. We'll need to insert the missing
465 // whitespace.
466 var ws = hterm.getWhitespace(-reverseOffset);
467
468 // This whitespace should be completely unstyled. Underline and background
469 // color would be visible on whitespace, so we can't use one of those
470 // spans to hold the text.
471 if (!(this.textAttributes.underline || this.textAttributes.background)) {
472 // Best case scenario, we can just pretend the spaces were part of the
473 // original string.
474 str = ws + str;
475 } else if (cursorNode.nodeType == 3 ||
476 !(cursorNode.style.textDecoration ||
477 cursorNode.style.backgroundColor)) {
478 // Second best case, the current node is able to hold the whitespace.
479 cursorNode.textContent = (cursorNodeText += ws);
480 } else {
481 // Worst case, we have to create a new node to hold the whitespace.
482 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
483 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
484 this.cursorNode_ = cursorNode = wsNode;
485 this.cursorOffset_ = offset = -reverseOffset;
486 cursorNodeText = ws;
487 }
488
489 // We now know for sure that we're at the last character of the cursor node.
490 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800491 }
492
rgindaa19afe22012-01-25 15:40:22 -0800493 if (this.textAttributes.matchesContainer(cursorNode)) {
494 // The new text can be placed directly in the cursor node.
495 if (reverseOffset == 0) {
496 cursorNode.textContent = cursorNodeText + str;
497 } else if (offset == 0) {
498 cursorNode.textContent = str + cursorNodeText;
499 } else {
500 cursorNode.textContent = cursorNodeText.substr(0, offset) + str +
501 cursorNodeText.substr(offset);
502 }
rginda8ba33642011-12-14 12:31:31 -0800503
rgindaa19afe22012-01-25 15:40:22 -0800504 this.cursorOffset_ += strLength;
505 return;
rginda87b86462011-12-14 13:48:03 -0800506 }
507
rgindaa19afe22012-01-25 15:40:22 -0800508 // The cursor node is the wrong style for the new text. If we're at the
509 // beginning or end of the cursor node, then the adjacent node is also a
510 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800511
rgindaa19afe22012-01-25 15:40:22 -0800512 if (offset == 0) {
513 // At the beginning of the cursor node, the check the previous sibling.
514 var previousSibling = cursorNode.previousSibling;
515 if (previousSibling &&
516 this.textAttributes.matchesContainer(previousSibling)) {
517 previousSibling.textContent += str;
518 this.cursorNode_ = previousSibling;
519 this.cursorOffset_ = previousSibling.textContent.length;
520 return;
521 }
522
523 var newNode = this.textAttributes.createContainer(str);
524 this.cursorRowNode_.insertBefore(newNode, cursorNode);
525 this.cursorNode_ = newNode;
526 this.cursorOffset_ = strLength;
527 return;
528 }
529
530 if (reverseOffset == 0) {
531 // At the end of the cursor node, the check the next sibling.
532 var nextSibling = cursorNode.nextSibling;
533 if (nextSibling &&
534 this.textAttributes.matchesContainer(nextSibling)) {
535 nextSibling.textContent = str + nextSibling.textContent;
536 this.cursorNode_ = nextSibling;
537 this.cursorOffset_ = strLength;
538 return;
539 }
540
541 var newNode = this.textAttributes.createContainer(str);
542 this.cursorRowNode_.insertBefore(newNode, nextSibling);
543 this.cursorNode_ = newNode;
544 // We specifically need to include any missing whitespace here, since it's
545 // going in a new node.
546 this.cursorOffset_ = str.length;
547 return;
548 }
549
550 // Worst case, we're somewhere in the middle of the cursor node. We'll
551 // have to split it into two nodes and insert our new container in between.
552 this.splitNode_(cursorNode, offset);
553 var newNode = this.textAttributes.createContainer(str);
554 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
555 this.cursorNode_ = newNode;
556 this.cursorOffset_ = strLength;
557};
558
559/**
560 * Insert an array of DOM nodes at the beginning of the cursor row.
561 *
562 * This does not pay attention to the cursor column, it only prepends to the
563 * beginning of the current row.
564 *
565 * This method does not attempt to coalesce rows of the same style. It assumes
566 * that the rows being inserted have already been coalesced, and that there
567 * would be no gain in coalescing only the final node.
rginda35c456b2012-02-09 17:29:05 -0800568 *
569 * The cursor will be reset to the zero'th column.
rgindaa19afe22012-01-25 15:40:22 -0800570 */
571hterm.Screen.prototype.prependNodes = function(ary) {
572 var parentNode = this.cursorRowNode_;
573
574 for (var i = ary.length - 1; i >= 0; i--) {
575 parentNode.insertBefore(ary[i], parentNode.firstChild);
576 }
rginda35c456b2012-02-09 17:29:05 -0800577
578 // We have to leave the cursor in a sensible state so we don't confuse
579 // setCursorPosition. It's fastest to just leave it at the start of
580 // the row. If the caller wants it somewhere else, they can move it
581 // on their own.
582 this.cursorPosition.column = 0;
583 this.cursorNode_ = parentNode.firstChild;
584 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800585};
586
587/**
588 * Overwrite the text at the current cursor position.
589 *
rgindaa19afe22012-01-25 15:40:22 -0800590 * You must call maybeClipCurrentRow() after in order to check overflow.
rginda8ba33642011-12-14 12:31:31 -0800591 */
592hterm.Screen.prototype.overwriteString = function(str) {
593 var maxLength = this.columnCount_ - this.cursorPosition.column;
594 if (!maxLength)
rgindaa19afe22012-01-25 15:40:22 -0800595 return [str];
596
597 if ((this.cursorNode_.textContent.substr(this.cursorOffset_) == str) &&
598 this.textAttributes.matchesContainer(this.cursorNode_)) {
599 // This overwrite would be a no-op, just move the cursor and return.
600 this.cursorOffset_ += str.length;
601 this.cursorPosition.column += str.length;
602 return;
603 }
rginda8ba33642011-12-14 12:31:31 -0800604
605 this.deleteChars(Math.min(str.length, maxLength));
rgindaa19afe22012-01-25 15:40:22 -0800606 this.insertString(str);
rginda8ba33642011-12-14 12:31:31 -0800607};
608
609/**
610 * Forward-delete one or more characters at the current cursor position.
611 *
612 * Text to the right of the deleted characters is shifted left. Only affects
613 * characters on the same row as the cursor.
614 *
615 * @param {integer} count The number of characters to delete. This is clamped
616 * to the column width minus the cursor column.
617 */
618hterm.Screen.prototype.deleteChars = function(count) {
619 var node = this.cursorNode_;
620 var offset = this.cursorOffset_;
621
rgindaa19afe22012-01-25 15:40:22 -0800622 if (node.textContent.length <= offset && !node.nextSibling) {
623 // There's nothing after this node/offset to delete, buh bye.
624 return;
625 }
626
rginda8ba33642011-12-14 12:31:31 -0800627 while (node && count) {
628 var startLength = node.textContent.length;
629
630 node.textContent = node.textContent.substr(0, offset) +
631 node.textContent.substr(offset + count);
632
633 var endLength = node.textContent.length;
634 count -= startLength - endLength;
635
636 if (endLength == 0 && node != this.cursorNode_) {
637 var nextNode = node.nextSibling;
638 node.parentNode.removeChild(node);
639 node = nextNode;
640 } else {
641 node = node.nextSibling;
642 }
643
644 offset = 0;
645 }
646};