From cd3cad6aae1163dbe2af837d227d88c491c5c6a3 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sat, 11 Jul 2020 18:46:17 +0200 Subject: [PATCH] TinyMCE 5 (#7403) --- jsdeps.json | 16 +- plugins/emoticons/composer.json | 2 +- plugins/emoticons/emoticons.php | 128 ++- plugins/emoticons/emoticons_engine.php | 152 --- plugins/emoticons/localization/en_US.inc | 2 - plugins/emoticons/tests/EmoticonsEngine.php | 48 - program/include/rcmail.php | 16 +- program/js/editor.js | 102 +- program/resources/tinymce/browser.css | 33 +- skins/classic/common.css | 39 +- skins/classic/functions.js | 2 +- skins/classic/mail.css | 10 +- skins/elastic/styles/colors.less | 4 + skins/elastic/styles/widgets/dialogs.less | 1 + skins/elastic/styles/widgets/editor.less | 998 ++++++-------------- skins/elastic/ui.js | 151 +-- skins/larry/mail.css | 11 +- skins/larry/styles.css | 50 +- skins/larry/ui.js | 4 +- tests/Browser/Components/HtmlEditor.php | 8 +- tests/phpunit.xml | 1 - 21 files changed, 509 insertions(+), 1269 deletions(-) delete mode 100644 plugins/emoticons/emoticons_engine.php delete mode 100644 plugins/emoticons/tests/EmoticonsEngine.php diff --git a/jsdeps.json b/jsdeps.json index ba486deba..c1d3179f4 100644 --- a/jsdeps.json +++ b/jsdeps.json @@ -36,28 +36,26 @@ }, { "lib": "tinymce", - "version": "4.8.2", + "version": "5.4.1", "url": "https://download.tiny.cloud/tinymce/community/tinymce_$v.zip", "dest": "program/js", - "sha1": "d7fced05acdeeb78299585ea9909b0de2b3d759d", + "sha1": "1266dcbbc6d13fa789b3443a63acff4b75f5a911", "license": "LGPL", - "copyright": "Copyright (c) 1999-2015 Ephox Corp. All rights reserved", + "copyright": "Copyright (c) Tiny Technologies, Inc. All rights reserved", "rm": "program/js/tinymce", "map": { "js/tinymce": "tinymce" }, "omit": [ "tinymce/license.txt", - "tinymce/jquery.tinymce.min.js" - ], - "addlicense": [ - "tinymce/tinymce.min.js" + "tinymce/jquery.tinymce.min.js", + "tinymce/themes/mobile" ] }, { "lib": "tinymce-langs", - "version": "4.8.2", - "url": "https://www.tiny.cloud/docs-4x/language/tinymce4x_languages.zip", + "version": "5.4.1", + "url": "https://www.tiny.cloud/tinymce-services-azure/1/i18n/download?langs=ar,hy,az,eu,be,bs,bg_BG,ca,zh_CN,zh_TW,hr,cs,cs_CZ,da,nl,en_CA,en_GB,eo,et,fo,fi,fr_FR,fr_CH,gd,gl,ka_GE,de,de_AT,el,he_IL,hi_IN,hu_HU,is_IS,id,ga,it,ja,kab,km_KH,ko_KR,ku,ku_IQ,lv,lt,lb,mk_MK,ml_IN,nb_NO,oc,fa,fa_IR,pl,pt_BR,pt_PT,ro,ru,sk,sl_SI,es,es_MX,sv_SE,tg,ta,ta_IN,tt,th_TH,tr,tr_TR,ug,uk,uk_UA,vi,vi_VN,cy&v=$v&extension=.zip", "dest": "program/js/tinymce" }, { diff --git a/plugins/emoticons/composer.json b/plugins/emoticons/composer.json index 30dd3928e..fa0f41ea5 100644 --- a/plugins/emoticons/composer.json +++ b/plugins/emoticons/composer.json @@ -3,7 +3,7 @@ "type": "roundcube-plugin", "description": "Plugin that adds emoticons support.", "license": "GPLv3+", - "version": "2.0", + "version": "3.0", "authors": [ { "name": "Thomas Bruederli", diff --git a/plugins/emoticons/emoticons.php b/plugins/emoticons/emoticons.php index 4eefed873..bc76017eb 100644 --- a/plugins/emoticons/emoticons.php +++ b/plugins/emoticons/emoticons.php @@ -1,15 +1,15 @@ add_hook('message_part_after', array($this, 'message_part_after')); - $this->add_hook('message_outgoing_body', array($this, 'message_outgoing_body')); - $this->add_hook('html2text', array($this, 'html2text')); $this->add_hook('html_editor', array($this, 'html_editor')); if ($rcube->task == 'settings') { @@ -35,8 +33,8 @@ class emoticons extends rcube_plugin } /** - * 'message_part_after' hook handler to replace common plain text emoticons - * with emoticon images () + * 'message_part_after' hook handler to replace common + * plain text emoticons with emoji */ function message_part_after($args) { @@ -48,65 +46,7 @@ class emoticons extends rcube_plugin return $args; } - require_once __DIR__ . '/emoticons_engine.php'; - - $args['body'] = emoticons_engine::text2icons($args['body']); - } - - return $args; - } - - /** - * 'message_outgoing_body' hook handler to replace image emoticons from TinyMCE - * editor with image attachments. - */ - function message_outgoing_body($args) - { - if ($args['type'] == 'html') { - $this->load_config(); - - $rcube = rcube::get_instance(); - if (!$rcube->config->get('emoticons_compose', true)) { - return $args; - } - - require_once __DIR__ . '/emoticons_engine.php'; - - // look for "emoticon" images from TinyMCE and change their src paths to - // be file paths on the server instead of URL paths. - $images = emoticons_engine::replace($args['body']); - - // add these images as attachments to the MIME message - foreach ($images as $img_name => $img_file) { - $args['message']->addHTMLImage($img_file, 'image/gif', '', true, $img_name); - } - } - - return $args; - } - - /** - * 'html2text' hook handler to replace image emoticons from TinyMCE - * editor with plain text emoticons. - * - * This is executed on html2text action, i.e. when switching from HTML to text - * in compose window (or similar place). Also when generating alternative - * text/plain part. - */ - function html2text($args) - { - $rcube = rcube::get_instance(); - - if ($rcube->action == 'html2text' || $rcube->action == 'send') { - $this->load_config(); - - if (!$rcube->config->get('emoticons_compose', true)) { - return $args; - } - - require_once __DIR__ . '/emoticons_engine.php'; - - $args['body'] = emoticons_engine::icons2text($args['body']); + $args['body'] = self::text2icons($args['body']); } return $args; @@ -170,16 +110,58 @@ class emoticons extends rcube_plugin */ function preferences_save($args) { - $rcube = rcube::get_instance(); - $dont_override = $rcube->config->get('dont_override', array()); - - if ($args['section'] == 'mailview' && !in_array('emoticons_display', $dont_override)) { - $args['prefs']['emoticons_display'] = rcube_utils::get_input_value('_emoticons_display', rcube_utils::INPUT_POST) ? true : false; + if ($args['section'] == 'mailview') { + $args['prefs']['emoticons_display'] = !empty(rcube_utils::get_input_value('_emoticons_display', rcube_utils::INPUT_POST)); } - else if ($args['section'] == 'compose' && !in_array('emoticons_compose', $dont_override)) { - $args['prefs']['emoticons_compose'] = rcube_utils::get_input_value('_emoticons_compose', rcube_utils::INPUT_POST) ? true : false; + else if ($args['section'] == 'compose') { + $args['prefs']['emoticons_compose'] = !empty(rcube_utils::get_input_value('_emoticons_compose', rcube_utils::INPUT_POST)); } return $args; } + + /** + * Replace common plain text emoticons with emoji + * + * @param string $text Text + * + * @return string Converted text + */ + protected static function text2icons($text) + { + // This is a lookbehind assertion which will exclude html entities + // E.g. situation when ";)" in "")" shouldn't be replaced by the icon + // It's so long because of assertion format restrictions + $entity = '(? self::ico_tag('1f603', ':D' ), // laugh + '/:-?\(/' => self::ico_tag('1f626', ':(' ), // frown + '/'.$entity.';-?\)/' => self::ico_tag('1f609', ';)' ), // wink + '/8-?\)/' => self::ico_tag('1f60e', '8)' ), // cool + '/(? self::ico_tag('1f62e', ':O' ), // surprised + '/(? self::ico_tag('1f61b', ':P' ), // tongue out + '/(? self::ico_tag('1f631', ':-@' ), // yell + '/O:-?\)/i' => self::ico_tag('1f607', 'O:-)' ), // innocent + '/(? self::ico_tag('1f60a', ':-)' ), // smile + '/(? self::ico_tag('1f633', ':-$' ), // embarrassed + '/(? self::ico_tag('1f48b', ':-*' ), // kiss + '/(? self::ico_tag('1f615', ':-S' ), // undecided + ); + + return preg_replace(array_keys($map), array_values($map), $text); + } + + protected static function ico_tag($ico, $title) + { + return html::span(array('title' => $title), "&#x{$ico};"); + } } diff --git a/plugins/emoticons/emoticons_engine.php b/plugins/emoticons/emoticons_engine.php deleted file mode 100644 index 4d534cdc1..000000000 --- a/plugins/emoticons/emoticons_engine.php +++ /dev/null @@ -1,152 +0,0 @@ - 'smiley-cool', - ':-#' => 'smiley-foot-in-mouth', - ':-*' => 'smiley-kiss', - ':-X' => 'smiley-sealed', - ':-P' => 'smiley-tongue-out', - ':-@' => 'smiley-yell', - ":'(" => 'smiley-cry', - ':-(' => 'smiley-frown', - ':-D' => 'smiley-laughing', - ':-)' => 'smiley-smile', - ':-S' => 'smiley-undecided', - ':-$' => 'smiley-embarassed', - 'O:-)' => 'smiley-innocent', - ':-|' => 'smiley-money-mouth', - ':-O' => 'smiley-surprised', - ';-)' => 'smiley-wink', - ); - - foreach ($emoticons as $idx => $file) { - // Cry - $file = preg_quote(self::IMG_PATH . $file . '.gif', '/'); - $search[] = '/]+\/>/i'; - $replace[] = $idx; - } - - return preg_replace($search, $replace, $html); - } - - /** - * Replace common plain text emoticons with empticon tags - * - * @param string $text Text - * - * @return string Converted text - */ - public static function text2icons($text) - { - // This is a lookbehind assertion which will exclude html entities - // E.g. situation when ";)" in "")" shouldn't be replaced by the icon - // It's so long because of assertion format restrictions - $entity = '(? self::img_tag('smiley-laughing.gif', ':D' ), - '/:-D/' => self::img_tag('smiley-laughing.gif', ':-D' ), - '/:\(/' => self::img_tag('smiley-frown.gif', ':(' ), - '/:-\(/' => self::img_tag('smiley-frown.gif', ':-(' ), - '/'.$entity.';\)/' => self::img_tag('smiley-wink.gif', ';)' ), - '/'.$entity.';-\)/' => self::img_tag('smiley-wink.gif', ';-)' ), - '/8\)/' => self::img_tag('smiley-cool.gif', '8)' ), - '/8-\)/' => self::img_tag('smiley-cool.gif', '8-)' ), - '/(? self::img_tag('smiley-surprised.gif', ':O' ), - '/(? self::img_tag('smiley-surprised.gif', ':-O' ), - '/(? self::img_tag('smiley-tongue-out.gif', ':P' ), - '/(? self::img_tag('smiley-tongue-out.gif', ':-P' ), - '/(? self::img_tag('smiley-yell.gif', ':@' ), - '/(? self::img_tag('smiley-yell.gif', ':-@' ), - '/O:\)/i' => self::img_tag('smiley-innocent.gif', 'O:)' ), - '/O:-\)/i' => self::img_tag('smiley-innocent.gif', 'O:-)' ), - '/(? self::img_tag('smiley-smile.gif', ':)' ), - '/(? self::img_tag('smiley-smile.gif', ':-)' ), - '/(? self::img_tag('smiley-embarassed.gif', ':$' ), - '/(? self::img_tag('smiley-embarassed.gif', ':-$' ), - '/(? self::img_tag('smiley-kiss.gif', ':*' ), - '/(? self::img_tag('smiley-kiss.gif', ':-*' ), - '/(? self::img_tag('smiley-undecided.gif', ':S' ), - '/(? self::img_tag('smiley-undecided.gif', ':-S' ), - ); - - return preg_replace(array_keys($map), array_values($map), $text); - } - - protected static function img_tag($ico, $title) - { - return html::img(array('src' => './' . self::IMG_PATH . $ico, 'title' => $title)); - } - - /** - * Replace emoticon icons 'src' attribute, so it can - * be replaced with real file by Mail_Mime. - * - * @param string &$html HTML content - * - * @return array List of image files - */ - public static function replace(&$html) - { - // Replace this: - // - // with this: - // - - $rcube = rcube::get_instance(); - $assets_dir = $rcube->config->get('assets_dir'); - $path = unslashify($assets_dir ?: INSTALL_PATH) . '/' . self::IMG_PATH; - $offset = 0; - $images = array(); - - // remove any null-byte characters before parsing - $html = preg_replace('/\x00/', '', $html); - - if (preg_match_all('# src=[\'"]([^\'"]+)#', $html, $matches, PREG_OFFSET_CAPTURE)) { - foreach ($matches[1] as $m) { - // find emoticon image tags - if (preg_match('#'. self::IMG_PATH . '(.*)$#', $m[0], $imatches)) { - $image_name = $imatches[1]; - - // sanitize image name so resulting attachment doesn't leave images dir - $image_name = preg_replace('/[^a-zA-Z0-9_\.\-]/i', '', $image_name); - $image_file = $path . $image_name; - - // Add the same image only once - $images[$image_name] = $image_file; - - $html = substr_replace($html, $image_file, $m[1] + $offset, strlen($m[0])); - $offset += strlen($image_file) - strlen($m[0]); - } - } - } - - return $images; - } -} diff --git a/plugins/emoticons/localization/en_US.inc b/plugins/emoticons/localization/en_US.inc index c1ab1dab8..c1f65d566 100644 --- a/plugins/emoticons/localization/en_US.inc +++ b/plugins/emoticons/localization/en_US.inc @@ -17,5 +17,3 @@ $labels = array(); $labels['emoticonsdisplay'] = 'Display emoticons in plain text messages'; $labels['emoticonscompose'] = 'Enable emoticons'; - -?> diff --git a/plugins/emoticons/tests/EmoticonsEngine.php b/plugins/emoticons/tests/EmoticonsEngine.php deleted file mode 100644 index 58ad0668c..000000000 --- a/plugins/emoticons/tests/EmoticonsEngine.php +++ /dev/null @@ -1,48 +0,0 @@ - array('smiley-laughing.gif', ':D' ), - ':-D' => array('smiley-laughing.gif', ':-D' ), - ':(' => array('smiley-frown.gif', ':(' ), - ':-(' => array('smiley-frown.gif', ':-(' ), - '8)' => array('smiley-cool.gif', '8)' ), - '8-)' => array('smiley-cool.gif', '8-)' ), - ':O' => array('smiley-surprised.gif', ':O' ), - ':-O' => array('smiley-surprised.gif', ':-O' ), - ':P' => array('smiley-tongue-out.gif', ':P' ), - ':-P' => array('smiley-tongue-out.gif', ':-P' ), - ':@' => array('smiley-yell.gif', ':@' ), - ':-@' => array('smiley-yell.gif', ':-@' ), - 'O:)' => array('smiley-innocent.gif', 'O:)' ), - 'O:-)' => array('smiley-innocent.gif', 'O:-)' ), - ':)' => array('smiley-smile.gif', ':)' ), - ':-)' => array('smiley-smile.gif', ':-)' ), - ':$' => array('smiley-embarassed.gif', ':$' ), - ':-$' => array('smiley-embarassed.gif', ':-$' ), - ':*' => array('smiley-kiss.gif', ':*' ), - ':-*' => array('smiley-kiss.gif', ':-*' ), - ':S' => array('smiley-undecided.gif', ':S' ), - ':-S' => array('smiley-undecided.gif', ':-S' ), - ); - - foreach ($map as $body => $expected) { - $result = emoticons_engine::text2icons($body); - - $this->assertRegExp('/' . preg_quote($expected[0], '/') . '/', $result); - $this->assertRegExp('/title="' . preg_quote($expected[1], '/') . '"/', $result); - } - } -} diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 2d5ee594e..09fcd3aef 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -2174,8 +2174,22 @@ class rcmail extends rcube } } - $this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia', 'close'); + $font_family = $this->output->get_env('default_font'); + $font_size = $this->output->get_env('default_font_size'); + $style = array(); + + if ($font_family) { + $style[] = "font-family: $font_family;"; + } + if ($font_size) { + $style[] = "font-size: $font_size;"; + } + if (!empty($style)) { + $config['content_style'] = "body {" . implode(' ', $style) . "}"; + } + $this->output->set_env('editor_config', $config); + $this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia', 'close'); if ($path = $this->config->get('media_browser_css_location', 'program/resources/tinymce/browser.css')) { if ($path != 'none' && ($path = $this->find_asset($path))) { diff --git a/program/js/editor.js b/program/js/editor.js index bc504c474..891ad7ddb 100644 --- a/program/js/editor.js +++ b/program/js/editor.js @@ -39,13 +39,17 @@ function rcube_text_editor(config, id) abs_url = location.href.replace(/[?#].*$/, '').replace(/\/$/, ''), conf = { selector: '#' + ($('#' + id).is('.mce_editor') ? id : 'fake-editor-id'), - cache_suffix: 's=4080200', - theme: 'modern', + cache_suffix: 's=5040100', + theme: 'silver', language: config.lang, content_css: rcmail.assets_path(config.content_css), + content_style: config.content_style, menubar: false, statusbar: false, - toolbar_items_size: 'small', + // toolbar_sticky: true, // does not work in scrollable element: https://github.com/tinymce/tinymce/issues/5227 + toolbar_drawer: 'sliding', + toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify' + + ' | fontselect fontsizeselect | forecolor backcolor', extended_valid_elements: 'font[face|size|color|style],span[id|class|align|style]', fontsize_formats: '8pt 9pt 10pt 11pt 12pt 14pt 18pt 24pt 36pt', // Allow style tag, have to be allowed inside body/div/blockquote (#7088) @@ -55,10 +59,18 @@ function rcube_text_editor(config, id) convert_urls: false, // #1486944 image_description: false, paste_webkit_style: "color font-size font-family", + automatic_uploads: false, // allows to paste images paste_data_images: true, + // Note: We disable contextmenu options specifically for browser_spellcheck:true. + // Otherwise user would have to use Right-Click with CTRL to get to + // the browser's spellchecker options. Should you disable browser_spellcheck + // you can enable other contextmenu options (by removing these options below). browser_spellcheck: true, + contextmenu: 'spellchecker', anchor_bottom: false, - anchor_top: false + anchor_top: false, + file_picker_types: 'image media', + file_picker_callback: function(callback, value, meta) { ref.file_picker_callback(callback, value, meta); } }; // register spellchecker for plain text editor @@ -84,29 +96,22 @@ function rcube_text_editor(config, id) // minimal editor if (config.mode == 'identity') { + conf.toolbar += ' | charmap hr link unlink image code $extra'; $.extend(conf, { - plugins: 'autolink charmap code colorpicker hr image link paste tabfocus textcolor', - toolbar: 'bold italic underline alignleft aligncenter alignright alignjustify' - + ' | outdent indent charmap hr link unlink image code forecolor' - + ' | fontselect fontsizeselect', - file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); }, - file_browser_callback_types: 'image' + plugins: 'autolink charmap code hr image link paste tabfocus', + file_picker_types: 'image' }); } // full-featured editor else { - $.extend(conf, { - plugins: 'autolink charmap code colorpicker directionality link lists image media nonbreaking' - + ' paste table tabfocus textcolor searchreplace spellchecker', - toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify' - + ' | bullist numlist outdent indent ltr rtl blockquote | forecolor backcolor | fontselect fontsizeselect' + conf.toolbar += ' | bullist numlist outdent indent ltr rtl blockquote' + ' | link unlink table | $extra charmap image media | code searchreplace undo redo', + $.extend(conf, { + plugins: 'autolink charmap code directionality link lists image media nonbreaking' + + ' paste table tabfocus searchreplace spellchecker', spellchecker_rpc_url: abs_url + '/?_task=utils&_action=spell_html&_remote=1', spellchecker_language: rcmail.env.spell_lang, - accessibility_focus: false, - file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); }, - // @todo: support more than image (types: file, image, media) - file_browser_callback_types: 'image media' + min_height: 400, }); } @@ -176,7 +181,7 @@ function rcube_text_editor(config, id) if (rcmail.env.action == 'compose') { var area = $('#' + this.id), - height = $('div.mce-toolbar-grp', area.parent()).first().height(); + height = $('div.tox-toolbar__group', area.parent()).first().height(); // the editor might be still not fully loaded, making the editing area // inaccessible, wait and try again (#1490310) @@ -184,19 +189,9 @@ function rcube_text_editor(config, id) return setTimeout(function () { ref.init_callback(editor); }, 300); } - var css = {}, - elem = rcube_find_object('_from'), + var elem = rcube_find_object('_from'), fe = rcmail.env.compose_focus_elem; - if (rcmail.env.default_font) - css['font-family'] = rcmail.env.default_font; - - if (rcmail.env.default_font_size) - css['font-size'] = rcmail.env.default_font_size; - - if (css['font-family'] || css['font-size']) - $(this.editor.getBody()).css(css); - if (elem && elem.type == 'select-one') { // insert signature (only for the first time) if (!rcmail.env.identities_initialized) @@ -654,21 +649,27 @@ function rcube_text_editor(config, id) }; // image selector - this.file_browser_callback = function(field_name, url, type) + this.file_picker_callback = function(callback, value, meta) { var i, button, elem, cancel, dialog, fn, hint, list = [], + type = meta.filetype, form = $('.upload-form').clone(); // open image selector dialog this.editor.windowManager.open({ title: rcmail.get_label('select' + type), - width: 500, - html: '
', - buttons: [{text: rcmail.get_label('close'), onclick: function() { ref.file_browser_close(); }}] + body: { + type: 'panel', + items: [{ + type: 'htmlpanel', + html: '
', + }] + }, + buttons: [{type: 'cancel', text: rcmail.get_label('close'), onclick: function() { ref.file_picker_close(); }}] }); - rcmail.env.file_browser_field = field_name; - rcmail.env.file_browser_type = type; + rcmail.env.file_picker_callback = callback; + rcmail.env.file_picker_type = type; dialog = $('#image-selector'); @@ -681,14 +682,17 @@ function rcube_text_editor(config, id) .text(rcmail.get_label('add' + type)) .focus(); + if (!button.is('.btn')) + button.addClass('tox-button'); + // fill images list with available images for (i in rcmail.env.attachments) { - if (elem = ref.file_browser_entry(i, rcmail.env.attachments[i])) { + if (elem = ref.file_picker_entry(i, rcmail.env.attachments[i])) { list.push(elem); } } - cancel = dialog.parent().parent().find('button').last().parent(); + cancel = dialog.parents('.tox-dialog').find('button').last(); // Add custom Tab key handlers, tabindex does not work list = $('#image-selector-list').append(list).on('keydown', 'li', function(e) { @@ -754,7 +758,7 @@ function rcube_text_editor(config, id) rcmail.env['file_dialog_event+' + type] = true; rcmail.addEventListener('fileuploaded', function(attr) { var elem; - if (elem = ref.file_browser_entry(attr.name, attr.attachment)) { + if (elem = ref.file_picker_entry(attr.name, attr.attachment)) { list.prepend(elem); elem.focus(); } @@ -765,23 +769,19 @@ function rcube_text_editor(config, id) }; // close file browser window - this.file_browser_close = function(url) + this.file_picker_close = function(url) { - var input = $('#' + rcmail.env.file_browser_field); - - if (url) - input.val(url); - this.editor.windowManager.close(); - input.focus(); + if (url) + rcmail.env.file_picker_callback(url); if (rcmail.env.old_file_drop) rcmail.gui_objects.filedrop = rcmail.env.old_file_drop; }; // creates file browser entry - this.file_browser_entry = function(file_id, file) + this.file_picker_entry = function(file_id, file) { if (!file.complete || !file.mimetype) { return; @@ -793,7 +793,7 @@ function rcube_text_editor(config, id) var rx, img_src; - switch (rcmail.env.file_browser_type) { + switch (rcmail.env.file_picker_type) { case 'image': rx = /^image\//i; break; @@ -817,10 +817,10 @@ function rcube_text_editor(config, id) .data('url', href) .append($('').append(img)) .append($('').text(file.name)) - .click(function() { ref.file_browser_close($(this).data('url')); }) + .click(function() { ref.file_picker_close($(this).data('url')); }) .keydown(function(e) { if (e.which == 13) { - ref.file_browser_close($(this).data('url')); + ref.file_picker_close($(this).data('url')); } }); } diff --git a/program/resources/tinymce/browser.css b/program/resources/tinymce/browser.css index 320028783..1afb99174 100644 --- a/program/resources/tinymce/browser.css +++ b/program/resources/tinymce/browser.css @@ -1,20 +1,17 @@ /* This file contains the CSS data for media file selector of TinyMCE */ #image-selector { - margin: 10px; - margin-bottom: 30px; padding-bottom: 85px; + border: 1px solid transparent; } #image-selector.droptarget.hover, #image-selector.droptarget.active { border: 1px solid #019bc6; - box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5); } #image-selector.droptarget.hover { background-color: #d9ecf4; - box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9); } #image-selector form { @@ -29,36 +26,29 @@ text-align: center; } -#image-selector a.button { - color: #525252; - text-decoration: none; - font-size: 11px; -} - #image-selector .upload-form { text-align: center; margin-bottom: 1rem; } -#image-selector .upload-form button { - padding: 4px 8px; - border: 1px solid #c0c0c0; -} - #image-selector-list { overflow-x: hidden; overflow-y: auto; - margin-left: 0; + padding: 0; height: 250px; } #image-selector-list li { line-height: 80px; - padding: 2px; + padding: 3px; + padding-left: 5px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; background: none; + display:flex; + align-items: center; + margin-bottom: 1px; } #image-selector-list li:hover, @@ -72,7 +62,6 @@ } #image-selector-list li span.name { - vertical-align: middle; font-weight: bold; padding-left: 10px; } @@ -80,8 +69,14 @@ #image-selector-list li span.img { height: 80px; width: 80px; + min-width: 80px; text-align: center; - display: inline-block; overflow: hidden; line-height: 80px; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + border: 1px solid #ddd; } diff --git a/skins/classic/common.css b/skins/classic/common.css index 2c0eb97ba..7a01b0645 100644 --- a/skins/classic/common.css +++ b/skins/classic/common.css @@ -1568,42 +1568,33 @@ table.quota-info td.root { } /********** TinyMCE styles **********/ -.mce-btn-small button -{ - height: 22px; + +div.tox .tox-toolbar, +div.tox .tox-toolbar__overflow, +div.tox .tox-toolbar__primary { + background-color: #f0f0f0; } -.mce-btn-small i -{ - line-height: 16px !important; - vertical-align: text-top !important; +div.tox .tox-toolbar__primary { + border: 0; } -.mce-combobox button -{ - padding: 6px 8px !important; +div.tox div.tox-dialog-wrap__backdrop { + background: #aaa; + opacity: .3; } -.mce-tinymce -{ - border-radius: 0 !important; -{ - -.mce-panel.mce-toolbar-grp -{ - border: 0 !important; +div.tox div.tox-dialog { + box-shadow: 1px 1px 18px #666; + border-width: 0; } -#image-selector-form.droptarget { +#image-selector.droptarget { background: url(images/filedrop.png) center bottom no-repeat; } -#image-selector-form.droptarget.hover -{ +#image-selector.droptarget.hover { background-color: #F0F0EE; - box-shadow: 0 0 5px 0 #999; - -moz-box-shadow: 0 0 5px 0 #999; - -o-box-shadow: 0 0 5px 0 #999; } /** PGP key import dialog **/ diff --git a/skins/classic/functions.js b/skins/classic/functions.js index 1b5bed403..a6a5b0ae3 100644 --- a/skins/classic/functions.js +++ b/skins/classic/functions.js @@ -577,7 +577,7 @@ resize_compose_body: function() h = div.height() - 2, x = bw.ie || bw.opera ? 4 : 0; - $('#compose-body_ifr').width(w + 6).height(h - 1 - $('div.mce-toolbar').height()); + $('#compose-body_ifr').width(w + 6).height(h - 1 - $('div.tox-toolbar').height()); $('#compose-body').width(w-x).height(h); $('#googie_edit_layer').width(w).height(h); }, diff --git a/skins/classic/mail.css b/skins/classic/mail.css index 3ea340692..b11166427 100644 --- a/skins/classic/mail.css +++ b/skins/classic/mail.css @@ -1414,15 +1414,11 @@ div.hide-headers border: 1px solid #999; } -#compose-body-div .mce-tinymce { +#compose-body-div .tox-tinymce { border: 0 !important; width: 100% !important; } -.mce-top-part::before { - box-shadow: none !important; -} - #compose-div .boxlistcontent { bottom: 23px; @@ -1764,10 +1760,6 @@ input.from_address position: absolute; } -#image-selector { - padding-bottom: 0 !important; -} - /**** Styles for widescreen (3-column) view ****/ .widescreen #mailview-top { diff --git a/skins/elastic/styles/colors.less b/skins/elastic/styles/colors.less index 56a96f7a1..cb5daec1f 100644 --- a/skins/elastic/styles/colors.less +++ b/skins/elastic/styles/colors.less @@ -205,6 +205,10 @@ @color-datepicker-active-background: @color-main; +// HTML editor +@color-editor-disabled-mask: fadeout(lighten(@color-black, 85), 10); + + // Image tools @color-image-tools: #fff; @color-image-tools-background: fadeout(@color-main, 60%); diff --git a/skins/elastic/styles/widgets/dialogs.less b/skins/elastic/styles/widgets/dialogs.less index 666f94ebf..38bf2f0d0 100644 --- a/skins/elastic/styles/widgets/dialogs.less +++ b/skins/elastic/styles/widgets/dialogs.less @@ -148,6 +148,7 @@ html.touch .popover { max-width: initial; margin: 0; height: auto; + z-index: 1300; // above TinyMCE dialogs .popover-header { border-radius: .25rem .25rem 0 0 !important; diff --git a/skins/elastic/styles/widgets/editor.less b/skins/elastic/styles/widgets/editor.less index 74c2a10ff..a8c5d0df3 100644 --- a/skins/elastic/styles/widgets/editor.less +++ b/skins/elastic/styles/widgets/editor.less @@ -12,758 +12,335 @@ /*** Text Editor widget (and TinyMCE editor) ***/ -.mce-tinymce { - &.mce-container.mce-panel { +// use of div.tox instead of just .tox is to have prio over TinyMCE styles +div.tox { + font-size: 1rem; + + &, :not(.svg) { + .font-family; + } + + &.tox-tinymce { border-radius: .25rem; - border-color: @color-input-border; - overflow: hidden; - } - - .mce-btn, - .mce-panel { - background-color: @color-input-addon-background; - } - - .mce-panel { - border-color: @color-input-border; + border: 1px solid @color-input-border; } &.focused { border-color: @color-input-border-focus !important; box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow !important; } -} -.mce-top-part::before, -.mce-tinymce, -.mce-window { - box-shadow: none !important; -} + .tox-toolbar-overlord { + z-index: 1; // for sticky header feature -.mce-btn { - &.mce-active { - background: @color-btn-secondary-background !important; + & > div { + // The svg is copied from TinyMCE with modified height params + background: url("data:image/svg+xml;charset=utf8,%3Csvg height='33px' viewBox='0 0 40 33px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='32px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E"); + background-color: @color-input-addon-background; + } } -} -.mce-window { - &.mce-container { + .tox-toolbar__primary { + border-top: 0; + } + + // This one is for mobile + .tox-toolbar { + background-color: @color-input-addon-background; + } + + .tox-edit-area { border: 0; + } - & :not(.mce-ico) { - .font-family; + .tox-dialog { + border-radius: 0; + border-color: @color-layout-border; + box-shadow: none; + align-self: unset !important; + + .tox-form__group { + margin-top: 0; + margin-bottom: .75rem; + } + + .tox-dialog__body-nav-item--active { + color: @color-link; + border-color: transparent; + + &:hover { + color: @color-link-hover; + } } } - .mce-reset { - background: #fff; + .tox-dialog__body-content { + overflow: unset; } - .mce-container-body { - &.mce-abs-layout { - overflow: unset; - } - - .mce-abs-end { - display: none; - } + .tox-dialog-wrap__backdrop { + background-color: @color-dialog-overlay-background; } - .mce-window-head { - height: @layout-header-height; + .tox-dialog__header { + height: (@layout-header-height - 1px); border-bottom: 1px solid @color-dialog-header-border; + justify-content: flex-end; // fixes close button position when dialog has no title padding: 0; - .mce-title { - line-height: @layout-header-height; - font-size: 1.25rem; - padding: 0 3rem 0 1rem; + .tox-button { color: @color-dialog-header; - } - - .mce-close { - border: 0; - color: @color-dialog-header; - background: transparent; right: 0; - top: 0; - position: absolute; - height: (@layout-header-height - .75rem); - width: 1.25em; - margin: 0 .25rem; - padding: .1rem .75rem; - cursor: pointer; - outline: 0; + height: (@layout-header-height - .7rem); + width: 2.25em; + margin-right: .4rem; + + &:hover { + background: transparent; + border-color: transparent; + } + + .tox-icon { + display: none; + } &:before { &:extend(.font-icon-class); content: @fa-var-times; - margin: 0; - } - - i { - display: none; + line-height: 1.5rem; + margin: 0 !important; } } } - .mce-foot { + .tox-dialog__footer { + height: (@layout-footer-height - 1px) !important; border: 0; - height: @layout-header-height !important; - position: relative; + margin: 0; + padding: 0 1rem; @media screen and (max-width: @screen-width-xs) { border-top: 1px solid @color-dialog-header-border; } - .mce-container-body { - height: 100% !important; - display: flex; - align-items: center; - justify-content: flex-end; // just 'end' does not work in Chrome + & > div { + white-space: nowrap; + max-height: (@layout-footer-height - 1px); - .mce-btn { - position: initial; - margin-right: .5rem; - line-height: 1; - width: auto !important; - height: auto !important; - - &:last-child { - margin-right: 1rem; - } - - .mce-txt { - line-height: 1.5; - vertical-align: unset; - } - - button:before { - &:extend(.font-icon-class); - display: inline; - float: none; - vertical-align: middle; - margin-right: .4rem; - } - } - - .mce-abs-end { - position: initial; - width: 1rem; - order: 9; + button:first-child { + margin: 0; } } - .mce-btn { - .btn-secondary; - border-radius: .3rem; - border-color: transparent; - - &:focus { - border-color: transparent !important; - color: @color-btn-secondary; - background: @color-btn-secondary-background; - } - - &.mce-primary { - .btn-primary; - } - - button:hover, - button { - color: @color-btn-secondary; - padding: .5rem .75rem; - } - } - - .mce-btn:last-child button:before { - content: @fa-var-times; - } - - .mce-btn.mce-primary button:before { - content: @fa-var-check; - } - - .mce-search-foot { - div:nth-of-type(2) button:before { - content: @fa-var-search; - } - div:nth-of-type(3) button:before, - div:nth-of-type(4) button:before { - content: @fa-var-pencil-alt; - } - div:nth-of-type(6) button:before { - content: @fa-var-chevron-left; - } - div:nth-of-type(7) button:before { - content: @fa-var-chevron-right; - } - div:nth-of-type(7) button:after { - &:extend(.font-icon-class); - display: inline; - float: none; - margin: 0 0 0 .2rem; - content: @fa-var-chevron-right; - } - - @media screen and (min-width: (@screen-width-xs + 1px)) { - div:nth-of-type(6) { - margin-left: .5rem; - } - div:nth-of-type(7) button { - &:before { - display: none; - } - } - } - } - } - - // Form elements, let's keep'em in .mce-window to make overwriting simpler - - .mce-formitem { - min-width: 450px; - position: unset !important; - - & > .mce-container-body { - margin-bottom: .5rem; - - & > * { - width: 75% !important; - position: unset !important; - } - - & > label { - max-width: 25%; - min-width: 25%; - line-height: 2.5 !important; - } - } - - .mce-widget { - border-radius: .25rem; - } - } - - .mce-form { - padding: 1rem; - box-sizing: border-box; - - .mce-form { - padding: 0; - position: unset !important; - width: 100% !important; - - & > .mce-container-body { - flex-wrap: wrap; - height: auto !important; - } - - .mce-formitem { - min-width: 100%; - width: 100% !important; - } - } - - .mce-container { - height: auto !important; - - .mce-container-body { - display: flex; - height: auto !important; - - & > input:not([size="5"]) { - position: relative; - left: 0 !important; - flex: 1; - } - - } - } - - & > .mce-container-body { - box-sizing: border-box; - width: 100% !important; - } - - .mce-form-split { - .mce-formitem { - min-width: auto; - - & > .mce-container-body { - width: 100% !important; - } - } - } - - label { - position: unset; - line-height: 2.5 !important; - height: auto !important; - } - } - - .mce-colorpicker { - & + .mce-form { - width: 150px !important; - padding: 0; - - .mce-formitem { - min-width: unset; - - & + :not(.mce-formitem) { - height: 50px !important; - } - } - } - } - - .mce-textbox { - padding: .375rem .75rem !important; - line-height: 1.5; - color: @color-font; - - &:not(textarea) { - height: auto !important; - } - - &:focus { - color: @color-font; - border-color: @color-input-border-focus; - box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow; - } - - &[size="5"] { - width: auto !important; - } - - &.mce-multiline { - line-height: 1.25; - width: 100% !important; - position: unset; - box-sizing: border-box; - display: block; - } - } - - .mce-listbox { - button { - line-height: 1.5; - padding: .375rem .75rem; - } - - &:focus { - border-color: @color-input-border-focus !important; - box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow !important; - } - } - - .mce-checkbox { - line-height: 2.5; - - i.mce-i-checkbox { - border: 0; - width: auto; - color: @color-checkbox; - text-indent: 0; + .tox-button { + .btn-primary; + font-weight: normal; + padding: .5rem .75rem; &:before { &:extend(.font-icon-class); - margin: 0; - content: @fa-var-toggle-off; - } - } - - &.mce-checked i.mce-i-checkbox:before { - content: @fa-var-toggle-on; - } - - &:focus { - i.mce-i-checkbox { - border: 0; - } - } - } - - .mce-combobox { - display: flex; - - input { - border-radius: .3rem 0 0 .3rem; - flex: 1; - - &:focus { - z-index: 1; - } - } - - &.mce-combobox-fake { - input { - border-radius: .3rem; - } - } - - button { - padding: .4rem .6rem; - } - - .mce-btn { - border-radius: 0 .3rem .3rem 0; - background: @color-input-addon-background; - - &:focus { - background-color: @color-input-border-focus-shadow; - border-color: #c5c5c5; - } - } - } - - .mce-tabs { - padding-top: 1rem; - margin: 0 1rem; - border-color: @color-layout-border; - - .mce-tab { - border-radius: .25rem .25rem 0 0; - padding: .5rem 1rem; - height: auto !important; - border: 1px solid transparent; - color: @color-link; - background: transparent; - margin-bottom: -1px; - - &.mce-active { - border: 1px solid @color-layout-border; - border-bottom-color: #fff; - color: @color-font !important; + width: 1em; + content: @fa-var-check; } - &:not(.mce-active):hover { - border: 1px solid @color-list-border; - border-bottom-color: transparent; - border-bottom: 0; + // this is redundant, but required because of tinymce styles interference + &:focus:not(:disabled) { + background: @color-btn-primary-background; + border-color: @color-btn-primary-background; } - &:focus { - outline: unset !important; - } - } - } - - .mce-label { - text-shadow: none; - color: @color-font; - } -} - - -// Menus and popovers, e.g. color selector, emoticons selector, font selector -.mce-menu, -.mce-floatpanel.mce-popover { - box-shadow: 3px 3px 5px @color-popover-shadow !important; - border-color: @color-layout-border !important; - border-radius: .3rem; -} - -.mce-menu { - .mce-menu-item.mce-active { - color: @color-menu-hover; - background-color: @color-menu-hover-background; - } - - .popover-header { - display: none !important; - } -} - -div.mce-menubtn.mce-opened { - z-index: 65530 !important; // BUG: https://github.com/tinymce/tinymce/issues/4542 -} - -#mce-modal-block.mce-in { - background-color: @color-dialog-overlay-background; - opacity: 1 !important; -} - -@media screen and (max-width: @screen-width-xs) { - .mce-window { - width: 100% !important; - height: 100% !important; - left: 0 !important; - top: 0 !important; - border-width: 0 !important; - - & > .mce-reset { - display: flex; - flex-direction: column; - height: 100%; - } - - .mce-window-body { - flex: 1; - overflow-y: auto !important; - } - - & > .mce-reset > div, - .mce-container-body { - width: 100% !important; - } - - .mce-window-head { - background-color: @color-layout-mobile-header-background; - - .mce-title { - font-size: 1rem; - text-align: center; - padding: 0 1rem; - } - - .mce-close { - display: none; - } - } - - .mce-foot { - background-color: @color-layout-mobile-footer-background; - - .mce-container-body { - justify-content: space-evenly; - - .mce-btn { - position: initial; - height: 100% !important; - margin: 0; - background: transparent; - border-width: 0; - - &:focus { - box-shadow: none; - } - - &:hover { - background: transparent; - } - - &:last-child { - margin: 0; - } - - button { - color: @color-font; - padding: .45rem; - font-size: .9rem; - - &:before { - display: block; - float: none; - width: 100%; - margin: 0; - line-height: 1.75; - height: 1.75rem; - } - } - } - - .mce-abs-end { - display: none; - } - } - - .mce-search-foot { - div:nth-of-type(7) button:after { - display: none; - } - } - } - - .mce-form, - .mce-form + .mce-container, // for Embed tab in Media dialog - .mce-formitem, - .mce-combobox, - .mce-panel:not(.mce-popover) { - width: 100% !important; - } - - .mce-formitem { - min-width: unset; - } - - .mce-form { - & > .mce-container-body { - display: flex; - flex-direction: column; - left: 0; - right: 0; - box-sizing: border-box; - } - - .mce-container-body { - height: auto !important; - flex-direction: column; - - & > label { - position: unset !important; - width: 100% !important; - max-width: 100%; - } - - & > label + * { - position: unset !important; - width: auto !important; - } - - & > .mce-checkbox { - position: absolute; - left: 0 !important; - top: 3rem !important; - } - } - } - - // FIXME: How to fix the input width in less hacky way? - .mce-combobox input { - max-width: ~"calc(100% - 4rem)"; - } - .mce-combobox-fake input { - max-width: ~"calc(100% - 1.7rem)"; - } - } - - .mce-menu { - width: @layout-mobile-menu-width !important; - right: 0; - top: 0 !important; - left: auto !important; - height: 100% !important; - max-height: unset !important; - padding: 0 !important; - margin: 0 !important; - border-radius: 0; - border: 0 !important; - - .popover-header { - display: block !important; - - a { - font-size: 1.2rem; - line-height: @layout-touch-header-height; + &.tox-button--secondary { + .btn-secondary; + color: @color-btn-secondary; &:before { content: @fa-var-times; } - } - } - .mce-container-body { - width: 100% !important; - } - - .mce-menu-item { - height: @layout-touch-menu-record-height; - line-height: @layout-touch-menu-record-height; - padding: 0 .5rem; - border-left: 0; - border-bottom: 1px solid @color-list-border; - margin: 0; - - .mce-ico { - font-size: 150%; - padding: 0 .7rem 0 .25rem; - margin-top: -.2rem; - } - - .mce-text { - font-size: 1.2rem; - .font-family; - line-height: @layout-touch-menu-record-height; - color: @color-font; - } - - .mce-caret { - display: none; - } - - &.mce-menu-item-preview { - &.mce-active { - border-left: none; - position: relative; - - &:after { - .font-icon-class; // :extend() does not work here - content: @fa-var-check; - position: absolute; - right: .5rem; - top: 0; - color: @color-font; - } + // this is redundant, but required because of tinymce styles interference + &:focus:not(:disabled) { + background: @color-btn-secondary-background; + border-color: @color-btn-secondary-background; } } + } + } - &.mce-menu-item-expand { - position: relative; + .tox-search-dialog { + .tox-form__group:not(:first-child) { + flex: initial !important; + } - &:after { - .font-icon-class; // :extend() does not work here - content: @fa-var-angle-right; - position: absolute; - right: .5rem; - top: 0; + .tox-dialog__footer-start { + button { + padding: .25rem; + } + } + + .tox-dialog__footer-end { + button { + &:before { + content: @fa-var-pencil-alt !important; + } + + &:nth-of-type(1):before { + content: @fa-var-search !important; + } + } + } + } + + .tox-dialog__title { + line-height: @layout-header-height; + font-size: 1.25rem; + font-weight: bold; + padding: 0 0 0 1rem; + width: 100%; + color: @color-dialog-header; + } + + // Make toolbar buttons smaller + .tox-tbtn { + height: 28px; + + &:not(.tox-tbtn--select,.tox-split-button__chevron) { + width: 32px; + } + } + + .tox-label { + color: @color-font; + padding-bottom: .25rem; + } + + // Adding .form-control does not work with TinyMCE skins, + // so we have to overwrite some props here + .tox-color-input > input, + .tox-selectfield select, + .tox-textarea, + .tox-textfield { + .font-family !important; + font-size: @page-font-size; + line-height: 1.5; + color: @color-font; + border-radius: .25rem; + min-height: 0; + padding: .375rem .75rem; + + &:focus { + border-color: @color-input-border-focus; + box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow; + } + } + + .tox-color-input span { + top: 5px; + } + + .custom-switch { + position: relative; + font-size: 1rem; + margin-top: .15rem; + + .tox-checkbox__icons { + display: none; + } + + .tox-checkbox__label { + margin: 0; + } + } + + .image-selector { + font-size: 1rem; + button { + .btn-secondary; + padding: .5rem .75rem; + line-height: 1.5; + } + } + + // small fix for image dialog + .tox-form__controls-h-stack div:not(:last-child) { + flex: 1; + } + + .tox-collection__item-label { + white-space: nowrap; // fix TinyMCE bug + } +} + +@media screen and (max-width: @screen-width-xs) { + div.tox { + .tox-dialog { + margin: 0 !important; + width: 100% !important; + height: 100%; + left: 0 !important; + top: 0 !important; + border-width: 0 !important; + } + + .tox-dialog__header { + background-color: @color-layout-mobile-header-background; + + .tox-button { + display: none; + } + } + + .tox-dialog__title { + font-size: 1rem; + text-align: center; + padding: 0 1rem; + } + + .tox-dialog__footer { + background-color: @color-layout-mobile-footer-background; + + .tox-button { + color: @color-font !important; + background: transparent !important; + padding: .45rem; + margin: 0 !important; + border: 0; + font-size: 90%; + + &:before { + display: block; + float: none; + width: 100%; + margin: 0; + line-height: 1.75; + height: 1.75rem; + } + + &:active, + &:focus, + &:hover { + background: transparent; + border: 0; + box-shadow: none; color: @color-font; } } - } - } - .mce-menu-item-sep, - .mce-menu-shortcut { - display: none !important; - } + & > div { + justify-content: space-evenly; + display: flex; + width: 100%; - .mce-charmap-dialog { - position: unset !important; - - + .mce-container { - display: none; - } - } - - .mce-charmap { - display: block; - - tbody { - display: block; - } - - tr { - display: flex; - flex-wrap: wrap; - } - - td { - flex: 1; - height: auto !important; - min-width: 17%; - padding: 0 !important; - border: 0 !important; - border-bottom: 1px solid @color-list-border !important; - - div { - font-size: 3rem; - line-height: 2; + &:empty { + display: none; + } } } } } -html.touch .mce-grid td { - padding: .5rem; -} - - /*** Media file selector for TinyMCE ***/ .image-selector { - margin: 1rem 1rem 1rem 1rem !important; padding: 1rem .5rem 10rem .5rem !important; &.droptarget { @@ -774,27 +351,17 @@ html.touch .mce-grid td { } } - button { - .btn-secondary; - padding: .5rem .75rem; - line-height: 1.5; - position: relative; - - &:before { - line-height: 1; - } - } - form { position: absolute; top: 0; } .attachmentslist { - margin-left: 0; + margin: 0; overflow-x: hidden; overflow-y: auto; height: 19.1em; + padding: 0 !important; li { padding: .25rem; @@ -855,51 +422,41 @@ html.touch .mce-grid td { .mce-i-html { display: block; - padding: 1px 5px; - margin: 2px; - width: 2rem; - height: 24px; - border: 1px solid transparent; - color: #595959; + margin: 2px 2px 2px 4px; + width: 34px; + height: 28px; + border-radius: .25rem; + color: #222f3e; // from TinyMCE &:focus, &:hover { text-decoration: none; border-color: #e2e4e7; - background-color: #fff; + background-color: #dee0e2; // from TinyMCE + } + + &:before { + &:extend(.font-icon-class); + content: @fa-var-image; + margin: 0; + width: 34px; + line-height: 28px; } } } // hide toolbar in html mode and in mailvelope mode &.mailvelope .editor-toolbar, - .mce-tinymce + textarea + .editor-toolbar { + .tox-tinymce + .editor-toolbar { display: none; } - .mce-i-html:before, - .mce-i-plaintext:before { - &:extend(.font-icon-class); - margin: 0; - width: 1em; - font-size: 1.2rem; - } - - .mce-i-html:before { - content: @fa-var-image; - line-height: 1.2em; - } - - .mce-i-plaintext:before { - content: @fa-var-window-close; //@fa-var-align-justify; - } - & > .googie_edit_layer, & > textarea { font-family: monospace; font-size: 13px; width: 100% !important; - padding-top: 2.5rem; + padding-top: 48px; resize: none; } @@ -909,8 +466,9 @@ html.touch .mce-grid td { min-height: 30em; } - #composebody_ifr { - min-height: 30em; + & > .tox-tinymce.focused { + border-color: @color-input-border-focus; + box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow !important; } } diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js index c5acc5343..a8eba38e8 100644 --- a/skins/elastic/ui.js +++ b/skins/elastic/ui.js @@ -486,20 +486,6 @@ function rcube_elastic_ui() .addEventListener('clonerow', pretty_checkbox_fix) .addEventListener('init', init); - // Add styling for TinyMCE editor popups - // We need to use MutationObserver, as TinyMCE does not provide any events for this - if (window.MutationObserver && window.tinymce) { - var callback = function(list) { - $.each(list, function() { - $.each(this.addedNodes, function() { - tinymce_style(this); - }); - }); - }; - - (new MutationObserver(callback)).observe(document.body, {childList: true}); - } - // Create floating action button(s) if ((layout.list.length || layout.content.length) && is_mobile()) { var fabuttons = []; @@ -1150,86 +1136,6 @@ function rcube_elastic_ui() $('select:not([multiple])', context).each(function() { pretty_select(this); }); }; - /** - * Detects if the element is TinyMCE dialog/menu - * and adds Elastic styling to it - */ - function tinymce_style(elem) - { - // TinyMCE dialog widnows - if ($(elem).is('.mce-window')) { - var body = $(elem).find('.mce-window-body'), - foot = $(elem).find('.mce-foot > .mce-container-body'); - - // Apply basic forms style - if (body.length) { - bootstrap_style(body[0]); - } - - body.find('button').filter(function() { return $(this).parent('.mce-btn').length > 0; }).removeClass('btn btn-secondary'); - - // Fix icons in Find and Replace dialog footer - if (foot.children('.mce-widget').length === 5) { - foot.addClass('mce-search-foot'); - } - - // Apply some form structure fixes and helper classes - $(elem).find('.mce-charmap').parent().parent().addClass('mce-charmap-dialog'); - $(elem).find('.mce-combobox').each(function() { - if (!$(this).children('.mce-btn').length) { - $(this).addClass('mce-combobox-fake'); - } - }); - $(elem).find('.mce-form > .mce-container-body').each(function() { - if ($(this).children('.mce-formitem').length > 4) { - $(this).addClass('mce-form-split'); - } - }); - $(elem).find('.mce-form').next(':not(.mce-formitem)').addClass('mce-form'); - - // Fix dialog height (e.g. Table properties dialog) - if (!is_mobile()) { - var offset, max_height = 0, height = body.height(); - $(elem).find('.mce-form').each(function() { - max_height = Math.max(max_height, $(this).height()); - }); - - if (height < max_height) { - max_height += (body.find('.mce-tabs').height() || 0) + 25; - body.height(max_height); - $(elem).height($(elem).height() + (max_height - height)); - $(elem).css('top', ($(window).height() - $(elem).height())/2 + 'px'); - } - } - } - // TinyMCE menus on mobile - else if ($(elem).is('.mce-menu')) { - $(elem).prepend( - $('

').append( - $('') - .text(rcmail.gettext('close')) - .on('click', function() { $(document.body).click(); }))); - - if (window.MutationObserver) { - var callback = function() { - if (mode != 'phone') { - return; - } - if (!$('.mce-menu:visible').length) { - $('div.mce-overlay').click(); - } - else if (!$('div.mce-overlay').length) { - $('
').attr('class', 'popover-overlay mce-overlay') - .appendTo('body') - .click(function() { $(this).remove(); }); - } - }; - - (new MutationObserver(callback)).observe(elem, {attributes: true}); - } - } - }; - /** * Initializes popup menus */ @@ -1434,15 +1340,8 @@ function rcube_elastic_ui() o.config.plugins += ' autoresize'; if (is_touch()) { - // Make the toolbar icons bigger - o.config.toolbar_items_size = null; - // Use minimalistic toolbar - o.config.toolbar = 'undo redo | insert | styleselect'; - - if (o.config.plugins.match(/emoticons/)) { - o.config.toolbar += ' emoticons'; - } + o.config.toolbar = 'undo redo | link image styleselect'; } if (rcmail.task == 'mail' && rcmail.env.action == 'compose') { @@ -1462,8 +1361,8 @@ function rcube_elastic_ui() // Keep the editor toolbar on top of the screen on scroll form.on('scroll', function() { - var container = $('.mce-container-body', form), - toolbar = $('.mce-top-part', container), + var container = $('.tox-editor-container', form), + toolbar = container.find('.tox-toolbar-overlord'), editor_offset = container.offset(), header_top = form.offset().top; @@ -1476,16 +1375,17 @@ function rcube_elastic_ui() }); $(window).resize(function() { form.trigger('scroll'); }); + } if (is_editor) { o.config.toolbar = 'plaintext | ' + o.config.toolbar; // Use setup_callback, we can't use editor-load event o.config.setup_callback = function(ed) { - ed.addButton('plaintext', { + ed.ui.registry.addButton('plaintext', { tooltip: rcmail.gettext('plaintoggle'), - icon: 'plaintext', - onclick: function(e) { + icon: 'close', + onAction: function(e) { if (rcmail.command('toggle-editor', {id: ed.id, html: false}, '', e.originalEvent)) { $('#' + ed.id).parent().removeClass('ishtml'); } @@ -1494,6 +1394,43 @@ function rcube_elastic_ui() }; } + // Add styling for TinyMCE dialogs + onload.push(function(ed) { + ed.on('OpenWindow', function(e) { + var dialog = $('.tox-dialog:last')[0], + callback = function(e) { + var body = $(dialog).find('.tox-dialog__body'), + foot = $(dialog).find('.tox-dialog__footer'), + buttons = foot.find('button'); + + if (!e) { + // Fix icons in Find and Replace dialog footer + if (buttons.length === 4) { + body.closest('.tox-dialog').addClass('tox-search-dialog'); + } + // Switch Save and Cancel buttons order + else if (buttons.length == 2) { + buttons.first().insertAfter(buttons[1]); + } + + // TODO: Styling form elements does not work well because of + // https://github.com/tinymce/tinymce/issues/4867 + // also https://github.com/tinymce/tinymce/issues/4869 + } + + body.find('select').each(function() { pretty_select(this); }); + body.find('.tox-checkbox > input').each(function() { pretty_checkbox(this); }); + }; + + // TODO: Maybe some day we'll not have to use MutationObserver + // https://github.com/tinymce/tinymce/issues/4869 + if (window.MutationObserver) { + (new MutationObserver(callback)).observe($('.tox-dialog__body-content', dialog)[0], {childList: true}); + } + callback(); + }); + }); + rcmail.addEventListener('editor-load', function(e) { $.each(onload, function() { this(e.ref.editor); }); }); diff --git a/skins/larry/mail.css b/skins/larry/mail.css index b8fa60c40..695f40fda 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -1305,7 +1305,6 @@ body.status-flagged .flag-icon { overflow: auto; } -#image-selector.droptarget, #compose-attachments.droptarget { background-image: url(images/filedrop.png); background-position: center bottom; @@ -1340,19 +1339,11 @@ body.status-flagged .flag-icon { padding-bottom: 8px; } -#composebodycontainer .mce-tinymce { +#composebodycontainer .tox-tinymce { border: 0 !important; margin-top: 1px; } -#composebodycontainer .mce-panel { - border-color: #ddd !important; -} - -#composebody_toolbargroup { - border-bottom: 1px solid #ddd; -} - #uploadform a.iconlink { margin-left: 1em; text-indent: -5000px; diff --git a/skins/larry/styles.css b/skins/larry/styles.css index c01f31db4..240ca2f7d 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -1684,11 +1684,6 @@ table.propform td.title { white-space: nowrap; } -table.propform .mceLayout td { - padding: 0; - border-bottom: 0; -} - ul.proplist { list-style: none; margin: 0; @@ -3061,47 +3056,32 @@ ul.toolbarmenu li span.copy { padding: 0.5em 1em; } -#image-selector-form.droptarget { +/** Common TinyMCE fixes **/ + +#image-selector.droptarget { background: url(images/filedrop.png) center bottom no-repeat; } -/** Common TinyMCE fixes **/ -.mce-btn-small:not(.mce-active) { - background: transparent !important; +div.tox .tox-toolbar, +div.tox .tox-toolbar__overflow, +div.tox .tox-toolbar__primary { + background-color: #f0f0f0; } -.mce-btn-small:not(.mce-active):hover { - background: white !important; +div.tox .tox-toolbar__primary { + border: 0; } -.mce-btn-small .mce-ico { - display: inline; /* for old Firefox */ +div.tox div.tox-dialog-wrap__backdrop { + background: #aaa; + opacity: .3; } -.mce-btn-small i { - line-height: 16px !important; - vertical-align: text-top !important; +div.tox div.tox-dialog { + box-shadow: 1px 1px 18px #666; + border-width: 0; } -_:not(), _:-moz-handler-blocked, .mozilla .mce-btn-small i { - line-height: 20px !important; -} - -.mce-top-part::before { - box-shadow: none !important; -} - -.mce-textbox { - border-radius: 0; - box-shadow: none; -} - -button.mce-close, -.mce-btn button, -.mce-textbox:focus { - box-shadow: none; - outline: none; -} .mce-menu { z-index: 65537 !important; diff --git a/skins/larry/ui.js b/skins/larry/ui.js index fbf7c8d51..9de92b5af 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -600,8 +600,8 @@ function rcube_mail_ui() h = body.parent().height() - 8; body.width(w).height(h); - $('#composebodycontainer > div').width(w+8); - $('#composebody_ifr').height(h + 4 - $('div.mce-toolbar').height()); + $('#composebodycontainer > div').width(w+7); + $('#composebody_ifr').height(h + 4 - $('div.tox-toolbar').height()); $('#googie_edit_layer').width(w).height(h); // $('#composebodycontainer')[(btns ? 'addClass' : 'removeClass')]('buttons'); // $('#composeformbuttons')[(btns ? 'show' : 'hide')](); diff --git a/tests/Browser/Components/HtmlEditor.php b/tests/Browser/Components/HtmlEditor.php index 40d71a393..0a3cc8cb2 100644 --- a/tests/Browser/Components/HtmlEditor.php +++ b/tests/Browser/Components/HtmlEditor.php @@ -53,8 +53,8 @@ class HtmlEditor extends Component return [ '@plain-toolbar' => '.editor-toolbar', '@plain-body' => 'textarea', - '@html-editor' => '.mce-tinymce', - '@html-toolbar' => '.mce-tinymce .mce-toolbar', + '@html-editor' => '.tox-tinymce', + '@html-toolbar' => '.tox-tinymce .tox-editor-header', '@html-body' => 'iframe', ]; } @@ -84,10 +84,10 @@ class HtmlEditor extends Component if ($accept_warning) { $browser->waitForDialog()->acceptDialog(); } - $browser->waitFor('@html-body'); + $browser->waitFor('@html-body')->waitFor('@html-toolbar'); } else { - $browser->click('@html-toolbar .mce-i-plaintext'); + $browser->click('.tox-toolbar__group:first-child button'); if ($accept_warning) { $browser->waitForDialog()->acceptDialog(); } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 41cb5c1ce..749b255dc 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -64,7 +64,6 @@ ./../plugins/database_attachments/tests/DatabaseAttachments.php ./../plugins/debug_logger/tests/DebugLogger.php ./../plugins/emoticons/tests/Emoticons.php - ./../plugins/emoticons/tests/EmoticonsEngine.php ./../plugins/enigma/tests/Enigma.php ./../plugins/example_addressbook/tests/ExampleAddressbook.php ./../plugins/filesystem_attachments/tests/FilesystemAttachments.php