liquid_cms 0.3.0.1 → 0.3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG.rdoc +5 -1
  2. data/Gemfile.lock +1 -1
  3. data/README.rdoc +5 -1
  4. data/app/helpers/cms/common_helper.rb +1 -0
  5. data/app/views/cms/pages/_page.html.erb +2 -1
  6. data/app/views/layouts/cms.html.erb +2 -1
  7. data/lib/generators/liquid_cms/templates/public/cms/codemirror/LICENSE +2 -2
  8. data/lib/generators/liquid_cms/templates/public/cms/codemirror/css/csscolors.css +12 -8
  9. data/lib/generators/liquid_cms/templates/public/cms/codemirror/css/docs.css +123 -29
  10. data/lib/generators/liquid_cms/templates/public/cms/codemirror/csstest.html +1 -1
  11. data/lib/generators/liquid_cms/templates/public/cms/codemirror/htmltest.html +1 -1
  12. data/lib/generators/liquid_cms/templates/public/cms/codemirror/index.html +232 -179
  13. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/codemirror.js +211 -65
  14. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/editor.js +360 -194
  15. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/mirrorframe.js +1 -1
  16. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/parsecss.js +11 -7
  17. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/parsejavascript.js +14 -5
  18. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/parsesparql.js +1 -1
  19. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/select.js +140 -87
  20. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/stringstream.js +5 -0
  21. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/tokenizejavascript.js +1 -1
  22. data/lib/generators/liquid_cms/templates/public/cms/codemirror/js/undo.js +7 -7
  23. data/lib/generators/liquid_cms/templates/public/cms/codemirror/manual.html +148 -52
  24. data/lib/generators/liquid_cms/templates/public/cms/codemirror/story.html +631 -614
  25. data/lib/generators/liquid_cms/templates/public/cms/stylesheets/styles.css +7 -7
  26. data/lib/liquid_cms/version.rb +1 -1
  27. metadata +4 -26
  28. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/lua/LICENSE +0 -32
  29. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/lua/css/luacolors.css +0 -63
  30. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/lua/index.html +0 -68
  31. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/lua/js/parselua.js +0 -253
  32. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/php/LICENSE +0 -37
  33. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/php/css/phpcolors.css +0 -114
  34. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/php/index.html +0 -292
  35. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/php/js/parsephp.js +0 -371
  36. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/php/js/parsephphtmlmixed.js +0 -90
  37. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/php/js/tokenizephp.js +0 -1006
  38. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/plsql/LICENSE +0 -22
  39. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/plsql/css/plsqlcolors.css +0 -57
  40. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/plsql/index.html +0 -67
  41. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/plsql/js/parseplsql.js +0 -233
  42. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/python/LICENSE +0 -32
  43. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/python/css/pythoncolors.css +0 -58
  44. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/python/index.html +0 -141
  45. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/python/js/parsepython.js +0 -542
  46. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/sql/LICENSE +0 -22
  47. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/sql/css/sqlcolors.css +0 -57
  48. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/sql/index.html +0 -56
  49. data/lib/generators/liquid_cms/templates/public/cms/codemirror/contrib/sql/js/parsesql.js +0 -211
@@ -8,6 +8,14 @@ var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test
8
8
  var webkit = /AppleWebKit/.test(navigator.userAgent);
9
9
  var safari = /Apple Computers, Inc/.test(navigator.vendor);
10
10
  var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent);
11
+ var mac = /Mac/.test(navigator.platform);
12
+
13
+ // TODO this is related to the backspace-at-end-of-line bug. Remove
14
+ // this if Opera gets their act together, make the version check more
15
+ // broad if they don't.
16
+ var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
17
+ // TODO remove this once WebKit 533 becomes less common.
18
+ var slowWebkit = /AppleWebKit\/533/.test(navigator.userAgent);
11
19
 
12
20
  // Make sure a string does not contain two consecutive 'collapseable'
13
21
  // whitespace characters.
@@ -15,7 +23,7 @@ function makeWhiteSpace(n) {
15
23
  var buffer = [], nb = true;
16
24
  for (; n > 0; n--) {
17
25
  buffer.push((nb || n == 1) ? nbsp : " ");
18
- nb = !nb;
26
+ nb ^= true;
19
27
  }
20
28
  return buffer.join("");
21
29
  }
@@ -24,22 +32,22 @@ function makeWhiteSpace(n) {
24
32
  // by the browser, but will not break text-wrapping either.
25
33
  function fixSpaces(string) {
26
34
  if (string.charAt(0) == " ") string = nbsp + string.slice(1);
27
- return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);})
35
+ return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
28
36
  .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
29
37
  }
30
38
 
31
39
  function cleanText(text) {
32
- return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
40
+ return text.replace(/\u00a0/g, " ");
33
41
  }
34
42
 
35
43
  // Create a SPAN node with the expected properties for document part
36
44
  // spans.
37
- function makePartSpan(value, doc) {
45
+ function makePartSpan(value) {
38
46
  var text = value;
39
47
  if (value.nodeType == 3) text = value.nodeValue;
40
- else value = doc.createTextNode(text);
48
+ else value = document.createTextNode(text);
41
49
 
42
- var span = doc.createElement("SPAN");
50
+ var span = document.createElement("SPAN");
43
51
  span.isPart = true;
44
52
  span.appendChild(value);
45
53
  span.currentText = text;
@@ -58,8 +66,11 @@ function makePartSpan(value, doc) {
58
66
  var webkitLastLineHack = webkit ?
59
67
  function(container) {
60
68
  var last = container.lastChild;
61
- if (!last || !last.isPart || last.textContent != "\u200b")
62
- container.appendChild(makePartSpan("\u200b", container.ownerDocument));
69
+ if (!last || !last.hackBR) {
70
+ var br = document.createElement("BR");
71
+ br.hackBR = true;
72
+ container.appendChild(br);
73
+ }
63
74
  } : function() {};
64
75
 
65
76
  var Editor = (function(){
@@ -75,13 +86,12 @@ var Editor = (function(){
75
86
  // Helper function for traverseDOM. Flattens an arbitrary DOM node
76
87
  // into an array of textnodes and <br> tags.
77
88
  function simplifyDOM(root, atEnd) {
78
- var doc = root.ownerDocument;
79
89
  var result = [];
80
90
  var leaving = true;
81
91
 
82
92
  function simplifyNode(node, top) {
83
93
  if (node.nodeType == 3) {
84
- var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
94
+ var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " "));
85
95
  if (text.length) leaving = false;
86
96
  result.push(node);
87
97
  }
@@ -90,11 +100,11 @@ var Editor = (function(){
90
100
  result.push(node);
91
101
  }
92
102
  else {
93
- forEach(node.childNodes, simplifyNode);
103
+ for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
94
104
  if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
95
105
  leaving = true;
96
106
  if (!atEnd || !top)
97
- result.push(doc.createElement("BR"));
107
+ result.push(document.createElement("BR"));
98
108
  }
99
109
  }
100
110
  }
@@ -108,14 +118,7 @@ var Editor = (function(){
108
118
  // the nodes. It makes sure that all nodes up to and including the
109
119
  // one whose text is being yielded have been 'normalized' to be just
110
120
  // <span> and <br> elements.
111
- // See the story.html file for some short remarks about the use of
112
- // continuation-passing style in this iterator.
113
121
  function traverseDOM(start){
114
- function _yield(value, c){cc = c; return value;}
115
- function push(fun, arg, c){return function(){return fun(arg, c);};}
116
- function stop(){cc = stop; throw StopIteration;};
117
- var cc = push(scanNode, start, stop);
118
- var owner = start.ownerDocument;
119
122
  var nodeQueue = [];
120
123
 
121
124
  // Create a function that can be used to insert nodes after the
@@ -144,13 +147,13 @@ var Editor = (function(){
144
147
  var text = "\n";
145
148
  if (part.nodeType == 3) {
146
149
  select.snapshotChanged();
147
- part = makePartSpan(part, owner);
150
+ part = makePartSpan(part);
148
151
  text = part.currentText;
149
152
  afterBR = false;
150
153
  }
151
154
  else {
152
155
  if (afterBR && window.opera)
153
- point(makePartSpan("", owner));
156
+ point(makePartSpan(""));
154
157
  afterBR = true;
155
158
  }
156
159
  part.dirty = true;
@@ -160,14 +163,13 @@ var Editor = (function(){
160
163
  }
161
164
 
162
165
  // Extract the text and newlines from a DOM node, insert them into
163
- // the document, and yield the textual content. Used to replace
166
+ // the document, and return the textual content. Used to replace
164
167
  // non-normalized nodes.
165
- function writeNode(node, c, end) {
166
- var toYield = [];
167
- forEach(simplifyDOM(node, end), function(part) {
168
- toYield.push(insertPart(part));
169
- });
170
- return _yield(toYield.join(""), c);
168
+ function writeNode(node, end) {
169
+ var simplified = simplifyDOM(node, end);
170
+ for (var i = 0; i < simplified.length; i++)
171
+ simplified[i] = insertPart(simplified[i]);
172
+ return simplified.join("");
171
173
  }
172
174
 
173
175
  // Check whether a node is a normalized <span> element.
@@ -179,38 +181,36 @@ var Editor = (function(){
179
181
  return false;
180
182
  }
181
183
 
182
- // Handle a node. Add its successor to the continuation if there
183
- // is one, find out whether the node is normalized. If it is,
184
- // yield its content, otherwise, normalize it (writeNode will take
185
- // care of yielding).
186
- function scanNode(node, c){
187
- if (node.nextSibling)
188
- c = push(scanNode, node.nextSibling, c);
184
+ // Advance to next node, return string for current node.
185
+ function next() {
186
+ if (!start) throw StopIteration;
187
+ var node = start;
188
+ start = node.nextSibling;
189
189
 
190
190
  if (partNode(node)){
191
191
  nodeQueue.push(node);
192
192
  afterBR = false;
193
- return _yield(node.currentText, c);
193
+ return node.currentText;
194
194
  }
195
195
  else if (isBR(node)) {
196
196
  if (afterBR && window.opera)
197
- node.parentNode.insertBefore(makePartSpan("", owner), node);
197
+ node.parentNode.insertBefore(makePartSpan(""), node);
198
198
  nodeQueue.push(node);
199
199
  afterBR = true;
200
- return _yield("\n", c);
200
+ return "\n";
201
201
  }
202
202
  else {
203
203
  var end = !node.nextSibling;
204
204
  point = pointAt(node);
205
205
  removeElement(node);
206
- return writeNode(node, c, end);
206
+ return writeNode(node, end);
207
207
  }
208
208
  }
209
209
 
210
210
  // MochiKit iterators are objects with a next function that
211
211
  // returns the next value or throws StopIteration when there are
212
212
  // no more values.
213
- return {next: function(){return cc();}, nodes: nodeQueue};
213
+ return {next: next, nodes: nodeQueue};
214
214
  }
215
215
 
216
216
  // Determine the text size of a processed node.
@@ -240,128 +240,139 @@ var Editor = (function(){
240
240
  // indicating whether anything was found, and can be called again to
241
241
  // skip to the next find. Use the select and replace methods to
242
242
  // actually do something with the found locations.
243
- function SearchCursor(editor, string, fromCursor, caseFold) {
243
+ function SearchCursor(editor, string, from, caseFold) {
244
244
  this.editor = editor;
245
- this.caseFold = caseFold;
246
- if (caseFold) string = string.toLowerCase();
247
245
  this.history = editor.history;
248
246
  this.history.commit();
249
-
250
- // Are we currently at an occurrence of the search string?
247
+ this.valid = !!string;
251
248
  this.atOccurrence = false;
252
- // The object stores a set of nodes coming after its current
253
- // position, so that when the current point is taken out of the
254
- // DOM tree, we can still try to continue.
255
- this.fallbackSize = 15;
256
- var cursor;
257
- // Start from the cursor when specified and a cursor can be found.
258
- if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
259
- this.line = cursor.node;
260
- this.offset = cursor.offset;
249
+ if (caseFold == undefined) caseFold = string == string.toLowerCase();
250
+
251
+ function getText(node){
252
+ var line = cleanText(editor.history.textAfter(node));
253
+ return (caseFold ? line.toLowerCase() : line);
254
+ }
255
+
256
+ var topPos = {node: null, offset: 0};
257
+ if (from && typeof from == "object" && typeof from.character == "number") {
258
+ editor.checkLine(from.line);
259
+ var pos = {node: from.line, offset: from.character};
260
+ this.pos = {from: pos, to: pos};
261
+ }
262
+ else if (from) {
263
+ this.pos = {from: select.cursorPos(editor.container, true) || topPos,
264
+ to: select.cursorPos(editor.container, false) || topPos};
261
265
  }
262
266
  else {
263
- this.line = null;
264
- this.offset = 0;
267
+ this.pos = {from: topPos, to: topPos};
265
268
  }
266
- this.valid = !!string;
267
269
 
270
+ if (caseFold) string = string.toLowerCase();
268
271
  // Create a matcher function based on the kind of string we have.
269
- var target = string.split("\n"), self = this;
272
+ var target = string.split("\n");
270
273
  this.matches = (target.length == 1) ?
271
274
  // For one-line strings, searching can be done simply by calling
272
- // indexOf on the current line.
273
- function() {
274
- var line = cleanText(self.history.textAfter(self.line).slice(self.offset));
275
- var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string);
276
- if (match > -1)
277
- return {from: {node: self.line, offset: self.offset + match},
278
- to: {node: self.line, offset: self.offset + match + string.length}};
275
+ // indexOf or lastIndexOf on the current line.
276
+ function(reverse, node, offset) {
277
+ var line = getText(node), len = string.length, match;
278
+ if (reverse ? (offset >= len && (match = line.lastIndexOf(string, offset - len)) != -1)
279
+ : (match = line.indexOf(string, offset)) != -1)
280
+ return {from: {node: node, offset: match},
281
+ to: {node: node, offset: match + len}};
279
282
  } :
280
283
  // Multi-line strings require internal iteration over lines, and
281
284
  // some clunky checks to make sure the first match ends at the
282
285
  // end of the line and the last match starts at the start.
283
- function() {
284
- var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
285
- var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]);
286
- if (match == -1 || match != firstLine.length - target[0].length)
287
- return false;
288
- var startOffset = self.offset + match;
289
-
290
- var line = self.history.nodeAfter(self.line);
291
- for (var i = 1; i < target.length - 1; i++) {
292
- var lineText = cleanText(self.history.textAfter(line));
293
- if ((self.caseFold ? lineText.toLowerCase() : lineText) != target[i])
294
- return false;
295
- line = self.history.nodeAfter(line);
286
+ function(reverse, node, offset) {
287
+ var idx = (reverse ? target.length - 1 : 0), match = target[idx], line = getText(node);
288
+ var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match));
289
+ if (reverse ? offsetA >= offset || offsetA != match.length
290
+ : offsetA <= offset || offsetA != line.length - match.length)
291
+ return;
292
+
293
+ var pos = node;
294
+ while (true) {
295
+ if (reverse && !pos) return;
296
+ pos = (reverse ? this.history.nodeBefore(pos) : this.history.nodeAfter(pos) );
297
+ if (!reverse && !pos) return;
298
+
299
+ line = getText(pos);
300
+ match = target[reverse ? --idx : ++idx];
301
+
302
+ if (idx > 0 && idx < target.length - 1) {
303
+ if (line != match) return;
304
+ else continue;
305
+ }
306
+ var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length);
307
+ if (reverse ? offsetB != line.length - match.length : offsetB != match.length)
308
+ return;
309
+ return {from: {node: reverse ? pos : node, offset: reverse ? offsetB : offsetA},
310
+ to: {node: reverse ? node : pos, offset: reverse ? offsetA : offsetB}};
296
311
  }
297
-
298
- var lastLine = cleanText(self.history.textAfter(line));
299
- if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0)
300
- return false;
301
-
302
- return {from: {node: self.line, offset: startOffset},
303
- to: {node: line, offset: target[target.length - 1].length}};
304
312
  };
305
313
  }
306
314
 
307
315
  SearchCursor.prototype = {
308
- findNext: function() {
316
+ findNext: function() {return this.find(false);},
317
+ findPrevious: function() {return this.find(true);},
318
+
319
+ find: function(reverse) {
309
320
  if (!this.valid) return false;
310
- this.atOccurrence = false;
311
- var self = this;
312
321
 
313
- // Go back to the start of the document if the current line is
314
- // no longer in the DOM tree.
315
- if (this.line && !this.line.parentNode) {
316
- this.line = null;
317
- this.offset = 0;
322
+ var self = this, pos = reverse ? this.pos.from : this.pos.to,
323
+ node = pos.node, offset = pos.offset;
324
+ // Reset the cursor if the current line is no longer in the DOM tree.
325
+ if (node && !node.parentNode) {
326
+ node = null; offset = 0;
318
327
  }
319
-
320
- // Set the cursor's position one character after the given
321
- // position.
322
- function saveAfter(pos) {
323
- if (self.history.textAfter(pos.node).length > pos.offset) {
324
- self.line = pos.node;
325
- self.offset = pos.offset + 1;
326
- }
327
- else {
328
- self.line = self.history.nodeAfter(pos.node);
329
- self.offset = 0;
330
- }
328
+ function savePosAndFail() {
329
+ var pos = {node: node, offset: offset};
330
+ self.pos = {from: pos, to: pos};
331
+ self.atOccurrence = false;
332
+ return false;
331
333
  }
332
334
 
333
335
  while (true) {
334
- var match = this.matches();
335
- // Found the search string.
336
- if (match) {
337
- this.atOccurrence = match;
338
- saveAfter(match.from);
336
+ if (this.pos = this.matches(reverse, node, offset)) {
337
+ this.atOccurrence = true;
339
338
  return true;
340
339
  }
341
- this.line = this.history.nodeAfter(this.line);
342
- this.offset = 0;
343
- // End of document.
344
- if (!this.line) {
345
- this.valid = false;
346
- return false;
340
+
341
+ if (reverse) {
342
+ if (!node) return savePosAndFail();
343
+ node = this.history.nodeBefore(node);
344
+ offset = this.history.textAfter(node).length;
347
345
  }
346
+ else {
347
+ var next = this.history.nodeAfter(node);
348
+ if (!next) {
349
+ offset = this.history.textAfter(node).length;
350
+ return savePosAndFail();
351
+ }
352
+ node = next;
353
+ offset = 0;
354
+ }
348
355
  }
349
356
  },
350
357
 
351
358
  select: function() {
352
359
  if (this.atOccurrence) {
353
- select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
360
+ select.setCursorPos(this.editor.container, this.pos.from, this.pos.to);
354
361
  select.scrollToCursor(this.editor.container);
355
362
  }
356
363
  },
357
364
 
358
365
  replace: function(string) {
359
366
  if (this.atOccurrence) {
360
- var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
361
- this.line = end.node;
362
- this.offset = end.offset;
367
+ var end = this.editor.replaceRange(this.pos.from, this.pos.to, string);
368
+ this.pos.to = end;
363
369
  this.atOccurrence = false;
364
370
  }
371
+ },
372
+
373
+ position: function() {
374
+ if (this.atOccurrence)
375
+ return {line: this.pos.from.node, character: this.pos.from.offset};
365
376
  }
366
377
  };
367
378
 
@@ -370,10 +381,8 @@ var Editor = (function(){
370
381
  this.options = options;
371
382
  window.indentUnit = options.indentUnit;
372
383
  this.parent = parent;
373
- this.doc = document;
374
- var container = this.container = this.doc.body;
375
- this.win = window;
376
- this.history = new History(container, options.undoDepth, options.undoDelay, this);
384
+ var container = this.container = document.body;
385
+ this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
377
386
  var self = this;
378
387
 
379
388
  if (!Editor.Parser)
@@ -395,13 +404,21 @@ var Editor = (function(){
395
404
  }
396
405
 
397
406
  function setEditable() {
398
- // In IE, designMode frames can not run any scripts, so we use
399
- // contentEditable instead.
407
+ // Use contentEditable instead of designMode on IE, since designMode frames
408
+ // can not run any scripts. It would be nice if we could use contentEditable
409
+ // everywhere, but it is significantly flakier than designMode on every
410
+ // single non-IE browser.
400
411
  if (document.body.contentEditable != undefined && internetExplorer)
401
412
  document.body.contentEditable = "true";
402
413
  else
403
414
  document.designMode = "on";
404
415
 
416
+ // Work around issue where you have to click on the actual
417
+ // body of the document to focus it in IE, making focusing
418
+ // hard when the document is small.
419
+ if (internetExplorer && options.height != "dynamic")
420
+ document.body.style.minHeight = (frameElement.clientHeight - 2 * document.body.offsetTop - 5) + "px";
421
+
405
422
  document.documentElement.style.borderWidth = "0";
406
423
  if (!options.textWrapping)
407
424
  container.style.whiteSpace = "nowrap";
@@ -431,7 +448,7 @@ var Editor = (function(){
431
448
  // workaround for a gecko bug [?] where going forward and then
432
449
  // back again breaks designmode (no more cursor)
433
450
  if (gecko)
434
- addEventHandler(this.win, "pagehide", function(){self.unloaded = true;});
451
+ addEventHandler(window, "pagehide", function(){self.unloaded = true;});
435
452
 
436
453
  addEventHandler(document.body, "paste", function(event) {
437
454
  cursorActivity();
@@ -474,10 +491,13 @@ var Editor = (function(){
474
491
  return "";
475
492
 
476
493
  var accum = [];
477
- select.markSelection(this.win);
494
+ select.markSelection();
478
495
  forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
479
496
  webkitLastLineHack(this.container);
480
497
  select.selectMarked();
498
+ // On webkit, don't count last (empty) line if the webkitLastLineHack BR is present
499
+ if (webkit && this.container.lastChild.hackBR)
500
+ accum.pop();
481
501
  return cleanText(accum.join(""));
482
502
  },
483
503
 
@@ -514,6 +534,16 @@ var Editor = (function(){
514
534
  return startOfLine(line.previousSibling);
515
535
  },
516
536
 
537
+ visibleLineCount: function() {
538
+ var line = this.container.firstChild;
539
+ while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
540
+ if (!line) return false;
541
+ var innerHeight = (window.innerHeight
542
+ || document.documentElement.clientHeight
543
+ || document.body.clientHeight);
544
+ return Math.floor(innerHeight / line.offsetHeight);
545
+ },
546
+
517
547
  selectLines: function(startLine, startOffset, endLine, endOffset) {
518
548
  this.checkLine(startLine);
519
549
  var start = {node: startLine, offset: startOffset}, end = null;
@@ -576,10 +606,10 @@ var Editor = (function(){
576
606
  }
577
607
  }
578
608
 
579
- var lines = asEditorLines(content), doc = this.container.ownerDocument;
609
+ var lines = asEditorLines(content);
580
610
  for (var i = 0; i < lines.length; i++) {
581
- if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
582
- this.container.insertBefore(makePartSpan(lines[i], doc), before);
611
+ if (i > 0) this.container.insertBefore(document.createElement("BR"), before);
612
+ this.container.insertBefore(makePartSpan(lines[i]), before);
583
613
  }
584
614
  this.addDirtyNode(line);
585
615
  this.scheduleHighlight();
@@ -617,31 +647,68 @@ var Editor = (function(){
617
647
  webkitLastLineHack(this.container);
618
648
  },
619
649
 
650
+ cursorCoords: function(start) {
651
+ var sel = select.cursorPos(this.container, start);
652
+ if (!sel) return null;
653
+ var off = sel.offset, node = sel.node, self = this;
654
+ function measureFromNode(node, xOffset) {
655
+ var y = -(document.body.scrollTop || document.documentElement.scrollTop || 0),
656
+ x = -(document.body.scrollLeft || document.documentElement.scrollLeft || 0) + xOffset;
657
+ forEach([node, window.frameElement], function(n) {
658
+ while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
659
+ });
660
+ return {x: x, y: y, yBot: y + node.offsetHeight};
661
+ }
662
+ function withTempNode(text, f) {
663
+ var node = document.createElement("SPAN");
664
+ node.appendChild(document.createTextNode(text));
665
+ try {return f(node);}
666
+ finally {if (node.parentNode) node.parentNode.removeChild(node);}
667
+ }
668
+
669
+ while (off) {
670
+ node = node ? node.nextSibling : this.container.firstChild;
671
+ var txt = nodeText(node);
672
+ if (off < txt.length)
673
+ return withTempNode(txt.substr(0, off), function(tmp) {
674
+ tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
675
+ tmp.className = node.className;
676
+ self.container.appendChild(tmp);
677
+ return measureFromNode(node, tmp.offsetWidth);
678
+ });
679
+ off -= txt.length;
680
+ }
681
+ if (node && isSpan(node))
682
+ return measureFromNode(node, node.offsetWidth);
683
+ else if (node && node.nextSibling && isSpan(node.nextSibling))
684
+ return measureFromNode(node.nextSibling, 0);
685
+ else
686
+ return withTempNode("\u200b", function(tmp) {
687
+ if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
688
+ else self.container.insertBefore(tmp, self.container.firstChild);
689
+ return measureFromNode(tmp, 0);
690
+ });
691
+ },
692
+
620
693
  reroutePasteEvent: function() {
621
694
  if (this.capturingPaste || window.opera) return;
622
695
  this.capturingPaste = true;
623
- var te = parent.document.createElement("TEXTAREA");
624
- te.style.position = "absolute";
625
- te.style.left = "-10000px";
626
- te.style.width = "10px";
627
- te.style.top = nodeTop(frameElement) + "px";
628
- var wrap = window.frameElement.CodeMirror.wrapping;
629
- wrap.parentNode.insertBefore(te, wrap);
696
+ var te = window.frameElement.CodeMirror.textareaHack;
630
697
  parent.focus();
698
+ te.value = "";
631
699
  te.focus();
632
700
 
633
701
  var self = this;
634
702
  this.parent.setTimeout(function() {
635
703
  self.capturingPaste = false;
636
- self.win.focus();
704
+ window.focus();
637
705
  if (self.selectionSnapshot) // IE hack
638
- self.win.select.setBookmark(self.container, self.selectionSnapshot);
706
+ window.select.setBookmark(self.container, self.selectionSnapshot);
639
707
  var text = te.value;
640
708
  if (text) {
641
709
  self.replaceSelection(text);
642
710
  select.scrollToCursor(self.container);
643
711
  }
644
- removeElement(te);
645
712
  }, 10);
646
713
  },
647
714
 
@@ -667,7 +734,7 @@ var Editor = (function(){
667
734
  },
668
735
 
669
736
  reindentSelection: function(direction) {
670
- if (!select.somethingSelected(this.win)) {
737
+ if (!select.somethingSelected()) {
671
738
  this.indentAtCursor(direction);
672
739
  }
673
740
  else {
@@ -684,11 +751,14 @@ var Editor = (function(){
684
751
  },
685
752
  ungrabKeys: function() {
686
753
  this.frozen = "leave";
687
- this.keyFilter = null;
688
754
  },
689
755
 
690
- setParser: function(name) {
756
+ setParser: function(name, parserConfig) {
691
757
  Editor.Parser = window[name];
758
+ parserConfig = parserConfig || this.options.parserConfig;
759
+ if (parserConfig && Editor.Parser.configure)
760
+ Editor.Parser.configure(parserConfig);
761
+
692
762
  if (this.container.firstChild) {
693
763
  forEach(this.container.childNodes, function(n) {
694
764
  if (n.nodeType != 3) n.dirty = true;
@@ -700,8 +770,8 @@ var Editor = (function(){
700
770
 
701
771
  // Intercept enter and tab, and assign their new functions.
702
772
  keyDown: function(event) {
703
- if (this.frozen == "leave") this.frozen = null;
704
- if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) {
773
+ if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
774
+ if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
705
775
  event.stop();
706
776
  this.frozen(event);
707
777
  return;
@@ -722,8 +792,9 @@ var Editor = (function(){
722
792
  this.reparseBuffer();
723
793
  }
724
794
  else {
725
- select.insertNewlineAtCursor(this.win);
726
- this.indentAtCursor();
795
+ select.insertNewlineAtCursor();
796
+ var mode = this.options.enterMode;
797
+ if (mode != "flat") this.indentAtCursor(mode == "keep" ? "keep" : undefined);
727
798
  select.scrollToCursor(this.container);
728
799
  }
729
800
  event.stop();
@@ -742,6 +813,13 @@ var Editor = (function(){
742
813
  else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
743
814
  if (this.end()) event.stop();
744
815
  }
816
+ // Only in Firefox is the default behavior for PgUp/PgDn correct.
817
+ else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
818
+ if (this.pageUp()) event.stop();
819
+ }
820
+ else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn
821
+ if (this.pageDown()) event.stop();
822
+ }
745
823
  else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
746
824
  this.highlightParens(event.shiftKey, true);
747
825
  event.stop();
@@ -770,7 +848,7 @@ var Editor = (function(){
770
848
  this.options.saveFunction();
771
849
  event.stop();
772
850
  }
773
- else if (internetExplorer && code == 86) {
851
+ else if (code == 86 && !mac) { // V
774
852
  this.reroutePasteEvent();
775
853
  }
776
854
  }
@@ -779,20 +857,63 @@ var Editor = (function(){
779
857
  // Check for characters that should re-indent the current line,
780
858
  // and prevent Opera from handling enter and tab anyway.
781
859
  keyPress: function(event) {
782
- var electric = Editor.Parser.electricChars, self = this;
860
+ var electric = this.options.electricChars && Editor.Parser.electricChars, self = this;
783
861
  // Hack for Opera, and Firefox on OS X, in which stopping a
784
862
  // keydown event does not prevent the associated keypress event
785
863
  // from happening, so we have to cancel enter and tab again
786
864
  // here.
787
- if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
865
+ if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
788
866
  event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
789
- (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
867
+ (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
790
868
  event.stop();
869
+ else if (mac && (event.ctrlKey || event.metaKey) && event.character == "v") {
870
+ this.reroutePasteEvent();
871
+ }
791
872
  else if (electric && electric.indexOf(event.character) != -1)
792
873
  this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
793
- else if ((event.character == "v" || event.character == "V")
794
- && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
795
- this.reroutePasteEvent();
874
+ // Work around a bug where pressing backspace at the end of a
875
+ // line, or delete at the start, often causes the cursor to jump
876
+ // to the start of the line in Opera 10.60.
877
+ else if (brokenOpera) {
878
+ if (event.code == 8) { // backspace
879
+ var sel = select.selectionTopNode(this.container), self = this,
880
+ next = sel ? sel.nextSibling : this.container.firstChild;
881
+ if (sel !== false && next && isBR(next))
882
+ this.parent.setTimeout(function(){
883
+ if (select.selectionTopNode(self.container) == next)
884
+ select.focusAfterNode(next.previousSibling, self.container);
885
+ }, 20);
886
+ }
887
+ else if (event.code == 46) { // delete
888
+ var sel = select.selectionTopNode(this.container), self = this;
889
+ if (sel && isBR(sel)) {
890
+ this.parent.setTimeout(function(){
891
+ if (select.selectionTopNode(self.container) != sel)
892
+ select.focusAfterNode(sel, self.container);
893
+ }, 20);
894
+ }
895
+ }
896
+ }
897
+ // In 533.* WebKit versions, when the document is big, typing
898
+ // something at the end of a line causes the browser to do some
899
+ // kind of stupid heavy operation, creating delays of several
900
+ // seconds before the typed characters appear. This very crude
901
+ // hack inserts a temporary zero-width space after the cursor to
902
+ // make it not be at the end of the line.
903
+ else if (slowWebkit) {
904
+ var sel = select.selectionTopNode(this.container),
905
+ next = sel ? sel.nextSibling : this.container.firstChild;
906
+ // Doesn't work on empty lines, for some reason those always
907
+ // trigger the delay.
908
+ if (sel && next && isBR(next) && !isBR(sel)) {
909
+ var cheat = document.createTextNode("\u200b");
910
+ this.container.insertBefore(cheat, next);
911
+ this.parent.setTimeout(function() {
912
+ if (cheat.nodeValue == "\u200b") removeElement(cheat);
913
+ else cheat.nodeValue = cheat.nodeValue.replace("\u200b", "");
914
+ }, 20);
915
+ }
916
+ }
796
917
  },
797
918
 
798
919
  // Mark the node at the cursor dirty when a non-safe key is
@@ -806,32 +927,45 @@ var Editor = (function(){
806
927
  // so that it has an indentation method. Returns the whitespace
807
928
  // element that has been modified or created (if any).
808
929
  indentLineAfter: function(start, direction) {
930
+ function whiteSpaceAfter(node) {
931
+ var ws = node ? node.nextSibling : self.container.firstChild;
932
+ if (!ws || !hasClass(ws, "whitespace")) return null;
933
+ return ws;
934
+ }
935
+
809
936
  // whiteSpace is the whitespace span at the start of the line,
810
937
  // or null if there is no such node.
811
- var whiteSpace = start ? start.nextSibling : this.container.firstChild;
812
- if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
813
- whiteSpace = null;
814
-
815
- // Sometimes the start of the line can influence the correct
816
- // indentation, so we retrieve it.
817
- var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
818
- var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
819
-
820
- // Ask the lexical context for the correct indentation, and
821
- // compute how much this differs from the current indentation.
938
+ var self = this, whiteSpace = whiteSpaceAfter(start);
822
939
  var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
823
- if (direction != null && this.options.tabMode == "shift")
824
- newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
825
- else if (start)
826
- newIndent = start.indentation(nextChars, curIndent, direction);
827
- else if (Editor.Parser.firstIndentation)
828
- newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
940
+
941
+ if (direction == "keep") {
942
+ if (start) {
943
+ var prevWS = whiteSpaceAfter(startOfLine(start.previousSibling))
944
+ if (prevWS) newIndent = prevWS.currentText.length;
945
+ }
946
+ }
947
+ else {
948
+ // Sometimes the start of the line can influence the correct
949
+ // indentation, so we retrieve it.
950
+ var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
951
+ var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
952
+
953
+ // Ask the lexical context for the correct indentation, and
954
+ // compute how much this differs from the current indentation.
955
+ if (direction != null && this.options.tabMode == "shift")
956
+ newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
957
+ else if (start)
958
+ newIndent = start.indentation(nextChars, curIndent, direction);
959
+ else if (Editor.Parser.firstIndentation)
960
+ newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
961
+ }
962
+
829
963
  var indentDiff = newIndent - curIndent;
830
964
 
831
965
  // If there is too much, this is just a matter of shrinking a span.
832
966
  if (indentDiff < 0) {
833
967
  if (newIndent == 0) {
834
- if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
968
+ if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild || firstText, 0);
835
969
  removeElement(whiteSpace);
836
970
  whiteSpace = null;
837
971
  }
@@ -847,18 +981,23 @@ var Editor = (function(){
847
981
  if (whiteSpace) {
848
982
  whiteSpace.currentText = makeWhiteSpace(newIndent);
849
983
  whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
984
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
850
985
  }
851
986
  // Otherwise, we have to add a new whitespace node.
852
987
  else {
853
- whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
988
+ whiteSpace = makePartSpan(makeWhiteSpace(newIndent));
854
989
  whiteSpace.className = "whitespace";
855
990
  if (start) insertAfter(whiteSpace, start);
856
991
  else this.container.insertBefore(whiteSpace, this.container.firstChild);
992
+ select.snapshotMove(firstText && (firstText.firstChild || firstText),
993
+ whiteSpace.firstChild, newIndent, false, true);
857
994
  }
858
- if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
995
+ }
996
+ // Make sure cursor ends up after the whitespace
997
+ else if (whiteSpace) {
998
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, newIndent, false);
859
999
  }
860
1000
  if (indentDiff != 0) this.addDirtyNode(start);
861
- return whiteSpace;
862
1001
  },
863
1002
 
864
1003
  // Re-highlight the selected part of the document.
@@ -867,7 +1006,7 @@ var Editor = (function(){
867
1006
  var to = select.selectionTopNode(this.container, false);
868
1007
  if (pos === false || to === false) return false;
869
1008
 
870
- select.markSelection(this.win);
1009
+ select.markSelection();
871
1010
  if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
872
1011
  return false;
873
1012
  select.selectMarked();
@@ -879,7 +1018,7 @@ var Editor = (function(){
879
1018
  // is re-indented.
880
1019
  handleTab: function(direction) {
881
1020
  if (this.options.tabMode == "spaces")
882
- select.insertTabAtCursor(this.win);
1021
+ select.insertTabAtCursor();
883
1022
  else
884
1023
  this.reindentSelection(direction);
885
1024
  },
@@ -914,6 +1053,37 @@ var Editor = (function(){
914
1053
  return true;
915
1054
  },
916
1055
 
1056
+ pageUp: function() {
1057
+ var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
1058
+ if (line === false || scrollAmount === false) return false;
1059
+ // Try to keep one line on the screen.
1060
+ scrollAmount -= 2;
1061
+ for (var i = 0; i < scrollAmount; i++) {
1062
+ line = this.prevLine(line);
1063
+ if (line === false) break;
1064
+ }
1065
+ if (i == 0) return false; // Already at first line
1066
+ select.setCursorPos(this.container, {node: line, offset: 0});
1067
+ select.scrollToCursor(this.container);
1068
+ return true;
1069
+ },
1070
+
1071
+ pageDown: function() {
1072
+ var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
1073
+ if (line === false || scrollAmount === false) return false;
1074
+ // Try to move to the last line of the current page.
1075
+ scrollAmount -= 2;
1076
+ for (var i = 0; i < scrollAmount; i++) {
1077
+ var nextLine = this.nextLine(line);
1078
+ if (nextLine === false) break;
1079
+ line = nextLine;
1080
+ }
1081
+ if (i == 0) return false; // Already at last line
1082
+ select.setCursorPos(this.container, {node: line, offset: 0});
1083
+ select.scrollToCursor(this.container);
1084
+ return true;
1085
+ },
1086
+
917
1087
  // Delay (or initiate) the next paren highlight event.
918
1088
  scheduleParenHighlight: function() {
919
1089
  if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
@@ -953,7 +1123,7 @@ var Editor = (function(){
953
1123
  unhighlight(self.highlighted[1]);
954
1124
  }
955
1125
 
956
- if (!window.select) return;
1126
+ if (!window.parent || !window.select) return;
957
1127
  // Clear the event property.
958
1128
  if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
959
1129
  this.parenEvent = null;
@@ -1036,13 +1206,9 @@ var Editor = (function(){
1036
1206
  // there's nothing to indent.
1037
1207
  if (cursor === false)
1038
1208
  return;
1039
- var lineStart = startOfLine(cursor);
1040
- var whiteSpace = this.indentLineAfter(lineStart, direction);
1041
- if (cursor == lineStart && whiteSpace)
1042
- cursor = whiteSpace;
1043
- // This means the indentation has probably messed up the cursor.
1044
- if (cursor == whiteSpace)
1045
- select.focusAfterNode(cursor, this.container);
1209
+ select.markSelection();
1210
+ this.indentLineAfter(startOfLine(cursor), direction);
1211
+ select.selectMarked();
1046
1212
  },
1047
1213
 
1048
1214
  // Indent all lines whose start falls inside of the current
@@ -1067,8 +1233,8 @@ var Editor = (function(){
1067
1233
  cursorActivity: function(safe) {
1068
1234
  // pagehide event hack above
1069
1235
  if (this.unloaded) {
1070
- this.win.document.designMode = "off";
1071
- this.win.document.designMode = "on";
1236
+ window.document.designMode = "off";
1237
+ window.document.designMode = "on";
1072
1238
  this.unloaded = false;
1073
1239
  }
1074
1240
 
@@ -1153,14 +1319,14 @@ var Editor = (function(){
1153
1319
  highlightDirty: function(force) {
1154
1320
  // Prevent FF from raising an error when it is firing timeouts
1155
1321
  // on a page that's no longer loaded.
1156
- if (!window.select) return false;
1322
+ if (!window.parent || !window.select) return false;
1157
1323
 
1158
- if (!this.options.readOnly) select.markSelection(this.win);
1324
+ if (!this.options.readOnly) select.markSelection();
1159
1325
  var start, endTime = force ? null : time() + this.options.passTime;
1160
1326
  while ((time() < endTime || force) && (start = this.getDirtyNode())) {
1161
1327
  var result = this.highlight(start, endTime);
1162
1328
  if (result && result.node && result.dirty)
1163
- this.addDirtyNode(result.node);
1329
+ this.addDirtyNode(result.node.nextSibling);
1164
1330
  }
1165
1331
  if (!this.options.readOnly) select.selectMarked();
1166
1332
  if (start) this.scheduleHighlight();
@@ -1173,12 +1339,12 @@ var Editor = (function(){
1173
1339
  var self = this, pos = null;
1174
1340
  return function() {
1175
1341
  // FF timeout weirdness workaround.
1176
- if (!window.select) return;
1342
+ if (!window.parent || !window.select) return;
1177
1343
  // If the current node is no longer in the document... oh
1178
1344
  // well, we start over.
1179
1345
  if (pos && pos.parentNode != self.container)
1180
1346
  pos = null;
1181
- select.markSelection(self.win);
1347
+ select.markSelection();
1182
1348
  var result = self.highlight(pos, time() + passTime, true);
1183
1349
  select.selectMarked();
1184
1350
  var newPos = result ? (result.node && result.node.nextSibling) : null;
@@ -1244,7 +1410,7 @@ var Editor = (function(){
1244
1410
  }
1245
1411
  // Create a part corresponding to a given token.
1246
1412
  function tokenPart(token){
1247
- var part = makePartSpan(token.value, self.doc);
1413
+ var part = makePartSpan(token.value);
1248
1414
  part.className = token.style;
1249
1415
  return part;
1250
1416
  }