unfold: better management of static files thanks to finders for plugins and third...
[myslice.git] / plugins / code_editor / static / js / EditorCM.js
1 /*
2  Class: MooshellEditor
3  Editor using CodeMirror
4  http://codemirror.net
5  */
6
7 // jordan
8 var codemirrorpath = "";
9 // end jorda
10
11 var disallowedPlatforms = ['ios', 'android', 'ipod'];
12 var default_code_mirror_options = {
13   gutters: ["note-gutter", "CodeMirror-linenumbers"],
14   tabSize: 4,
15   indentUnit: 4,
16   matchBrackets: true,
17   lineNumbers: true,
18   lineWrapping: true,
19   tabMode: 'spaces' // or 'shift'
20 };
21
22 var MooShellEditor = new Class({
23   Implements: [Options, Events],
24
25   parameter: 'Editor',
26
27   options: {
28     useCodeMirror: true,
29     codeMirrorOptions: default_code_mirror_options,
30     syntaxHighlighting: []
31   },
32
33   window_names: {
34     'javascript': 'JavaScript',
35     'html': 'HTML',
36     'css': 'CSS',
37     'scss': 'SCSS',
38     'coffeescript': 'CoffeeScript',
39     'javascript 1.7': 'JavaScript 1.7'
40   },
41
42   initialize: function(el, options) {
43     this.validationTooltip;
44
45     // switch off CodeMirror for IE
46     //if (Browser.Engine.trident) options.useCodeMirror = false;
47     this.element = $(el)[0]; // jordan added [0] for jquery
48     if (!this.options.syntaxHighlighting.contains(options.language)) {
49       this.forceDefaultCodeMirrorOptions();
50     }
51     this.setOptions(options);
52     var is_disallowed = (disallowedPlatforms.contains(Browser.Platform.name));
53     if (this.options.useCodeMirror && !is_disallowed) {
54       // hide textarea
55       this.element.hide();
56       // prepare settings
57       if (!this.options.codeMirrorOptions.stylesheet && this.options.stylesheet) {
58         this.options.codeMirrorOptions.stylesheet = this.options.stylesheet.map(function(path) {
59           return mediapath + path;
60         });
61       }
62       if (!this.options.codeMirrorOptions.path) {
63         this.options.codeMirrorOptions.path = codemirrorpath + 'js/';
64       }
65       if (!this.options.codeMirrorOptions.content) {
66         this.options.codeMirrorOptions.content = this.element.get('value');
67       }
68
69       var parentNode = this.element.getParent();
70       var options = { value: this.element.value };
71       options = Object.append(options, this.options.codeMirrorOptions);
72       this.editor = CodeMirror(parentNode, options);
73
74       // run this after initialization
75       if (!this.options.codeMirrorOptions.initCallback){
76         if (this.options.codeMirrorOptions.autofocus) {
77           // set current editor
78           Layout.current_editor = this.options.name;
79         }
80       }
81
82       // set editor options that can only be set once the editor is initialized
83       var cur = this.editor.getLineHandle(this.editor.getCursor().line);
84       this.setEditorEvents({
85         focus: function(){
86           Layout.current_editor = this.options.name;
87           // this.editor.removeLineClass(cur, 'background', 'activeline');
88           // this.editor.hlLine = this.editor.addLineClass(0, "background", "activeline");
89         },
90         cursorActivity: function(){
91           // var cur = this.editor.getLineHandle(this.editor.getCursor().line);
92           // if (cur != this.editor.hlLine) {
93           //   this.editor.removeLineClass(this.editor.hlLine, 'background', 'activeline');
94           //   this.editor.hlLine = this.editor.addLineClass(cur, 'background', 'activeline');
95           // }
96         },
97         blur: function(){
98           // this.editor.removeLineClass(this.editor.hlLine, 'background', 'activeline');
99         },
100         change: function(){
101           this.validateEditorInput.call(this, parentNode);
102           window.editorsModified = true;
103         }.bind(this)
104       });
105
106       // disable new line insertion when saving fiddle
107       CodeMirror.keyMap.default['Ctrl-Enter'] = function(){
108         return false;
109       };
110
111       // don't let Emmet capture this
112       delete CodeMirror.keyMap.default['Cmd-L'];
113     }
114
115     this.editorLabelFX = new Fx.Tween(this.getLabel(), {
116       property: 'opacity',
117       link: 'cancel'
118     });
119
120     this.getWindow().addEvents({
121       mouseenter: function() {
122         this.editorLabelFX.start(0);
123       }.bind(this),
124       mouseleave: function() {
125         this.editorLabelFX.start(0.8);
126       }.bind(this)
127     });
128
129 // jordan commented
130 //    mooshell.addEvents({
131 //      'run': this.b64decode.bind(this)
132 //    });
133
134     Layout.registerEditor(this);
135     this.setLabelName(this.options.language || this.options.name);
136   },
137
138   validateEditorInput: function(parentNode){
139     var currentValue = this.getCode();
140     var warnings = [];
141
142     // destroy tooltip if it already exists for this editor
143     if (this.validationTooltip){
144       this.validationTooltip.destroy();
145     }
146
147     // create the container
148     this.validationTooltip = Element('ul', {
149       'class': 'warningTooltip'
150     });
151
152     // collect warnings
153     Object.each(this.options.disallowed, function(value, key){
154       if (currentValue.test(key, 'i')){
155         warnings.push('<li>' + value + '</li>');
156       }
157     });
158
159     // inject container
160     this.validationTooltip = this.validationTooltip.inject(parentNode);
161
162     // squash and apply warnings
163     this.validationTooltip.set({
164       html: warnings.join('')
165     });
166   },
167
168   getEditor: function() {
169     return this.editor || this.element;
170   },
171
172   getWindow: function() {
173     if (!this.window) {
174       this.window = this.element.getParent('.window');
175     }
176     return this.window;
177   },
178
179   getLabel: function() {
180     return this.getWindow().getElement('.window_label');
181   },
182
183   b64decode: function() {
184     this.element.set('value', this.before_decode);
185   },
186
187   getCode: function() {
188     return (this.editor) ? this.editor.getValue() : this.element.get('value');
189   },
190
191   updateFromMirror: function() {
192     this.before_decode = this.getCode();
193     this.element.set('value', Base64.encode(this.before_decode));
194   },
195
196   updateCode: function() {
197     this.element.set('value', this.getCode());
198   },
199
200   clean: function() {
201     this.element.set('value', '');
202     this.cleanEditor();
203   },
204
205   cleanEditor: function() {
206     if (this.editor) this.editor.setCode('');
207   },
208
209   hide: function() {
210     this.getWindow().hide();
211   },
212
213   show: function() {
214     this.getWindow().show();
215   },
216
217   setEditorOptions: function(options){
218     Object.each(options, function(fn, key){
219       this.editor.setOption(key, fn.bind(this));
220     }, this);
221   },
222
223   setEditorEvents: function(e){
224     Object.each(e, function(fn, key){
225       this.editor.on(key, fn.bind(this));
226     }, this);
227   },
228
229   setLanguage: function(language) {
230     // Todo: This is hacky
231     this.setLabelName(language);
232   },
233
234   setLabelName: function(language) {
235     this.getLabel().set('text', this.window_names[language] || language);
236   },
237
238   setStyle: function(key, value) {
239     if (this.editor) return $(this.editor.frame).setStyle(key, value);
240     return this.element.setStyle(key, value);
241   },
242
243   setStyles: function(options) {
244     if (this.editor) return $(this.editor.frame).setStyles(options);
245     return this.element.setStyles(options);
246   },
247
248   setWidth: function(width) {
249     this.getWindow().setStyle('width', width);
250   },
251
252   setHeight: function(height) {
253     this.getWindow().setStyle('height', height);
254   },
255
256   getPosition: function() {
257     if (this.editor) return $(this.editor.frame).getPosition();
258     return this.element.getPosition();
259   },
260
261   forceDefaultCodeMirrorOptions: function() {
262     this.options.codeMirrorOptions = default_code_mirror_options;
263   }
264 });
265
266
267 /*
268  * JS specific settings
269  */
270 MooShellEditor.JS = new Class({
271   Extends: MooShellEditor,
272
273   options: {
274     name: 'js',
275     language: 'javascript',
276     useCodeMirror: true,
277     codeMirrorOptions: {
278       autofocus: true,
279       mode: 'javascript'
280     },
281     syntaxHighlighting: ['javascript', 'javascript 1.7'],
282     disallowed: {
283       '<script': "Input plain JavaScript code, no HTML.",
284       'document.write': "<code>document.write</code> is disallowed in JSFiddle envioriment and might break your fiddle."
285     }
286   },
287
288   initialize: function(el, options) {
289     this.setOptions(options);
290     this.parent(el, this.options);
291   }
292 });
293
294 /*
295  * CSS specific settings
296  */
297 MooShellEditor.CSS = new Class({
298   Extends: MooShellEditor,
299
300   options: {
301     name: 'css',
302     language: 'css',
303     useCodeMirror: true,
304     codeMirrorOptions: {
305       mode: 'css'
306     },
307     syntaxHighlighting: ['css', 'scss']
308   },
309
310   initialize: function(el, options) {
311     this.setOptions(options);
312     this.parent(el, this.options);
313   }
314 });
315
316 /*
317  * HTML specific settings
318  */
319 MooShellEditor.HTML = new Class({
320   Extends: MooShellEditor,
321
322   options: {
323     name: 'html',
324     language: 'html',
325     useCodeMirror: true,
326     codeMirrorOptions: {
327       mode: 'xml'
328     },
329     syntaxHighlighting: ['html'],
330     disallowed: {
331       '<html': "No need for the <code>HTML</code> tag, it's already in the output.",
332       '<meta': "No need for the <code>META</code> tags.",
333       '<head*.?>': "No need for the <code>HEAD</code> tag, it's already in the output.",
334       '<doctype': "Select <code>DOCTYPE</code> from the <strong>Info</strong> panel on the left, instead of adding a tag.",
335       '<script/?.*text\/javascript': "For JavaScript use the panel below or the <strong>Resources</strong> panel for external files.",
336       '<link/?.*rel/?.*stylesheet': "For external CSS files use the <strong>Resources</strong> panel on the left.",
337       '<style': "For CSS use the panel on the right.",
338     }
339   },
340
341   initialize: function(el, options) {
342     this.setOptions(options);
343     this.parent(el, this.options);
344   }
345 });