liquid_cms 0.3.0.1 → 0.3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }