Aleksander Machniak
2015-08-12 6b31846c438dc19cca0f2a01582bdf47dff61f81
commit | author | age
b34d67 1 /**
TB 2  * Roundcube editor js library
3  *
4  * This file is part of the Roundcube Webmail client
5  *
6  * @licstart  The following is the entire license notice for the
7  * JavaScript code in this file.
8  *
9  * Copyright (c) 2006-2014, The Roundcube Dev Team
10  *
11  * The JavaScript code in this page is free software: you can
12  * redistribute it and/or modify it under the terms of the GNU
13  * General Public License (GNU GPL) as published by the Free Software
14  * Foundation, either version 3 of the License, or (at your option)
15  * any later version.  The code is distributed WITHOUT ANY WARRANTY;
16  * without even the implied warranty of MERCHANTABILITY or FITNESS
17  * FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
18  *
19  * As additional permission under GNU GPL version 3 section 7, you
20  * may distribute non-source (e.g., minimized or compacted) forms of
21  * that code without the copy of the GNU GPL normally required by
22  * section 4, provided you include this license notice and a URL
23  * through which recipients can access the Corresponding Source.
24  *
25  * @licend  The above is the entire license notice
26  * for the JavaScript code in this file.
27  *
28  * @author Eric Stadtherr <estadtherr@gmail.com>
646b64 29  * @author Aleksander Machniak <alec@alec.pl>
b34d67 30  */
a0109c 31
646b64 32 /**
AM 33  * Roundcube Text Editor Widget class
34  * @constructor
35  */
36 function rcube_text_editor(config, id)
4ca10b 37 {
646b64 38   var ref = this,
681ba6 39     abs_url = location.href.replace(/[?#].*$/, '').replace(/\/$/, ''),
646b64 40     conf = {
AM 41       selector: '#' + ($('#' + id).is('.mce_editor') ? id : 'fake-editor-id'),
db7115 42       cache_suffix: 's=4010900',
6fa5b4 43       theme: 'modern',
66df08 44       language: config.lang,
681ba6 45       content_css: rcmail.assets_path('program/js/tinymce/roundcube/content.css'),
6fa5b4 46       menubar: false,
AM 47       statusbar: false,
ccb417 48       toolbar_items_size: 'small',
2d889e 49       extended_valid_elements: 'font[face|size|color|style],span[id|class|align|style]',
e0a5ce 50       relative_urls: false,
A 51       remove_script_host: false,
2d889e 52       convert_urls: false, // #1486944
89d6ce 53       image_description: false,
AM 54       paste_webkit_style: "color font-size font-family",
815804 55       paste_data_images: true,
VB 56       browser_spellcheck: true
2d889e 57     };
A 58
646b64 59   // register spellchecker for plain text editor
AM 60   this.spellcheck_observer = function() {};
61   if (config.spellchecker) {
62     this.spellchecker = config.spellchecker;
63     if (config.spellcheck_observer) {
64       this.spellchecker.spelling_state_observer = this.spellcheck_observer = config.spellcheck_observer;
65     }
66   }
67
b0bb38 68   // secure spellchecker requests with Roundcube token
AM 69   // Note: must be registered only once (#1490311)
70   if (!tinymce.registered_request_token) {
71     tinymce.registered_request_token = true;
72     tinymce.util.XHR.on('beforeSend', function(e) {
73       e.xhr.setRequestHeader('X-Roundcube-Request', rcmail.env.request_token);
74     });
75   }
76
646b64 77   // minimal editor
AM 78   if (config.mode == 'identity') {
2d889e 79     $.extend(conf, {
3cc1af 80       plugins: 'autolink charmap code colorpicker hr image link paste tabfocus textcolor',
6fa5b4 81       toolbar: 'bold italic underline alignleft aligncenter alignright alignjustify'
3cc1af 82         + ' | outdent indent charmap hr link unlink image code forecolor'
AM 83         + ' | fontselect fontsizeselect',
84       file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); },
85       file_browser_callback_types: 'image'
b8ae50 86     });
646b64 87   }
AM 88   // full-featured editor
89   else {
2d889e 90     $.extend(conf, {
3e7536 91       plugins: 'autolink charmap code colorpicker directionality emoticons link image media nonbreaking'
AM 92         + ' paste table tabfocus textcolor searchreplace' + (config.spellcheck ? ' spellchecker' : ''),
6fa5b4 93       toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify'
89d6ce 94         + ' | bullist numlist outdent indent ltr rtl blockquote | forecolor backcolor | fontselect fontsizeselect'
AM 95         + ' | link unlink table | emoticons charmap image media | code searchreplace undo redo',
681ba6 96       spellchecker_rpc_url: abs_url + '/?_task=utils&_action=spell_html&_remote=1',
4d0238 97       spellchecker_language: rcmail.env.spell_lang,
2d889e 98       accessibility_focus: false,
646b64 99       file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); },
b21f8b 100       // @todo: support more than image (types: file, image, media)
c5bfe6 101       file_browser_callback_types: 'image media'
b8ae50 102     });
4be86f 103   }
A 104
2d889e 105   // support external configuration settings e.g. from skin
A 106   if (window.rcmail_editor_settings)
107     $.extend(conf, window.rcmail_editor_settings);
108
646b64 109   conf.setup = function(ed) {
AM 110     ed.on('init', function(ed) { ref.init_callback(ed); });
111     // add handler for spellcheck button state update
112     ed.on('SpellcheckStart SpellcheckEnd', function(args) {
113       ref.spellcheck_active = args.type == 'spellcheckstart';
114       ref.spellcheck_observer();
115     });
116     ed.on('keypress', function() {
117       rcmail.compose_type_activity++;
a8f4d8 118     });
646b64 119   };
AM 120
121   // textarea identifier
122   this.id = id;
123   // reference to active editor (if in HTML mode)
124   this.editor = null;
125
6fa5b4 126   tinymce.init(conf);
b8ae50 127
646b64 128   // react to real individual tinyMCE editor init
AM 129   this.init_callback = function(event)
130   {
131     this.editor = event.target;
c288f9 132
646b64 133     if (rcmail.env.action != 'compose') {
AM 134       return;
43fb35 135     }
c288f9 136
a172f3 137     var area = $('#' + this.id),
AM 138       height = $('div.mce-toolbar-grp:first', area.parent()).height();
139
140     // the editor might be still not fully loaded, making the editing area
141     // inaccessible, wait and try again (#1490310)
142     if (height > 200 || height > area.height()) {
143       return setTimeout(function () { ref.init_callback(event); }, 300);
144     }
145
646b64 146     var css = {},
AM 147       elem = rcube_find_object('_from'),
148       fe = rcmail.env.compose_focus_elem;
665cc5 149
646b64 150     if (rcmail.env.default_font)
AM 151       css['font-family'] = rcmail.env.default_font;
152
153     if (rcmail.env.default_font_size)
154       css['font-size'] = rcmail.env.default_font_size;
155
156     if (css['font-family'] || css['font-size'])
157       $(this.editor.getBody()).css(css);
158
159     if (elem && elem.type == 'select-one') {
160       // insert signature (only for the first time)
161       if (!rcmail.env.identities_initialized)
162         rcmail.change_identity(elem);
163
164       // Focus previously focused element
165       if (fe && fe.id != this.id) {
a172f3 166         window.focus(); // for WebKit (#1486674)
AM 167         fe.focus();
168         rcmail.env.compose_focus_elem = null;
646b64 169       }
AM 170     }
171
a172f3 172     // set tabIndex and set focus to element that was focused before
AM 173     ref.tabindex(fe && fe.id == ref.id);
174     // Trigger resize (needed for proper editor resizing in some browsers)
175     $(window).resize();
646b64 176   };
AM 177
178   // set tabIndex on tinymce editor
179   this.tabindex = function(focus)
180   {
181     if (rcmail.env.task == 'mail' && this.editor) {
182       var textarea = this.editor.getElement(),
aa1ddf 183         body = this.editor.getBody(),
646b64 184         node = this.editor.getContentAreaContainer().childNodes[0];
89d6ce 185
4cdc70 186       if (textarea && node)
A 187         node.tabIndex = textarea.tabIndex;
23c00e 188
TB 189       // find :prev and :next elements to get focus when tabbing away
190       if (textarea.tabIndex > 0) {
191         var x = null,
192           tabfocus_elements = [':prev',':next'],
193           el = tinymce.DOM.select('*[tabindex='+textarea.tabIndex+']:not(iframe)');
aa1ddf 194
AM 195         tinymce.each(el, function(e, i) { if (e.id == ref.id) { x = i; return false; } });
23c00e 196         if (x !== null) {
TB 197           if (el[x-1] && el[x-1].id) {
198             tabfocus_elements[0] = el[x-1].id;
199           }
200           if (el[x+1] && el[x+1].id) {
201             tabfocus_elements[1] = el[x+1].id;
202           }
aa1ddf 203           this.editor.settings.tabfocus_elements = tabfocus_elements.join(',');
23c00e 204         }
TB 205       }
aa1ddf 206
AM 207       // ContentEditable reset fixes invisible cursor issue in Firefox < 25
208       if (bw.mz && bw.vendver < 25)
209         $(body).prop('contenteditable', false).prop('contenteditable', true);
210
211       if (focus)
212         body.focus();
4cdc70 213     }
646b64 214   };
b8ae50 215
646b64 216   // switch html/plain mode
7d3be1 217   this.toggle = function(ishtml, noconvert)
646b64 218   {
AM 219     var curr, content, result,
220       // these non-printable chars are not removed on text2html and html2text
221       // we can use them as temp signature replacement
222       sig_mark = "\u0002\u0003",
223       input = $('#' + this.id),
224       signature = rcmail.env.identity ? rcmail.env.signatures[rcmail.env.identity] : null,
225       is_sig = signature && signature.text && signature.text.length > 1;
5821ff 226
646b64 227     // apply spellcheck changes if spell checker is active
AM 228     this.spellcheck_stop();
8f142e 229
646b64 230     if (ishtml) {
AM 231       content = input.val();
8f142e 232
646b64 233       // replace current text signature with temp mark
b0c902 234       if (is_sig) {
AM 235         content = content.replace(/\r\n/, "\n");
236         content = content.replace(signature.text.replace(/\r\n/, "\n"), sig_mark);
237       }
b21f8b 238
7d3be1 239       var init_editor = function(data) {
646b64 240         // replace signature mark with html version of the signature
AM 241         if (is_sig)
242           data = data.replace(sig_mark, '<div id="_rc_sig">' + signature.html + '</div>');
b21f8b 243
646b64 244         input.val(data);
AM 245         tinymce.execCommand('mceAddEditor', false, ref.id);
246
a172f3 247         if (ref.editor) {
AM 248           var body = $(ref.editor.getBody());
249           // #1486593
250           ref.tabindex(true);
251           // put cursor on start of the compose body
252           ref.editor.selection.setCursorLocation(body.children().first().get(0));
253         }
7d3be1 254       };
TB 255
256       // convert to html
257       if (!noconvert) {
258         result = rcmail.plain2html(content, init_editor);
259       }
260       else {
261         init_editor(content);
262         result = true;
263       }
8f142e 264     }
646b64 265     else if (this.editor) {
AM 266       if (is_sig) {
267         // get current version of signature, we'll need it in
268         // case of html2text conversion abort
269         if (curr = this.editor.dom.get('_rc_sig'))
270           curr = curr.innerHTML;
8f142e 271
646b64 272         // replace current signature with some non-printable characters
AM 273         // we use non-printable characters, because this replacement
274         // is visible to the user
275         // doing this after getContent() would be hard
276         this.editor.dom.setHTML('_rc_sig', sig_mark);
b21f8b 277       }
AM 278
646b64 279       // get html content
AM 280       content = this.editor.getContent();
b21f8b 281
7d3be1 282       var init_plaintext = function(data) {
646b64 283         tinymce.execCommand('mceRemoveEditor', false, ref.id);
AM 284         ref.editor = null;
b21f8b 285
646b64 286         // replace signture mark with text version of the signature
AM 287         if (is_sig)
288           data = data.replace(sig_mark, "\n" + signature.text);
b21f8b 289
646b64 290         input.val(data).focus();
aa1ddf 291         rcmail.set_caret_pos(input.get(0), 0);
7d3be1 292       };
TB 293
294       // convert html to text
295       if (!noconvert) {
296         result = rcmail.html2plain(content, init_plaintext);
297       }
298       else {
299         init_plaintext(input.val());
300         result = true;
301       }
b21f8b 302
646b64 303       // bring back current signature
AM 304       if (!result && curr)
305         this.editor.dom.setHTML('_rc_sig', curr);
306     }
b21f8b 307
646b64 308     return result;
AM 309   };
b21f8b 310
646b64 311   // start spellchecker
AM 312   this.spellcheck_start = function()
313   {
314     if (this.editor) {
315       tinymce.execCommand('mceSpellCheck', true);
316       this.spellcheck_observer();
317     }
318     else if (this.spellchecker && this.spellchecker.spellCheck) {
319       this.spellchecker.spellCheck();
320     }
321   };
b21f8b 322
646b64 323   // stop spellchecker
AM 324   this.spellcheck_stop = function()
325   {
326     var ed = this.editor;
b21f8b 327
646b64 328     if (ed) {
a8f4d8 329       if (ed.plugins && ed.plugins.spellchecker && this.spellcheck_active) {
AM 330         ed.execCommand('mceSpellCheck', false);
646b64 331         this.spellcheck_observer();
a8f4d8 332       }
646b64 333     }
AM 334     else if (ed = this.spellchecker) {
335       if (ed.state && ed.state != 'ready' && ed.state != 'no_error_found')
336         $(ed.spell_span).trigger('click');
337     }
338   };
b21f8b 339
646b64 340   // spellchecker state
AM 341   this.spellcheck_state = function()
342   {
343     var ed;
b21f8b 344
646b64 345     if (this.editor)
AM 346       return this.spellcheck_active;
347     else if ((ed = this.spellchecker) && ed.state)
348       return ed.state != 'ready' && ed.state != 'no_error_found';
349   };
b21f8b 350
a8f4d8 351   // resume spellchecking, highlight provided mispellings without a new ajax request
646b64 352   this.spellcheck_resume = function(data)
AM 353   {
354     var ed = this.editor;
355
356     if (ed) {
a8f4d8 357       ed.plugins.spellchecker.markErrors(data);
646b64 358     }
AM 359     else if (ed = this.spellchecker) {
360       ed.prepare(false, true);
361       ed.processData(data);
362     }
363   };
364
365   // get selected (spellcheker) language
366   this.get_language = function()
367   {
368     if (this.editor) {
369       return this.editor.settings.spellchecker_language || rcmail.env.spell_lang;
370     }
371     else if (this.spellchecker) {
372       return GOOGIE_CUR_LANG;
373     }
374   };
375
376   // set language for spellchecking
377   this.set_language = function(lang)
378   {
379     var ed = this.editor;
380
381     if (ed) {
382       ed.settings.spellchecker_language = lang;
383     }
384     if (ed = this.spellchecker) {
385       ed.setCurrentLanguage(lang);
386     }
387   };
388
389   // replace selection with text snippet
390   this.replace = function(text)
391   {
392     var ed = this.editor;
393
394     // insert into tinymce editor
395     if (ed) {
396       ed.getWin().focus(); // correct focus in IE & Chrome
397       ed.selection.setContent(rcmail.quote_html(text).replace(/\r?\n/g, '<br/>'), { format:'text' });
398     }
399     // replace selection in compose textarea
400     else if (ed = rcube_find_object(this.id)) {
401       var selection = $(ed).is(':focus') ? rcmail.get_input_selection(ed) : { start:0, end:0 },
402         inp_value = ed.value;
403         pre = inp_value.substring(0, selection.start),
404         end = inp_value.substring(selection.end, inp_value.length);
405
406       // insert response text
407       ed.value = pre + text + end;
408
409       // set caret after inserted text
410       rcmail.set_caret_pos(ed, selection.start + text.length);
411       ed.focus();
412     }
413   };
414
415   // get selected text (if no selection returns all text) from the editor
45bfde 416   this.get_content = function(args)
646b64 417   {
45bfde 418     var sigstart, ed = this.editor, text = '', strip = false,
AM 419       defaults = {refresh: true, selection: false, nosig: false, format: 'html'};
646b64 420
45bfde 421     args = $.extend(defaults, args);
AM 422
423     // apply spellcheck changes if spell checker is active
424     if (args.refresh) {
425       this.spellcheck_stop();
426     }
646b64 427
AM 428     // get selected text from tinymce editor
429     if (ed) {
430       ed.getWin().focus(); // correct focus in IE & Chrome
45bfde 431       if (args.selection)
AM 432         text = ed.selection.getContent({format: args.format});
646b64 433
AM 434       if (!text) {
45bfde 435         text = ed.getContent({format: args.format});
AM 436         // @todo: strip signature in html mode
437         strip = args.format == 'text';
646b64 438       }
AM 439     }
440     // get selected text from compose textarea
441     else if (ed = rcube_find_object(this.id)) {
45bfde 442       if (args.selection && $(ed).is(':focus')) {
646b64 443         text = rcmail.get_input_selection(ed).text;
AM 444       }
445
446       if (!text) {
447         text = ed.value;
448         strip = true;
449       }
450     }
451
452     // strip off signature
45bfde 453     // @todo: make this optional
AM 454     if (strip && args.nosig) {
646b64 455       sigstart = text.indexOf('-- \n');
AM 456       if (sigstart > 0) {
457         text = text.substring(0, sigstart);
458       }
459     }
460
461     return text;
462   };
463
464   // change user signature text
465   this.change_signature = function(id, show_sig)
466   {
aa1ddf 467     var position_element, cursor_pos, p = -1,
646b64 468       input_message = $('#' + this.id),
AM 469       message = input_message.val(),
470       sig = rcmail.env.identity;
471
472     if (!this.editor) { // plain text mode
473       // remove the 'old' signature
474       if (show_sig && sig && rcmail.env.signatures && rcmail.env.signatures[sig]) {
475         sig = rcmail.env.signatures[sig].text;
476         sig = sig.replace(/\r\n/g, '\n');
477
478         p = rcmail.env.top_posting ? message.indexOf(sig) : message.lastIndexOf(sig);
479         if (p >= 0)
480           message = message.substring(0, p) + message.substring(p+sig.length, message.length);
481       }
aa1ddf 482
646b64 483       // add the new signature string
AM 484       if (show_sig && rcmail.env.signatures && rcmail.env.signatures[id]) {
485         sig = rcmail.env.signatures[id].text;
486         sig = sig.replace(/\r\n/g, '\n');
487
09225a 488         // in place of removed signature
AM 489         if (p >= 0) {
490           message = message.substring(0, p) + sig + message.substring(p, message.length);
491           cursor_pos = p - 1;
492         }
493         // empty message
494         else if (!message) {
495           message = '\n\n' + sig;
496           cursor_pos = 0;
497         }
498         else if (rcmail.env.top_posting && !rcmail.env.sig_below) {
499           // at cursor position
500           if (pos = rcmail.get_caret_pos(input_message.get(0))) {
646b64 501             message = message.substring(0, pos) + '\n' + sig + '\n\n' + message.substring(pos, message.length);
AM 502             cursor_pos = pos;
503           }
09225a 504           // on top
AM 505           else {
646b64 506             message = '\n\n' + sig + '\n\n' + message.replace(/^[\r\n]+/, '');
09225a 507             cursor_pos = 0;
646b64 508           }
AM 509         }
510         else {
511           message = message.replace(/[\r\n]+$/, '');
09225a 512           cursor_pos = !rcmail.env.top_posting && message.length ? message.length + 1 : 0;
646b64 513           message += '\n\n' + sig;
AM 514         }
515       }
09225a 516       else {
646b64 517         cursor_pos = rcmail.env.top_posting ? 0 : message.length;
09225a 518       }
646b64 519
AM 520       input_message.val(message);
521
522       // move cursor before the signature
523       rcmail.set_caret_pos(input_message.get(0), cursor_pos);
524     }
525     else if (show_sig && rcmail.env.signatures) {  // html
526       var sigElem = this.editor.dom.get('_rc_sig');
527
528       // Append the signature as a div within the body
529       if (!sigElem) {
530         var body = this.editor.getBody(),
531           doc = this.editor.getDoc();
532
533         sigElem = doc.createElement('div');
534         sigElem.setAttribute('id', '_rc_sig');
535
09225a 536         if (rcmail.env.top_posting && !rcmail.env.sig_below) {
646b64 537           this.editor.getWin().focus(); // correct focus in IE & Chrome
AM 538
539           var node = this.editor.selection.getNode();
09225a 540
AM 541           // insert at start or at cursor position if found
542           body.insertBefore(sigElem, node.nodeName == 'BODY' ? body.firstChild : node.nextSibling);
543           body.insertBefore(doc.createElement('p'), sigElem);
646b64 544         }
AM 545         else {
546           body.appendChild(sigElem);
09225a 547           position_element = rcmail.env.top_posting ? body.firstChild : $(sigElem).prev();
646b64 548         }
b21f8b 549       }
646b64 550
8449e9 551       sigElem.innerHTML = rcmail.env.signatures[id] ? rcmail.env.signatures[id].html : '';
646b64 552     }
aa1ddf 553     else if (!rcmail.env.top_posting) {
AM 554       position_element = $(this.editor.getBody()).children().last();
555     }
556
557     // put cursor before signature and scroll the window
558     if (this.editor && position_element && position_element.length) {
559       this.editor.selection.setCursorLocation(position_element.get(0));
560       this.editor.getWin().scroll(0, position_element.offset().top);
561     }
646b64 562   };
AM 563
564   // trigger content save
565   this.save = function()
566   {
567     if (this.editor) {
568       this.editor.save();
569     }
570   };
571
572   // focus the editing area
573   this.focus = function()
574   {
575     (this.editor || rcube_find_object(this.id)).focus();
576   };
577
578   // image selector
579   this.file_browser_callback = function(field_name, url, type)
580   {
5b2311 581     var i, elem, cancel, dialog, fn, list = [];
646b64 582
AM 583     // open image selector dialog
584     dialog = this.editor.windowManager.open({
585       title: rcmail.gettext('select' + type),
586       width: 500,
587       height: 300,
588       html: '<div id="image-selector-list"><ul></ul></div>'
5b2311 589         + '<div id="image-selector-form"><div id="image-upload-button" class="mce-widget mce-btn" role="button" tabindex="0"></div></div>',
646b64 590       buttons: [{text: 'Cancel', onclick: function() { ref.file_browser_close(); }}]
AM 591     });
592
593     rcmail.env.file_browser_field = field_name;
594     rcmail.env.file_browser_type = type;
595
596     // fill images list with available images
597     for (i in rcmail.env.attachments) {
598       if (elem = ref.file_browser_entry(i, rcmail.env.attachments[i])) {
599         list.push(elem);
600       }
601     }
602
603     if (list.length) {
5b2311 604       $('#image-selector-list > ul').append(list).find('li:first').focus();
646b64 605     }
AM 606
607     // add hint about max file size (in dialog footer)
5b2311 608     $('div.mce-abs-end', dialog.getEl()).append($('<div class="hint">')
AM 609       .text($('div.hint', rcmail.gui_objects.uploadform).text()));
610
611     // init upload button
612     elem = $('#image-upload-button').append($('<span>').text(rcmail.gettext('add' + type)));
613     cancel = elem.parents('.mce-panel').find('button:last').parent();
614
615     // we need custom Tab key handlers, until we find out why
616     // tabindex do not work here as expected
617     elem.keydown(function(e) {
618       if (e.which == 9) {
619         // on Tab + Shift focus first file
620         if (rcube_event.get_modifier(e) == SHIFT_KEY)
621           $('#image-selector-list li:last').focus();
622         // on Tab focus Cancel button
623         else
624           cancel.focus();
625
626         return false;
627       }
628     });
629     cancel.keydown(function(e) {
630       if (e.which == 9) {
631         // on Tab + Shift focus upload button
632         if (rcube_event.get_modifier(e) == SHIFT_KEY)
633           elem.focus();
634         else
635           $('#image-selector-list li:first').focus();
636
637         return false;
638       }
639     });
646b64 640
AM 641     // enable (smart) upload button
642     this.hack_file_input(elem, rcmail.gui_objects.uploadform);
643
644     // enable drag-n-drop area
9fa836 645     if ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData) {
AM 646       if (!rcmail.env.filedrop) {
647         rcmail.env.filedrop = {};
648       }
649       if (rcmail.gui_objects.filedrop) {
650         rcmail.env.old_file_drop = rcmail.gui_objects.filedrop;
651       }
652
646b64 653       rcmail.gui_objects.filedrop = $('#image-selector-form');
AM 654       rcmail.gui_objects.filedrop.addClass('droptarget')
655         .bind('dragover dragleave', function(e) {
656           e.preventDefault();
657           e.stopPropagation();
658           $(this)[(e.type == 'dragover' ? 'addClass' : 'removeClass')]('hover');
659         })
660         .get(0).addEventListener('drop', function(e) { return rcmail.file_dropped(e); }, false);
661     }
662
663     // register handler for successful file upload
664     if (!rcmail.env.file_dialog_event) {
665       rcmail.env.file_dialog_event = true;
666       rcmail.addEventListener('fileuploaded', function(attr) {
667         var elem;
668         if (elem = ref.file_browser_entry(attr.name, attr.attachment)) {
669           $('#image-selector-list > ul').prepend(elem);
5b2311 670           elem.focus();
646b64 671         }
AM 672       });
673     }
3cc1af 674
AM 675     // @todo: upload progress indicator
646b64 676   };
AM 677
678   // close file browser window
679   this.file_browser_close = function(url)
680   {
5b2311 681     var input = $('#' + rcmail.env.file_browser_field);
AM 682
646b64 683     if (url)
5b2311 684       input.val(url);
646b64 685
AM 686     this.editor.windowManager.close();
5b2311 687
AM 688     input.focus();
646b64 689
AM 690     if (rcmail.env.old_file_drop)
691       rcmail.gui_objects.filedrop = rcmail.env.old_file_drop;
692   };
693
694   // creates file browser entry
695   this.file_browser_entry = function(file_id, file)
696   {
697     if (!file.complete || !file.mimetype) {
698       return;
9fa836 699     }
AM 700
701     if (rcmail.file_upload_id) {
702       rcmail.set_busy(false, null, rcmail.file_upload_id);
646b64 703     }
AM 704
c5bfe6 705     var rx, img_src;
AM 706
707     switch (rcmail.env.file_browser_type) {
708       case 'image':
709         rx = /^image\//i;
710         break;
711
712       case 'media':
713         rx = /^video\//i;
714         img_src = 'program/js/tinymce/roundcube/video.png';
715         break;
716
717       default:
718         return;
719     }
720
721     if (rx.test(file.mimetype)) {
3cc1af 722       var path = rcmail.env.comm_path + '&_from=' + rcmail.env.action,
AM 723         action = rcmail.env.compose_id ? '&_id=' + rcmail.env.compose_id + '&_action=display-attachment' : '&_action=upload-display',
724         href = path + action + '&_file=' + file_id,
c5bfe6 725         img = $('<img>').attr({title: file.name, src: img_src ? img_src : href + '&_thumbnail=1'});
646b64 726
5b2311 727       return $('<li>').attr({tabindex: 0})
AM 728         .data('url', href)
646b64 729         .append($('<span class="img">').append(img))
AM 730         .append($('<span class="name">').text(file.name))
5b2311 731         .click(function() { ref.file_browser_close($(this).data('url')); })
AM 732         .keydown(function(e) {
733           if (e.which == 13) {
734             ref.file_browser_close($(this).data('url'));
735           }
736           // we need custom Tab key handlers, until we find out why
737           // tabindex do not work here as expected
738           else if (e.which == 9) {
739             if (rcube_event.get_modifier(e) == SHIFT_KEY) {
740               if (!$(this).prev().focus().length)
741                 $('#image-upload-button').parents('.mce-panel').find('button:last').parent().focus();
742             }
743             else {
744               if (!$(this).next().focus().length)
745                 $('#image-upload-button').focus();
746             }
747
748             return false;
749           }
750         });
646b64 751     }
AM 752   };
753
754   // create smart files upload button
755   this.hack_file_input = function(elem, clone_form)
756   {
757     var link = $(elem),
9fa836 758       file = $('<input>').attr('name', '_file[]'),
646b64 759       form = $('<form>').attr({method: 'post', enctype: 'multipart/form-data'}),
AM 760       offset = link.offset();
761
762     // clone existing upload form
763     if (clone_form) {
764       file.attr('name', $('input[type="file"]', clone_form).attr('name'));
765       form.attr('action', $(clone_form).attr('action'))
766         .append($('<input>').attr({type: 'hidden', name: '_token', value: rcmail.env.request_token}));
767     }
768
769     function move_file_input(e) {
770       file.css({top: (e.pageY - offset.top - 10) + 'px', left: (e.pageX - offset.left - 10) + 'px'});
771     }
772
5b2311 773     file.attr({type: 'file', multiple: 'multiple', size: 5, title: '', tabindex: -1})
646b64 774       .change(function() { rcmail.upload_file(form, 'upload'); })
AM 775       .click(function() { setTimeout(function() { link.mouseleave(); }, 20); })
776       // opacity:0 does the trick, display/visibility doesn't work
777       .css({opacity: 0, cursor: 'pointer', position: 'relative', outline: 'none'})
778       .appendTo(form);
779
780     // In FF and IE we need to move the browser file-input's button under the cursor
781     // Thanks to the size attribute above we know the length of the input field
782     if (navigator.userAgent.match(/Firefox|MSIE/))
783       file.css({marginLeft: '-80px'});
784
785     // Note: now, I observe problem with cursor style on FF < 4 only
786     link.css({overflow: 'hidden', cursor: 'pointer'})
787       .mouseenter(function() { this.__active = true; })
788       // place button under the cursor
789       .mousemove(function(e) {
790         if (this.__active)
791           move_file_input(e);
792         // move the input away if button is disabled
793         else
794           $(this).mouseleave();
795       })
796       .mouseleave(function() {
797         file.css({top: '-10000px', left: '-10000px'});
798         this.__active = false;
799       })
800       .click(function(e) {
801         // forward click if mouse-enter event was missed
802         if (!this.__active) {
803           this.__active = true;
804           move_file_input(e);
805           file.trigger(e);
806         }
807       })
5b2311 808       .keydown(function(e) {
AM 809         if (e.which == 13) file.trigger('click');
810       })
646b64 811       .mouseleave()
AM 812       .append(form);
813   };
89d6ce 814 }