unfold: better management of static files thanks to finders for plugins and third...
[myslice.git] / plugins / code_editor / static / js / Actions.js
1 // TODO: refactor
2 // TODO: This code is now legacy code, none of it will make its way into Beta
3 window.editorsModified = false;
4
5 Request.JSON.implement({
6   initialize: function(options) {
7     this.parent(options);
8     this.setHeader('X-CSRFToken', Cookie.read('csrftoken'));
9   }
10 });
11
12 Track = {
13   ui: function(action){
14     if (typeof(_gaq) !== 'undefined'){
15       _gaq.push(['_trackEvent', 'UI',  action || null]);
16     }
17   }
18 };
19
20 /*
21  * Define actions on the run/save/clean buttons
22  */
23 var MooShellActions = new Class({
24   Implements: [Options, Events],
25   options: {
26     // onRun: $empty,
27     // onClean: $empty,
28     formId: 'show-result',
29     saveAndReloadId: 'update',
30     saveAsNewId: 'savenew',
31     runId: 'run',
32     draftId: 'mobile',
33     jslintId: 'jslint',
34     tidyId: 'tidy',
35     showJsId: 'showjscode',
36     shareSelector: '#share_link_dropdown, #share_embedded_dropdown, #share_result_dropdown',
37     favId: 'mark_favourite',
38     entriesSelector: 'textarea',
39     resultLabel: 'result_label',
40     resultInput: 'select_link',
41     collaborateId: 'collaborate',
42     example_id: false,
43     exampleURL: '',
44     exampleSaveURL: '',
45     loadDependenciesURL: '',
46     tidy: {
47       'javascript': 'js',
48       'javascript 1.7': 'js',
49       'html': 'html',
50       'css': 'css'
51     },
52     jslint: {
53       evil: true,
54       passfail: false,
55       browser: true,
56       newcap: false
57     },
58     jslintLanguages: ['text/javascript', 'javascript', 'javascript 1.7'],
59     showJSLanguages: ['text/coffeescript', 'coffeescript']
60   },
61   /*
62    * Assign actions
63    */
64   initialize: function(options) {
65     this.setOptions(options);
66     var addBound = function(el, callback, bind) {
67       el = $(el)[0]; // jordan added [0] for jquery
68       if (el){
69         el.addEvent('click', callback.bind(bind));
70       }
71     };
72
73     Layout.addEvent('ready', function() {
74       addBound(this.options.saveAndReloadId, this.saveAndReload, this);
75       addBound(this.options.saveAsNewId, this.saveAsNew, this);
76       addBound(this.options.runId, this.run, this);
77       addBound(this.options.draftId, this.run, this);
78       addBound(this.options.jslintId, this.jsLint, this);
79       addBound(this.options.showJsId, this.showJs, this);
80       addBound(this.options.tidyId, this.prepareAndLaunchTidy, this);
81       addBound(this.options.favId, this.makeFavourite, this);
82       // addBound(this.options.collaborateId, this.launchTowtruck, this);
83
84       // show key shortcuts
85       var keyActions = document.getElements('a.keyActions');
86       keyActions.addEvents({
87         click: function(event){
88           this.showShortcutDialog(event);
89         }.bind(this)
90       });
91       document.id(document.body).addEvents({
92         keydown: function(event){
93           if (event.shift && event.key === '/'){
94             var elType = new Element(event.target);
95             if (elType.get('tag') === 'body'){
96               keyActions[0].fireEvent('click');
97             }
98           }
99         }
100       });
101
102       var share = $$(this.options.shareSelector);
103       if (share.length > 0) {
104         share.addEvent('click', function() {
105           this.select();
106         });
107       }
108
109       // actions run if shell loaded
110       this.form = document.id(this.options.formId);
111
112       if (this.options.exampleURL) {
113         //  this.run();
114         this.displayExampleURL();
115       }
116       // assign change language in panel
117       $$('.panelChoice').addEvent('change', this.switchLanguageAction.bind(this));
118       this.showHideJsLint();
119       this.showHideTidyUp();
120       this.showHideShowJs();
121     }.bind(this));
122   },
123
124   switchLanguageAction: function(e) {
125     if (!e) return;
126     var sel = $(e.target);
127     this.switchLanguage(sel);
128   },
129
130   /*
131    * Change language in panel
132    */
133   switchLanguage: function(sel) {
134     var panel_name = sel.get('data-panel'),
135         editorClass = Layout.editors[panel_name],
136         // Klass = MooShellEditor[panel_name.toUpperCase()],
137         language = sel.getElement('option:selected').get('text'),
138         type = sel.getElement('option:selected').get('data-mime-type');
139
140     editorClass.editor.setOption('mode', type);
141     // editorClass.updateCode();
142
143     // editor.getWindow().getElement('.CodeMirror-wrapping').destroy();
144     // Layout.editors[panel_name] = editor = false;
145     // new Klass($(sel.get('data-panel_id')), {
146     //   language: language.toLowerCase()
147     // });
148
149     Layout.editors[panel_name].setLabelName(language);
150     window['panel_' + panel_name] = language.toLowerCase();
151     this.showHideTidyUp();
152     this.showHideJsLint();
153     this.showHideShowJs();
154
155     Track.ui('Switch language: ' + language);
156   },
157
158   loadAsset: function(check, file, callback, scope) {
159     if (!check()) {
160       if (scope){
161         callback = callback.bind(scope);
162       }
163
164       Asset.javascript(file, {
165         onload: callback
166       });
167       return true;
168     }
169   },
170
171   prepareCoffee: function(callback) {
172     return this.loadAsset(function() {
173       return $defined(window.CoffeeScript);
174     }, '/js/coffeescript/coffeescript.js', callback, this);
175   },
176
177   showJs: function(e) {
178     if (e) e.stop();
179     if (this.prepareCoffee(this.showJs, this)) return;
180     var html = '<div class="modalWrap modal_Coffee">' +
181       '<div class="modalHeading"><h3>JavaScript Code</h3><span class="close">Close window</span></div>'+
182       '<div id="" class="modalBody">',
183       jscode, coffeecode;
184
185     if (panel_js != 'coffeescript') return;
186     coffeecode = Layout.editors.js.editor.getValue();
187     try {
188       jscode = CoffeeScript.compile(coffeecode);
189       html += '<pre>' + jscode + '</pre>';
190     } catch(error) {
191       html += '<p class="error">' + error + '</p>';
192     }
193     html += '</div></div>';
194     new StickyWin({
195       content: html,
196       relativeTo: $(document.body),
197       position: 'center',
198       edge: 'center',
199       closeClassName: 'close',
200       draggable: true,
201       dragHandleSelector: 'h3',
202       closeOnEsc: true,
203       destroyOnClose: true,
204       allowMultiple: false,
205       onDisplay: this.showModalFx
206     }).show();
207
208     Track.ui('Show JS');
209   },
210
211   prepareTidyUp: function(callback, scope) {
212     //return this.loadAsset(function() {
213     //  return $defined(window.Beautifier);
214     //}, '/js/beautifier.js', callback, scope);
215   },
216
217   showHideShowJs: function() {
218     var show = false,
219         showjs = $(this.options.showJsId)[0]; // jordan
220
221     if (showjs){
222       show = (Layout.editors.js.editor.getOption('mode').contains('coffeescript'));
223       if (show) {
224         showjs.getParent('li').show();
225       } else {
226         showjs.getParent('li').hide();
227       }
228     }
229   },
230
231   showHideJsLint: function() {
232
233     var hide = true,
234         lint = $(this.options.jslintId)[0]; // jordan added [0] for jquery
235
236     if (!lint) return;
237     Layout.editors.each(function(w){
238       if (this.options.jslintLanguages.contains(w.editor.getOption('mode'))) {
239         hide = false;
240       }
241     }, this);
242     if (hide) {
243       lint.getParent('li').hide();
244     } else {
245       lint.getParent('li').show();
246     }
247   },
248
249   showHideTidyUp: function() {
250 /*
251     if (this.prepareTidyUp(this.showHideTidyUp, this)) return;
252     var hide = true,
253         tidy = $(this.options.tidyId);
254
255     if (!tidy) return;
256     Layout.editors.each(function(w){
257       language = this.options.tidy[w.options.language];
258       if (language && Beautifier[language]) {
259         hide = false;
260       }
261     }, this);
262     if (hide) {
263       tidy.getParent('li').hide();
264     } else {
265       tidy.getParent('li').show();
266     }
267 */
268   },
269
270   prepareAndLaunchTidy: function(e) {
271     e.stop();
272     if (this.prepareTidyUp(this.makeTidy.bind(this))) return;
273     this.makeTidy();
274   },
275
276   makeTidy: function(){
277 /*
278     Layout.editors.each(function(editorInstance){
279       var code = editorInstance.editor.getValue(), language;
280       if (code) {
281         language = this.options.tidy[editorInstance.options.language];
282         if (language && Beautifier[language]) {
283           var fixed = Beautifier[language](code);
284           if (fixed) editorInstance.editor.setValue(fixed);
285           else editorInstance.editor.reindent();
286         }
287       }
288     }, this);
289 */
290   },
291
292   jsLint: function(e) {
293     e.stop();
294     if (JSHINT){
295       return this.JSLintValidate();
296       Track.ui('Validate JavaScript');
297     }
298     // if (!window.JSLINT) {
299     //   // never happens as apparently JSLINT needs to be loaded before MooTools
300     //   Asset.javascript('/js/jslint.min.js', {
301     //     onload: this.JSLintValidate.bind(this)
302     //   });
303     // } else {
304     //   return this.JSLintValidate();
305     // }
306   },
307
308   JSLintValidate: function() {
309     var editor = Layout.editors.js.editor;
310     var html = '<div class="modalWrap modal_jslint">' +
311       '<div class="modalHeading"><h3>JSLint {title}</h3><span class="close">Close window</span></div>'+
312       '<div class="modalBody">{content}</div></div>';
313     var sticky = function(subs){
314       return new StickyWin({
315         content: html.substitute(subs),
316         relativeTo: $(document.body),
317         position: 'center',
318         edge: 'center',
319         closeClassName: 'close',
320         draggable: true,
321         dragHandleSelector: 'h3',
322         closeOnEsc: true,
323         destroyOnClose: true,
324         allowMultiple: false,
325         onDisplay: this.showModalFx
326       }).show();
327     };
328
329     if (this.options.jslintLanguages.contains(panel_js)){
330       var editorValue = editor.getValue();
331
332       // clear all markers from the gutter, we'll highlight errors again in the next step
333       for (var line = 0; editor.lineCount() >= line; line++){
334         editor.setGutterMarker(line, 'note-gutter', null);
335       }
336
337       if (editorValue.trim() === ''){
338         sticky.call(this, {
339           title: 'Valid!',
340           content: '<p>Good work! Your JavaScript code is perfectly valid.</p>'
341         });
342       } else {
343         if (JSHINT(editorValue)){
344           sticky.call(this, {
345             title: 'Valid!',
346             content: '<p>Good work! Your JavaScript code is perfectly valid.</p>'
347           });
348         } else {
349           Array.each(JSHINT.errors, function(error, index){
350             errorEl = Element('span', {
351               'class': 'CodeMirror-line-error',
352               'data-title': error.reason,
353               'text': '●'
354             });
355             editor.setGutterMarker(error.line - 1, 'note-gutter', errorEl);
356           }, this);
357         }
358       }
359     } else {
360       sticky.call(this, {
361         title: 'Sorry No JavaScript!',
362         content: '<p>You\'re using ' + panel_js + '</p>'
363       });
364     }
365   },
366
367   // mark shell as favourite
368   makeFavourite: function(e) {
369     e.stop();
370     new Request.JSON({
371       'url': makefavouritepath,
372       'data': {shell_id: this.options.example_id},
373       'onSuccess': function(response) {
374
375         // #TODO: reload page after successful save
376         window.location.href = response.url;
377         //$('mark_favourite').addClass('isFavourite').getElements('span')[0].set('text', 'Base');
378       }
379     }).send();
380     Track.ui('Mark as favourite');
381   },
382
383   launchTowtruck: function(event){
384     if (event){
385       event.stop();
386     }
387     TowTruck(this);
388     Track.ui('Launch TowTruck');
389   },
390
391   // save and create new pastie
392   saveAsNew: function(e) {
393     e.stop();
394
395     // reset change state so the confirmation doesn't appear on saving
396     window.editorsModified = false;
397     Layout.updateFromMirror();
398     $('id_slug').value='';
399     $('id_version').value='0';
400     new Request.JSON({
401       'url': this.options.exampleSaveUrl,
402       'onSuccess': function(json) {
403         Layout.decodeEditors();
404         if (!json.error) {
405
406           // reload page after successful save
407           window.location = json.pastie_url_relative;
408         } else {
409           alert('ERROR: ' + json.error);
410         }
411       }
412     }).send(this.form);
413
414     Track.ui('Save as new fiddle');
415   },
416
417   // update existing (create shell with new version)
418   saveAndReload: function(e) {
419     if (e) e.stop();
420
421     // reset change state so the confirmation doesn't appear on updating
422     window.editorsModified = false;
423     Layout.updateFromMirror();
424     new Request.JSON({
425       'url': this.options.exampleSaveUrl,
426       'onSuccess': function(json) {
427
428         // reload page after successful save
429         Layout.decodeEditors();
430         window.location = json.pastie_url_relative;
431       }
432     }).send(this.form);
433
434     Track.ui('Update fiddle');
435   },
436
437   // run - submit the form (targets to the iframe)
438   run: function(e) {
439     var draftonly = false;
440     if (e) e.stop();
441     if (e && ($(e.target).getParent().get('id') === 'mobile' || $(e.target).get('id') === 'mobile')) {
442       draftonly = new Element('input', {
443         'hidden': true,
444         'name': 'draftonly',
445         'id': 'draftonly',
446         'value': true
447       });
448       draftonly.inject(this.form);
449     }
450     Layout.updateFromMirror();
451     this.form.submit();
452     if (draftonly) {
453       draftonly.destroy();
454     }
455     this.fireEvent('run');
456
457     Track.ui('Run fiddle');
458   },
459
460   loadDraft: function(e) {
461     if (e) e.stop();
462     if (username) {
463       window.open('/draft/', 'jsfiddle_draft');
464     } else {
465       window.location = '/user/login/';
466     }
467
468     Track.ui('Load draft');
469   },
470
471   // This is a method to be called by keyboard shortcut
472   toggleSidebar: function(e) {
473      if (e) e.stop();
474      Layout.sidebar.toggle();
475
476      Track.ui('Toggle sidebar');
477   },
478
479   showShortcutDialog: function(e) {
480     if (e) e.stop();
481     var html = '<div class="modalWrap modal_kbd">' +
482                 '<div class="modalHeading"><h3>Keyboard shortcuts</h3><span class="close">Close window</span></div>'+
483                 '<div id="kbd" class="modalBody">' +
484                 '<ul>' +
485                 '<li><kbd>CTRL</kbd> + <kbd>Return</kbd> <span>Run fiddle</span></li>' +
486                 '<li><kbd>CTRL</kbd> + <kbd>S</kbd> <span>Save fiddle (Save or Update)</span></li>' +
487                 '<li><kbd>CTRL</kbd> + <kbd>Shift</kbd> + <kbd>Return</kbd> <span>Load draft</span></li>' +
488                 // '<li><kbd>Control</kbd> + <kbd>&uarr;</kbd> or <kbd>Control</kbd> + <kbd>&darr;</kbd> <span>Switch editor windows</span></li>' +
489                 '<li><kbd>CTRL</kbd> + <kbd>Shift</kbd> + <kbd>&uarr;</kbd> <span>Toggle sidebar</span></li>' +
490                 '<li><kbd>CTRL</kbd> + <kbd>K</kbd> <span>Collaboration with TowTruck (very Alpha, don\'t rely on it too much)</span></li>' +
491                 '</ul>' +
492                 '</div></div>';
493
494     new StickyWin({
495       content: html,
496       relativeTo: $(document.body),
497       position: 'center',
498       edge: 'center',
499       closeClassName: 'close',
500       draggable: true,
501       dragHandleSelector: 'h3',
502       closeOnEsc: true,
503       destroyOnClose: true,
504       allowMultiple: false,
505       onDisplay: this.showModalFx
506     }).show();
507
508     Track.ui('Show shortcuts modal');
509   },
510
511   showModalFx: function(){
512       $$('.modalWrap')[0].addClass('show');
513   },
514
515   switchTo: function(index) {
516     Layout.current_editor = Layout.editors_order[index];
517     Layout.editors[Layout.current_editor].editor.focus();
518   },
519
520   switchNext: function() {
521     // find current and switch to the next
522     var index = Layout.editors_order.indexOf(Layout.current_editor);
523     var nextindex = (index + 1) % 3;
524     this.switchTo(nextindex);
525   },
526
527   switchPrev: function() {
528     // find current and switch to previous
529     var index = Layout.editors_order.indexOf(Layout.current_editor);
530     var nextindex = (index - 1) % 3;
531     if (nextindex < 0) nextindex = 2;
532     this.switchTo(nextindex);
533   },
534
535   // rename iframe label to present the current URL
536   displayExampleURL: function() {
537     var resultInput = document.id(this.options.resultInput);
538     if (resultInput) {
539       if (Browser.Engine.gecko) {
540         resultInput.setStyle('padding-top', '4px');
541       }
542       // resultInput.select();
543     }
544   },
545
546   loadLibraryVersions: function(group_id) {
547     if (group_id) {
548       new Request.JSON({
549         url: this.options.loadLibraryVersionsURL.substitute({group_id: group_id}),
550         onSuccess: function(response) {
551           $('js_lib').empty();
552           $('js_dependency').empty();
553           response.libraries.each( function(lib) {
554             new Element('option', {
555               value: lib.id,
556               text: "{group_name} {version}".substitute(lib)
557             }).inject($('js_lib'));
558             if (lib.selected) $('js_lib').set('value',lib.id);
559           });
560           response.dependencies.each(function (dep) {
561             new Element('li', {
562               html: [
563                 "<input id='dep_{id}' type='checkbox' name='js_dependency[{id}]' value='{id}'/>",
564                 "<label for='dep_{id}'>{name}</label>"
565                 ].join('').substitute(dep)
566             }).inject($('js_dependency'));
567             if (dep.selected) $('dep_'+dep.id).set('checked', true);
568           });
569         }
570       }).send();
571     } else {
572       // XXX: would be good to send an error somehow
573     }
574   },
575
576   loadDependencies: function(lib_id) {
577     if (lib_id) {
578       new Request.JSON({
579         url: this.options.loadDependenciesURL.substitute({lib_id: lib_id}),
580         method: 'get',
581         onSuccess: function(response) {
582           $('js_dependency').empty();
583           response.each(function (dep) {
584             new Element('li', {
585               html: [
586                 "<input id='dep_{id}' type='checkbox' name='js_dependency[{id}]' value='{id}'/>",
587                 "<label for='dep_{id}'>{name}</label>"
588                 ].join('').substitute(dep)
589             }).inject($('js_dependency'));
590             if (dep.selected) $('dep_'+dep.id).set('checked', true);
591           });
592         }
593       }).send();
594     } else {
595       // XXX: would be good to send an error somehow
596     }
597   }
598 });
599
600 /**
601 *
602 *  Base64 encode / decode
603 *  http://www.webtoolkit.info/
604 *
605 **/
606
607 var Base64 = {
608
609   // private property
610   _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
611
612   // public method for encoding
613   encode : function (input) {
614     var output = "";
615     input = input || "";
616     var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
617     var i = 0;
618
619     input = Base64._utf8_encode(input);
620
621     while (i < input.length) {
622
623       chr1 = input.charCodeAt(i++);
624       chr2 = input.charCodeAt(i++);
625       chr3 = input.charCodeAt(i++);
626
627       enc1 = chr1 >> 2;
628       enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
629       enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
630       enc4 = chr3 & 63;
631
632       if (isNaN(chr2)) {
633         enc3 = enc4 = 64;
634       } else if (isNaN(chr3)) {
635         enc4 = 64;
636       }
637
638       output = output +
639       this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
640       this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
641
642     }
643
644     return output;
645   },
646
647   // public method for decoding
648   decode : function (input) {
649     var output = "";
650     var chr1, chr2, chr3;
651     var enc1, enc2, enc3, enc4;
652     var i = 0;
653
654     input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
655
656     while (i < input.length) {
657
658       enc1 = this._keyStr.indexOf(input.charAt(i++));
659       enc2 = this._keyStr.indexOf(input.charAt(i++));
660       enc3 = this._keyStr.indexOf(input.charAt(i++));
661       enc4 = this._keyStr.indexOf(input.charAt(i++));
662
663       chr1 = (enc1 << 2) | (enc2 >> 4);
664       chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
665       chr3 = ((enc3 & 3) << 6) | enc4;
666
667       output = output + String.fromCharCode(chr1);
668
669       if (enc3 != 64) {
670         output = output + String.fromCharCode(chr2);
671       }
672       if (enc4 != 64) {
673         output = output + String.fromCharCode(chr3);
674       }
675
676     }
677
678     output = Base64._utf8_decode(output);
679
680     return output;
681
682   },
683
684   // private method for UTF-8 encoding
685   _utf8_encode : function (string) {
686     var utftext = "";
687     string = string.replace(/\r\n/g,"\n");
688
689     for (var n = 0; n < string.length; n++) {
690
691       var c = string.charCodeAt(n);
692
693       if (c < 128) {
694         utftext += String.fromCharCode(c);
695       }
696       else if((c > 127) && (c < 2048)) {
697         utftext += String.fromCharCode((c >> 6) | 192);
698         utftext += String.fromCharCode((c & 63) | 128);
699       }
700       else {
701         utftext += String.fromCharCode((c >> 12) | 224);
702         utftext += String.fromCharCode(((c >> 6) & 63) | 128);
703         utftext += String.fromCharCode((c & 63) | 128);
704       }
705
706     }
707     return utftext;
708   },
709
710   // private method for UTF-8 decoding
711   _utf8_decode : function (utftext) {
712     var string = "";
713     var i = 0;
714     var c = c1 = c2 = 0;
715
716     while ( i < utftext.length ) {
717
718       c = utftext.charCodeAt(i);
719
720       if (c < 128) {
721         string += String.fromCharCode(c);
722         i++;
723       }
724       else if((c > 191) && (c < 224)) {
725         c2 = utftext.charCodeAt(i+1);
726         string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
727         i += 2;
728       }
729       else {
730         c2 = utftext.charCodeAt(i+1);
731         c3 = utftext.charCodeAt(i+2);
732         string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
733         i += 3;
734       }
735
736     }
737
738     return string;
739   }
740
741 };
742
743 var Dropdown = new Class({
744
745   initialize: function(){
746     this.dropdown = {
747       cont: document.getElements('.dropdownCont'),
748       trigger: document.getElements('.dropdown a.aiButton')
749     };
750
751     this.setDefaults();
752   },
753
754   setDefaults: function(){
755     this.dropdown.cont.fade('hide');
756     this.dropdown.cont.set('tween', {
757       duration: 200
758     });
759
760     this.dropdown.trigger.each(function(trigger){
761       trigger.addEvents({
762         click: this.toggle.bindWithEvent(trigger, this)
763       });
764     }, this);
765
766     $(document.body).addEvents({
767       click: function(e){
768         if (!$(e.target).getParent('.dropdownCont')){
769           this.hide();
770           // this.dropdown.trigger.getElement('span').removeClass('selected')
771         }
772       }.bind(this)
773     });
774   },
775
776   toggle: function(event, parent){
777     var trigger = Element(event.target);
778     event.stop();
779     parent.dropdown.cont.fade('out');
780     // trigger.removeClass('selected')
781
782     if (this.getNext('.dropdownCont').getStyles('opacity')['opacity'] === 0){
783       this.getNext('.dropdownCont').fade('in');
784       // trigger.addClass('selected');
785     }
786   },
787
788   hide: function(){
789     this.dropdown.cont.fade('out');
790   }
791 });
792
793 // mootools events don't work with beforeunload for some reason
794 window.onbeforeunload = function(){
795   if (window.editorsModified){
796     return "You've modified your fiddle, reloading the page will reset all changes."
797   }
798 };