summaryrefslogtreecommitdiff
path: root/src/static/js/caretPosition.js
blob: bc3fd0076f567ee5f64d424ee798e8c11ce35e3d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// One rep.line(div) can be broken in more than one line in the browser.
// This function is useful to get the caret position of the line as
// is represented by the browser
exports.getPosition = function ()
{
  var rect, line;
  var editor = $('#innerdocbody')[0];
  var range = getSelectionRange();
  var isSelectionInsideTheEditor = range && $(range.endContainer).closest('body')[0].id === 'innerdocbody';

  if(isSelectionInsideTheEditor){
    // when we have the caret in an empty line, e.g. a line with only a <br>,
    // getBoundingClientRect() returns all dimensions value as 0
    var selectionIsInTheBeginningOfLine = range.endOffset > 0;
    if (selectionIsInTheBeginningOfLine) {
      var clonedRange = createSelectionRange(range);
      line = getPositionOfElementOrSelection(clonedRange);
      clonedRange.detach()
    }

    // when there's a <br> or any element that has no height, we can't get
    // the dimension of the element where the caret is
    if(!rect || rect.height === 0){
      var clonedRange = createSelectionRange(range);

      // as we can't get the element height, we create a text node to get the dimensions
      // on the position
      var shadowCaret = $(document.createTextNode("|"));
      clonedRange.insertNode(shadowCaret[0]);
      clonedRange.selectNode(shadowCaret[0]);

      line = getPositionOfElementOrSelection(clonedRange);
      clonedRange.detach()
      shadowCaret.remove();
    }
  }
  return line;
}

var createSelectionRange = function (range) {
  clonedRange = range.cloneRange();

  // we set the selection start and end to avoid error when user selects a text bigger than
  // the viewport height and uses the arrow keys to expand the selection. In this particular
  // case is necessary to know where the selections ends because both edges of the selection
  // is out of the viewport but we only use the end of it to calculate if it needs to scroll
  clonedRange.setStart(range.endContainer, range.endOffset);
  clonedRange.setEnd(range.endContainer, range.endOffset);
  return clonedRange;
}

var getPositionOfRepLineAtOffset = function (node, offset) {
  // it is not a text node, so we cannot make a selection
  if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
    return getPositionOfElementOrSelection(node);
  }

  while (node.length === 0 && node.nextSibling) {
    node = node.nextSibling;
  }

  var newRange = new Range();
  newRange.setStart(node, offset);
  newRange.setEnd(node, offset);
  var linePosition = getPositionOfElementOrSelection(newRange);
  newRange.detach(); // performance sake
  return linePosition;
}

function getPositionOfElementOrSelection(element) {
  var rect = element.getBoundingClientRect();
  var linePosition = {
    bottom: rect.bottom,
    height: rect.height,
    top: rect.top
  }
  return linePosition;
}

// here we have two possibilities:
// [1] the line before the caret line has the same type, so both of them has the same margin, padding
// height, etc. So, we can use the caret line to make calculation necessary to know where is the top
// of the previous line
// [2] the line before is part of another rep line. It's possible this line has different margins
// height. So we have to get the exactly position of the line
exports.getPositionTopOfPreviousBrowserLine = function(caretLinePosition, rep) {
  var previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
  var isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);

  // the caret is in the beginning of a rep line, so the previous browser line
  // is the last line browser line of the a rep line
  if (isCaretLineFirstBrowserLine) { //[2]
    var lineBeforeCaretLine = rep.selStart[0] - 1;
    var firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep);
    var linePosition = getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
    previousLineTop = linePosition.top;
  }
  return previousLineTop;
}

function caretLineIsFirstBrowserLine(caretLineTop, rep)
{
  var caretRepLine = rep.selStart[0];
  var lineNode = rep.lines.atIndex(caretRepLine).lineNode;
  var firstRootNode = getFirstRootChildNode(lineNode);

  // to get the position of the node we get the position of the first char
  var positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
  return positionOfFirstRootNode.top === caretLineTop;
}

// find the first root node, usually it is a text node
function getFirstRootChildNode(node)
{
  if(!node.firstChild){
    return node;
  }else{
    return getFirstRootChildNode(node.firstChild);
  }

}

function getPreviousVisibleLine(line, rep)
{
  if (line < 0) {
    return 0;
  }else if (isLineVisible(line, rep)) {
    return line;
  }else{
    return getPreviousVisibleLine(line - 1, rep);
  }
}

function getDimensionOfLastBrowserLineOfRepLine(line, rep)
{
  var lineNode = rep.lines.atIndex(line).lineNode;
  var lastRootChildNode = getLastRootChildNode(lineNode);

  // we get the position of the line in the last char of it
  var lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
  return lastRootChildNodePosition;
}

function getLastRootChildNode(node)
{
  if(!node.lastChild){
    return {
      node: node,
      length: node.length
    };
  }else{
    return getLastRootChildNode(node.lastChild);
  }
}

// here we have two possibilities:
// [1] The next line is part of the same rep line of the caret line, so we have the same dimensions.
// So, we can use the caret line to calculate the bottom of the line.
// [2] the next line is part of another rep line. It's possible this line has different dimensions, so we
// have to get the exactly dimension of it
exports.getBottomOfNextBrowserLine = function(caretLinePosition, rep)
{
  var nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; //[1]
  var isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);

  // the caret is at the end of a rep line, so we can get the next browser line dimension
  // using the position of the first char of the next rep line
  if(isCaretLineLastBrowserLine){ //[2]
    var nextLineAfterCaretLine = rep.selStart[0] + 1;
    var firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep);
    var linePosition = getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
    nextLineBottom = linePosition.bottom;
  }
  return nextLineBottom;
}

function caretLineIsLastBrowserLineOfRepLine(caretLineTop, rep)
{
  var caretRepLine = rep.selStart[0];
  var lineNode = rep.lines.atIndex(caretRepLine).lineNode;
  var lastRootChildNode = getLastRootChildNode(lineNode);

  // we take a rep line and get the position of the last char of it
  var lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
  return lastRootChildNodePosition.top === caretLineTop;
}

function getPreviousVisibleLine(line, rep)
{
  var firstLineOfPad = 0;
  if (line <= firstLineOfPad) {
    return firstLineOfPad;
  }else if (isLineVisible(line,rep)) {
    return line;
  }else{
    return getPreviousVisibleLine(line - 1, rep);
  }
}
exports.getPreviousVisibleLine = getPreviousVisibleLine;

function getNextVisibleLine(line, rep)
{
  var lastLineOfThePad = rep.lines.length() - 1;
  if (line >= lastLineOfThePad) {
    return lastLineOfThePad;
  }else if (isLineVisible(line,rep)) {
    return line;
  }else{
    return getNextVisibleLine(line + 1, rep);
  }
}
exports.getNextVisibleLine = getNextVisibleLine;

function isLineVisible(line, rep)
{
  return rep.lines.atIndex(line).lineNode.offsetHeight > 0;
}

function getDimensionOfFirstBrowserLineOfRepLine(line, rep)
{
  var lineNode = rep.lines.atIndex(line).lineNode;
  var firstRootChildNode = getFirstRootChildNode(lineNode);

  // we can get the position of the line, getting the position of the first char of the rep line
  var firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
  return firstRootChildNodePosition;
}

function getSelectionRange()
{
  var selection;
  if (!window.getSelection) {
   return;
  }
  selection = window.getSelection();
  if (selection.rangeCount > 0) {
   return selection.getRangeAt(0);
  } else {
   return null;
  }
}