mirror of
				https://github.com/ether/etherpad-lite.git
				synced 2025-11-04 10:11:33 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			389 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			389 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * This code is mostly from the old Etherpad. Please help us to comment this code. 
 | 
						|
 * This helps other people to understand this code better and helps them to improve it.
 | 
						|
 * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Copyright 2009 Google Inc.
 | 
						|
 *
 | 
						|
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
 * you may not use this file except in compliance with the License.
 | 
						|
 * You may obtain a copy of the License at
 | 
						|
 *
 | 
						|
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 *
 | 
						|
 * Unless required by applicable law or agreed to in writing, software
 | 
						|
 * distributed under the License is distributed on an "AS-IS" BASIS,
 | 
						|
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
 * See the License for the specific language governing permissions and
 | 
						|
 * limitations under the License.
 | 
						|
 */
 | 
						|
 | 
						|
function makeVirtualLineView(lineNode)
 | 
						|
{
 | 
						|
 | 
						|
  // how much to jump forward or backward at once in a charSeeker before
 | 
						|
  // constructing a DOM node and checking the coordinates (which takes a
 | 
						|
  // significant fraction of a millisecond).  From the
 | 
						|
  // coordinates and the approximate line height we can estimate how
 | 
						|
  // many lines we have moved.  We risk being off if the number of lines
 | 
						|
  // we move is on the order of the line height in pixels.  Fortunately,
 | 
						|
  // when the user boosts the font-size they increase both.
 | 
						|
  var maxCharIncrement = 20;
 | 
						|
  var seekerAtEnd = null;
 | 
						|
 | 
						|
  function getNumChars()
 | 
						|
  {
 | 
						|
    return lineNode.textContent.length;
 | 
						|
  }
 | 
						|
 | 
						|
  function getNumVirtualLines()
 | 
						|
  {
 | 
						|
    if (!seekerAtEnd)
 | 
						|
    {
 | 
						|
      var seeker = makeCharSeeker();
 | 
						|
      seeker.forwardByWhile(maxCharIncrement);
 | 
						|
      seekerAtEnd = seeker;
 | 
						|
    }
 | 
						|
    return seekerAtEnd.getVirtualLine() + 1;
 | 
						|
  }
 | 
						|
 | 
						|
  function getVLineAndOffsetForChar(lineChar)
 | 
						|
  {
 | 
						|
    var seeker = makeCharSeeker();
 | 
						|
    seeker.forwardByWhile(maxCharIncrement, null, lineChar);
 | 
						|
    var theLine = seeker.getVirtualLine();
 | 
						|
    seeker.backwardByWhile(8, function()
 | 
						|
    {
 | 
						|
      return seeker.getVirtualLine() == theLine;
 | 
						|
    });
 | 
						|
    seeker.forwardByWhile(1, function()
 | 
						|
    {
 | 
						|
      return seeker.getVirtualLine() != theLine;
 | 
						|
    });
 | 
						|
    var lineStartChar = seeker.getOffset();
 | 
						|
    return {
 | 
						|
      vline: theLine,
 | 
						|
      offset: (lineChar - lineStartChar)
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  function getCharForVLineAndOffset(vline, offset)
 | 
						|
  {
 | 
						|
    // returns revised vline and offset as well as absolute char index within line.
 | 
						|
    // if offset is beyond end of line, for example, will give new offset at end of line.
 | 
						|
    var seeker = makeCharSeeker();
 | 
						|
    // go to start of line
 | 
						|
    seeker.binarySearch(function()
 | 
						|
    {
 | 
						|
      return seeker.getVirtualLine() >= vline;
 | 
						|
    });
 | 
						|
    var lineStart = seeker.getOffset();
 | 
						|
    var theLine = seeker.getVirtualLine();
 | 
						|
    // go to offset, overshooting the virtual line only if offset is too large for it
 | 
						|
    seeker.forwardByWhile(maxCharIncrement, null, lineStart + offset);
 | 
						|
    // get back into line
 | 
						|
    seeker.backwardByWhile(1, function()
 | 
						|
    {
 | 
						|
      return seeker.getVirtualLine() != theLine;
 | 
						|
    }, lineStart);
 | 
						|
    var lineChar = seeker.getOffset();
 | 
						|
    var theOffset = lineChar - lineStart;
 | 
						|
    // handle case of last virtual line; should be able to be at end of it
 | 
						|
    if (theOffset < offset && theLine == (getNumVirtualLines() - 1))
 | 
						|
    {
 | 
						|
      var lineLen = getNumChars();
 | 
						|
      theOffset += lineLen - lineChar;
 | 
						|
      lineChar = lineLen;
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      vline: theLine,
 | 
						|
      offset: theOffset,
 | 
						|
      lineChar: lineChar
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  return {
 | 
						|
    getNumVirtualLines: getNumVirtualLines,
 | 
						|
    getVLineAndOffsetForChar: getVLineAndOffsetForChar,
 | 
						|
    getCharForVLineAndOffset: getCharForVLineAndOffset,
 | 
						|
    makeCharSeeker: function()
 | 
						|
    {
 | 
						|
      return makeCharSeeker();
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  function deepFirstChildTextNode(nd)
 | 
						|
  {
 | 
						|
    nd = nd.firstChild;
 | 
						|
    while (nd && nd.firstChild) nd = nd.firstChild;
 | 
						|
    if (nd.data) return nd;
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  function makeCharSeeker( /*lineNode*/ )
 | 
						|
  {
 | 
						|
 | 
						|
    function charCoords(tnode, i)
 | 
						|
    {
 | 
						|
      var container = tnode.parentNode;
 | 
						|
 | 
						|
      // treat space specially; a space at the end of a virtual line
 | 
						|
      // will have weird coordinates
 | 
						|
      var isSpace = (tnode.nodeValue.charAt(i) === " ");
 | 
						|
      if (isSpace)
 | 
						|
      {
 | 
						|
        if (i == 0)
 | 
						|
        {
 | 
						|
          if (container.previousSibling && deepFirstChildTextNode(container.previousSibling))
 | 
						|
          {
 | 
						|
            tnode = deepFirstChildTextNode(container.previousSibling);
 | 
						|
            i = tnode.length - 1;
 | 
						|
            container = tnode.parentNode;
 | 
						|
          }
 | 
						|
          else
 | 
						|
          {
 | 
						|
            return {
 | 
						|
              top: container.offsetTop,
 | 
						|
              left: container.offsetLeft
 | 
						|
            };
 | 
						|
          }
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
          i--; // use previous char
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
 | 
						|
      var charWrapper = document.createElement("SPAN");
 | 
						|
 | 
						|
      // wrap the character
 | 
						|
      var tnodeText = tnode.nodeValue;
 | 
						|
      var frag = document.createDocumentFragment();
 | 
						|
      frag.appendChild(document.createTextNode(tnodeText.substring(0, i)));
 | 
						|
      charWrapper.appendChild(document.createTextNode(tnodeText.substr(i, 1)));
 | 
						|
      frag.appendChild(charWrapper);
 | 
						|
      frag.appendChild(document.createTextNode(tnodeText.substring(i + 1)));
 | 
						|
      container.replaceChild(frag, tnode);
 | 
						|
 | 
						|
      var result = {
 | 
						|
        top: charWrapper.offsetTop,
 | 
						|
        left: charWrapper.offsetLeft + (isSpace ? charWrapper.offsetWidth : 0),
 | 
						|
        height: charWrapper.offsetHeight
 | 
						|
      };
 | 
						|
 | 
						|
      while (container.firstChild) container.removeChild(container.firstChild);
 | 
						|
      container.appendChild(tnode);
 | 
						|
 | 
						|
      return result;
 | 
						|
    }
 | 
						|
 | 
						|
    var lineText = lineNode.textContent;
 | 
						|
    var lineLength = lineText.length;
 | 
						|
 | 
						|
    var curNode = null;
 | 
						|
    var curChar = 0;
 | 
						|
    var curCharWithinNode = 0
 | 
						|
    var curTop;
 | 
						|
    var curLeft;
 | 
						|
    var approxLineHeight;
 | 
						|
    var whichLine = 0;
 | 
						|
 | 
						|
    function nextNode()
 | 
						|
    {
 | 
						|
      var n = curNode;
 | 
						|
      if (!n) n = lineNode.firstChild;
 | 
						|
      else n = n.nextSibling;
 | 
						|
      while (n && !deepFirstChildTextNode(n))
 | 
						|
      {
 | 
						|
        n = n.nextSibling;
 | 
						|
      }
 | 
						|
      return n;
 | 
						|
    }
 | 
						|
 | 
						|
    function prevNode()
 | 
						|
    {
 | 
						|
      var n = curNode;
 | 
						|
      if (!n) n = lineNode.lastChild;
 | 
						|
      else n = n.previousSibling;
 | 
						|
      while (n && !deepFirstChildTextNode(n))
 | 
						|
      {
 | 
						|
        n = n.previousSibling;
 | 
						|
      }
 | 
						|
      return n;
 | 
						|
    }
 | 
						|
 | 
						|
    var seeker;
 | 
						|
    if (lineLength > 0)
 | 
						|
    {
 | 
						|
      curNode = nextNode();
 | 
						|
      var firstCharData = charCoords(deepFirstChildTextNode(curNode), 0);
 | 
						|
      approxLineHeight = firstCharData.height;
 | 
						|
      curTop = firstCharData.top;
 | 
						|
      curLeft = firstCharData.left;
 | 
						|
 | 
						|
      function updateCharData(tnode, i)
 | 
						|
      {
 | 
						|
        var coords = charCoords(tnode, i);
 | 
						|
        whichLine += Math.round((coords.top - curTop) / approxLineHeight);
 | 
						|
        curTop = coords.top;
 | 
						|
        curLeft = coords.left;
 | 
						|
      }
 | 
						|
 | 
						|
      seeker = {
 | 
						|
        forward: function(numChars)
 | 
						|
        {
 | 
						|
          var oldChar = curChar;
 | 
						|
          var newChar = curChar + numChars;
 | 
						|
          if (newChar > (lineLength - 1)) newChar = lineLength - 1;
 | 
						|
          while (curChar < newChar)
 | 
						|
          {
 | 
						|
            var curNodeLength = deepFirstChildTextNode(curNode).length;
 | 
						|
            var toGo = curNodeLength - curCharWithinNode;
 | 
						|
            if (curChar + toGo > newChar || !nextNode())
 | 
						|
            {
 | 
						|
              // going to next node would be too far
 | 
						|
              var n = newChar - curChar;
 | 
						|
              if (n >= toGo) n = toGo - 1;
 | 
						|
              curChar += n;
 | 
						|
              curCharWithinNode += n;
 | 
						|
              break;
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
              // go to next node
 | 
						|
              curChar += toGo;
 | 
						|
              curCharWithinNode = 0;
 | 
						|
              curNode = nextNode();
 | 
						|
            }
 | 
						|
          }
 | 
						|
          updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
 | 
						|
          return curChar - oldChar;
 | 
						|
        },
 | 
						|
        backward: function(numChars)
 | 
						|
        {
 | 
						|
          var oldChar = curChar;
 | 
						|
          var newChar = curChar - numChars;
 | 
						|
          if (newChar < 0) newChar = 0;
 | 
						|
          while (curChar > newChar)
 | 
						|
          {
 | 
						|
            if (curChar - curCharWithinNode <= newChar || !prevNode())
 | 
						|
            {
 | 
						|
              // going to prev node would be too far
 | 
						|
              var n = curChar - newChar;
 | 
						|
              if (n > curCharWithinNode) n = curCharWithinNode;
 | 
						|
              curChar -= n;
 | 
						|
              curCharWithinNode -= n;
 | 
						|
              break;
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
              // go to prev node
 | 
						|
              curChar -= curCharWithinNode + 1;
 | 
						|
              curNode = prevNode();
 | 
						|
              curCharWithinNode = deepFirstChildTextNode(curNode).length - 1;
 | 
						|
            }
 | 
						|
          }
 | 
						|
          updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
 | 
						|
          return oldChar - curChar;
 | 
						|
        },
 | 
						|
        getVirtualLine: function()
 | 
						|
        {
 | 
						|
          return whichLine;
 | 
						|
        },
 | 
						|
        getLeftCoord: function()
 | 
						|
        {
 | 
						|
          return curLeft;
 | 
						|
        }
 | 
						|
      };
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      curLeft = lineNode.offsetLeft;
 | 
						|
      seeker = {
 | 
						|
        forward: function(numChars)
 | 
						|
        {
 | 
						|
          return 0;
 | 
						|
        },
 | 
						|
        backward: function(numChars)
 | 
						|
        {
 | 
						|
          return 0;
 | 
						|
        },
 | 
						|
        getVirtualLine: function()
 | 
						|
        {
 | 
						|
          return 0;
 | 
						|
        },
 | 
						|
        getLeftCoord: function()
 | 
						|
        {
 | 
						|
          return curLeft;
 | 
						|
        }
 | 
						|
      };
 | 
						|
    }
 | 
						|
    seeker.getOffset = function()
 | 
						|
    {
 | 
						|
      return curChar;
 | 
						|
    };
 | 
						|
    seeker.getLineLength = function()
 | 
						|
    {
 | 
						|
      return lineLength;
 | 
						|
    };
 | 
						|
    seeker.toString = function()
 | 
						|
    {
 | 
						|
      return "seeker[curChar: " + curChar + "(" + lineText.charAt(curChar) + "), left: " + seeker.getLeftCoord() + ", vline: " + seeker.getVirtualLine() + "]";
 | 
						|
    };
 | 
						|
 | 
						|
    function moveByWhile(isBackward, amount, optCondFunc, optCharLimit)
 | 
						|
    {
 | 
						|
      var charsMovedLast = null;
 | 
						|
      var hasCondFunc = ((typeof optCondFunc) == "function");
 | 
						|
      var condFunc = optCondFunc;
 | 
						|
      var hasCharLimit = ((typeof optCharLimit) == "number");
 | 
						|
      var charLimit = optCharLimit;
 | 
						|
      while (charsMovedLast !== 0 && ((!hasCondFunc) || condFunc()))
 | 
						|
      {
 | 
						|
        var toMove = amount;
 | 
						|
        if (hasCharLimit)
 | 
						|
        {
 | 
						|
          var untilLimit = (isBackward ? curChar - charLimit : charLimit - curChar);
 | 
						|
          if (untilLimit < toMove) toMove = untilLimit;
 | 
						|
        }
 | 
						|
        if (toMove < 0) break;
 | 
						|
        charsMovedLast = (isBackward ? seeker.backward(toMove) : seeker.forward(toMove));
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    seeker.forwardByWhile = function(amount, optCondFunc, optCharLimit)
 | 
						|
    {
 | 
						|
      moveByWhile(false, amount, optCondFunc, optCharLimit);
 | 
						|
    }
 | 
						|
    seeker.backwardByWhile = function(amount, optCondFunc, optCharLimit)
 | 
						|
    {
 | 
						|
      moveByWhile(true, amount, optCondFunc, optCharLimit);
 | 
						|
    }
 | 
						|
    seeker.binarySearch = function(condFunc)
 | 
						|
    {
 | 
						|
      // returns index of boundary between false chars and true chars;
 | 
						|
      // positions seeker at first true char, or else last char
 | 
						|
      var trueFunc = condFunc;
 | 
						|
      var falseFunc = function()
 | 
						|
        {
 | 
						|
          return !condFunc();
 | 
						|
          };
 | 
						|
      seeker.forwardByWhile(20, falseFunc);
 | 
						|
      seeker.backwardByWhile(20, trueFunc);
 | 
						|
      seeker.forwardByWhile(10, falseFunc);
 | 
						|
      seeker.backwardByWhile(5, trueFunc);
 | 
						|
      seeker.forwardByWhile(1, falseFunc);
 | 
						|
      return seeker.getOffset() + (condFunc() ? 0 : 1);
 | 
						|
    }
 | 
						|
 | 
						|
    return seeker;
 | 
						|
  }
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
exports.makeVirtualLineView = makeVirtualLineView;
 |