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