├── .gitignore ├── CHANGELOG.md ├── MANUAL.md ├── README.md ├── composer.json ├── contextmenu.js ├── contextmenu.php ├── localization ├── ar.inc ├── bg_BG.inc ├── ca_ES.inc ├── cs_CZ.inc ├── da_DK.inc ├── de_CH.inc ├── de_DE.inc ├── en_GB.inc ├── en_US.inc ├── es_AR.inc ├── es_ES.inc ├── fr_FR.inc ├── gl_ES.inc ├── hu_HU.inc ├── it_IT.inc ├── ja_JP.inc ├── nl_NL.inc ├── pl_PL.inc ├── pt_BR.inc ├── pt_PT.inc ├── ro_RO.inc ├── ru_RU.inc ├── sk_SK.inc ├── sv_SE.inc ├── tr_TR.inc ├── uk_UA.inc ├── zh_CN.inc └── zh_TW.inc └── skins ├── classic ├── contextmenu.css ├── functions.js ├── images │ ├── contactactions.png │ ├── contexticons.png │ └── messageactions.png └── includes │ ├── mail.html │ └── settings.html ├── elastic ├── contextmenu.less ├── contextmenu.min.css ├── functions.js └── includes │ ├── mail.html │ └── settings.html └── larry ├── contextmenu.css ├── functions.js ├── images └── contexticons.png └── includes ├── mail.html └── settings.html /.gitignore: -------------------------------------------------------------------------------- 1 | skins/elastic/_custom.less 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Roundcube Webmail ContextMenu 2 | 3 | ## Version 3.3.1 (2022-06-18, rc-1.5) 4 | 5 | - Add placeholder for `undeleted` icon 6 | 7 | ## Version 3.3 (2021-10-19, rc-1.5) 8 | 9 | - Support Dark Mode in Elastic 10 | - Support for customizing Elastic skin 11 | 12 | ## Version 3.2.1 (2020-11-20, rc-1.4.4) 13 | 14 | - Various code improvements 15 | - Add `user-select: none;` to menus 16 | 17 | ## Version 3.2 (2020-04-27, rc-1.4.4) 18 | 19 | - Update command enabling after (req RC cb8c078) 20 | 21 | ## Version 3.1 (2020-01-11, rc-1.4) 22 | 23 | - Fix invalid function call to init_addressbook in Larry skin 24 | - Replace `+ events` with new `global_events` object 25 | 26 | ## Version 3.0 (2019-10-27, rc-1.4) 27 | 28 | - Rework Contextmenu settings object 29 | - Rework core Contextmenu JS functions 30 | - `rcm_listmenu_init()` -> `rcmail.contextmenu.init_list()` 31 | - `rcm_foldermenu_init()`-> `rcmail.contextmenu.init_folder()` 32 | - `rcm_abookmenu_init()` -> `rcmail.contextmenu.init_addressbook()` 33 | - `rcm_callbackmenu_init()` -> `return rcmail.contextmenu.init()` 34 | - `rcm_show_menu()` -> `rcmail.contextmenu.show_one()` 35 | - `rcm_hide_menu()` -> `rcmail.contextmenu.hide_all()` 36 | - `rcm_check_button_state()` -> `return rcmail.contextmenu.ui_button_check()` 37 | - Allow skins to set Contextmenu classes 38 | - Remove IE8 hacks 39 | - Improve menu element parsing, allow for popup menu functions in skin 40 | - Add new event `submenu_toggle` to handle display of hidden menus 41 | - Disable right click on context menu 42 | - Depreciate `right-arrow` class, use `sub-button` instead 43 | - Depreciate `contextRow` class, use `context-source` instead 44 | - Depreciate `rcmmainmenu` class, use `rcm-mainmenu` instead 45 | - Depreciate `rcmsubmenu` and `submenu` classes, use `rcm-submenu` instead 46 | - Depreciate `rcm_active` class, use `rcm-active` instead 47 | - Depreciate `rcm_ignore` class, use `rcm-ignore` instead 48 | - Add new event `addmenuitem` 49 | - Add ability to add to core event handlers as well as replace them 50 | - Improve collapseall/expandall JS functions to better mimic true click 51 | - Use `getselection` list event added (req RC e9eb87d, afaaa77, 2f7aaca) 52 | - Change `rcube_context_menu.list_object` from Object to String, backwards compatibility added for Contact list but no others 53 | - Replace `overload_commands` option with `always_enable_commands` 54 | - `overload_commands` option removed (req RC afaaa77) 55 | - Add Elastic skin support 56 | - Add `menu-change` JS event listener 57 | - Support new Contacts toolbar with Copy/Move actions (req RC eb0228b) 58 | - Add menus to Settings screens 59 | - Add `contextmenu_mouseover_timeout` config option to override default mouseover timeout for submenus 60 | - Extend activate hook, now returns JSON rather than Boolean 61 | - Make menu button inactive if all subactions are inactive (req RC daf4607) 62 | - Use new treelist collapse_all/expand_all functions (req RC 3a40f6c) 63 | 64 | ## Version 2.3 (2017-06-14, rc-1.3) 65 | 66 | - Remove special handling for group-create/rename (requires roundcube rev ec98aa5) 67 | - "Flattened" the larry theme: fresher look by removing shadows and gradients 68 | - Fix handling of folder names containing special chars 69 | 70 | ## Version 2.2 (2017-01-02, rc-1.3) 71 | 72 | - Use new 'Mark all as read' function from core (requires 1.3) 73 | - Enable compact+purge actions on all folders 74 | 75 | ## Version 2.1.2 (2015-04-18, rc-1.1) 76 | 77 | - Fix #67 address book copy/move action not updated 78 | 79 | ## Version 2.1.1 (2015-03-18, rc-1.1) 80 | 81 | - Update submenu detection after 619891c 82 | 83 | ## Version 2.1 (2015-01-06, rc-1.1) 84 | 85 | - Add specific menu for adding contact to group 86 | - Update after dev-accessibility merged to RC master 87 | - Support multi-folder search results (66536974fe) 88 | - Fix possible CSRF in `readfolder` action 89 | - Add option to move/copy contacts in address book 90 | 91 | ## Version 2.0 (2014-04-30, rc-1.0) 92 | 93 | - Remove IE6 support 94 | - Remove `rcm_contextmenu_register_command`, use commands from toolbars 95 | - Remove specially created menus, generate context menu dynamically from UI 96 | - Much better menu generation, improve loading times 97 | 98 | ## Version 1.13 (2014-02-01, rc-1.0) 99 | 100 | - Add menu to contact list on compose screen (requires b3c034c) 101 | 102 | ## Version 1.12 (2013-12-01, rc-1.0) 103 | 104 | - Update depreciated `moveto` command to `move` 105 | - Update config file var names to match core 106 | 107 | ## Version 1.11 (2013-05-19, rc-1.0) 108 | 109 | _code branching/tagging no longer sync'd to roundcube version_ 110 | - Add export option to address book menu 111 | - Add `contextmenu_show` JS event 112 | 113 | ## Version 1.10 (2013-03-03, rc-0.9) 114 | 115 | - Add remove selected contacts from group option 116 | - Rename default skin to classic (c40419bdfe) 117 | - `rcube_ui` > `rcube_utils` (r6091) 118 | - Update for Roundcube framework 119 | 120 | ## Version 1.9 (2012-04-14, rc-0.8) 121 | 122 | - Update after r5781 123 | - Add initial support for Larry 124 | 125 | ## Version 1.8 (2011-05-14, rc-0.8) 126 | 127 | - Fix `readfolder` method after r5557 128 | - Use new labels for group delete/rename (r4864) 129 | - Add `forward-attachment` function (r4761) 130 | 131 | ## Version 1.7 (2011-10-01, rc-0.5) 132 | 133 | - Add reply-list function (r4032) 134 | 135 | ## Version 1.6 (2010-08-01, rc-0.4) 136 | 137 | - Fix error in deleting after moving using context menu 138 | - Update hooks (r3840) 139 | 140 | ## Version 1.5 (2010-05-29, rc-0.4) 141 | 142 | - Hide menu after click in iframes 143 | - Update contact copying after r3694 144 | - Update hooks after r3685 145 | 146 | ## Version 1.4 (2010-03-30, rc-0.4) 147 | 148 | - Update after r3614 149 | - Update `jquery.ContextMenu` to 1.01 150 | - Use new `imap->search_once` function (r3446) 151 | - Add initial support for contact groups in address book 152 | - Fix mark all as read function 153 | - Update to r3393 154 | - Fix folder count with nested folders 155 | 156 | ## Version 1.3 (2010-02-07, rc-0.4) 157 | 158 | - Update after r3258 159 | - CSS update after r3141 160 | - Use `local_skin_path()` (rev 3076) 161 | - Added address book context menu (requires r3023) 162 | 163 | ## Version 1.2 (2009-09-18, rc-0.3) 164 | 165 | - Fix folder menu for UTF8 folders 166 | - Use `rcmail_send_unread_count` (r2960) 167 | - Add 'move to' to message list menu 168 | 169 | ## Version 1.1 (2009-09-11, rc-0.3) 170 | 171 | - Added folder list context menu 172 | - Moved menu generation from JS to PHP 173 | - Added support for sub menus 174 | - Added new `edit` action 175 | - Update `insertrow` event listener following changes in core 176 | - Add fallback to default skin 177 | - Better init restrictions 178 | - Correct style after r2541 179 | - Only init when on mailbox screen (while `_uid` is empty and user is logged in) 180 | - Added hook for other plugins to add items to the menu (see README) 181 | - Added support for new actions: show source, save .eml, open in new window 182 | - Added support for plugin template system 183 | - Added highlighting of selected row 184 | - Fixed menu in IE 185 | - Fixed delete in Trash folder 186 | - Added ability to mark or delete multiple messages 187 | - Fixed delete function 188 | - Fixed working with `Select` functions 189 | - Added event hooks when message list is updated 190 | - Created plugin -------------------------------------------------------------------------------- /MANUAL.md: -------------------------------------------------------------------------------- 1 | # ContextMenu manual 2 | 3 | This file provides information for plugin and skin developers. The ContextMenu plugin can be extended by other plugins; new menus can be created and existing menus manipulated. For basic installation information please see the [README](./README.md). 4 | 5 | - [Global options](#global-options) 6 | - [Creating a new ContextMenu](#creating-a-new-contextmenu) 7 | - [Working with an existing ContextMenu](#working-with-an-existing-contextmenu) 8 | - [Events](#events) 9 | - [ContextMenu and skins](#contextmenu-and-skins) 10 | 11 | ## Global options 12 | 13 | The following global options are available: 14 | * no_right_click_on_menu - (boolean) treat right click as left click when on the contextmenu 15 | * skip_commands - (array) Roundcube commands that should be ignored by the contextmenu 16 | * always_enable_commands - (array) Roundcube commands that should rename enabled, once clicked, until the menu is closed 17 | * command_pattern - (regex) pattern matching the Roundcube command call 18 | * popup_attrib - (string) attribute that contains the Roundcube popup call 19 | * popup_pattern - (regex) pattern matching the Roundcube popup call 20 | * popup_func - (string) function used by ContextMenu when opening a popup 21 | * classes - (object) CSS classes for various aspects of the menu 22 | * container - class(es) applied to the menu container 23 | * mainmenu - class(es) applied to the menu container of top level menus 24 | * submenu - class(es) applied to the menu container of sub menus 25 | * button_remove - thse class(es) will be removed from the context menu button (for example classes that are used in the UI which are not needed in the context menu) 26 | * button_active - class(es) for buttons in an active state 27 | * button_disabled - class(es) for buttons in an inactive state 28 | * menu_defaults - (object) default options for ContextMenu objects 29 | * global_events - (object) global events applied to every ContextMenu object 30 | * menu_events - (object) default events for ContextMenu objects 31 | 32 | ### Skip Commands 33 | Entries in the skip_commands array should take the form `-`, for example `mail-checkmail`. The following commands are disabled by default: 34 | * mail-checkmail 35 | * mail-compose 36 | * addressbook-add 37 | * addressbook-import 38 | * addressbook-advanced-search 39 | * addressbook-search-create 40 | 41 | ### Classes 42 | When specifying multiple classes in the global options **or in specific menu options** each class should be separated by a space, for example: 43 | ```js 44 | ... 45 | classes: { 46 | container: 'contextmenu popupmenu', 47 | ... 48 | ``` 49 | 50 | ## Creating a new ContextMenu 51 | 52 | By default ContextMenu is added to all tasks in Roundcube except `login`. 53 | 54 | The JavaScript function `rcmail.contextmenu.init()` creates the ContextMenu object. If the ContextMenu functions are enabled in the UI then the `rcmail.env.contextmenu` variable JavaScript will be set to true. Setting this variable to false will disable all context menus. 55 | 56 | ```js 57 | var menu = rcmail.contextmenu.init(props, events); 58 | ``` 59 | 60 | The functions takes 2 parameters: 61 | 62 | `props` (required) JSON object: 63 | * menu_name - (string) required - A friendly name for the ContextMenu, it is also used as the ID for the ContextMenu element. 64 | * menu_source - (string or array) required - See [Menu sources](#menu-sources) for details. 65 | * list_object - (object) optional - If ContextMenu is used on a Roundcube list object then that list object should be set here (e.g. `rcmail.message_list`), set to `null` if using ContextMenu on another element. It is set to `rcmail.message_list` by default. 66 | * source_class - (string) optional - The CSS class applied to the triggering element, `contextRow` by default. 67 | * mouseover_timeout - (int) optional - The delay for displaying submenus on mouseover, set to -1 to disable mouseover. `400` by default. 68 | * classes - (object) optional - CSS classes for various aspects of the menu: 69 | * source - class(es) applied to object on which the menu was triggered 70 | * div - class(es) applied to the menu container 71 | * ul - class(es) applied to the UL object in the menu 72 | * a - class(es) applied to the A objects in the menu 73 | * span - class(es) applied to spans inside in the A objects 74 | * sub_button_a - class(es) applied to A objects which have a submenu 75 | * sub_button_span - class(es) applied to the submenu indicator element, set to null to remove this span 76 | * modal - (boolean) optional - Display the menu in a modal fashion 77 | * skinable - (boolean) optional - Value passed to the skinable param of the menu-open event. `false` by default. 78 | 79 | `events` (optional) JSON object. ContextMenu triggers a number of events during execution, for example `command` is tiggered when the user clicks on an item in the menu. Full details of all the events can be found in the [Events](#events) section of this file. This parameters allows a plugin author to attach their own functions to the ContextMenu events, overriding the defaults. 80 | 81 | Displaying submenus on mouseover: A config value named `contextmenu_mouseover_timeout` can be set in your main Roundcube config file to define the default `mouseover_timeout` value as something other than `400`. 82 | 83 | Creating a simple ContextMenu looks like this: 84 | ```js 85 | var menu = rcmail.contextmenu.init( 86 | {'menu_name': 'messagelist', 'menu_source': '#messagetoolbar'}, 87 | {'beforeactivate': function(p) { 88 | rcmail.env.contextmenu_selection = p.ref.list_selection(true); 89 | }, 90 | 'afteractivate': function(p) { 91 | p.ref.list_selection(false, rcmail.env.contextmenu_selection); 92 | }}); 93 | ``` 94 | 95 | The ContextMenu must then be attached to the element(s) in the UI. For example: 96 | ```js 97 | $(el).on("contextmenu", function(e) { 98 | rcmail.contextmenu.show_one(e, obj, source_id, menu); 99 | }); 100 | ``` 101 | 102 | The `rcmail.contextmenu.show` displays a ContextMenu on the screen. It has the following parameters: 103 | * e - (event) The JS event object 104 | * obj - (object) The object the ContextMenu is active on (typically `this`) 105 | * source_id - (string) The object ID used by core function. When using the ContextMenu on a Roundcube list object then the ID can be retrieved from the object, like this: 106 | ```js 107 | if (uid = list_object.get_row_uid(this)) { 108 | rcmail.contextmenu.show_one(e, this, uid, menu); 109 | } 110 | ``` 111 | The ID can also be extracted from the originial function call, like this: 112 | ```js 113 | if (source.attr('onclick') && (matches = source.attr('onclick').match(rcmail.contextmenu.command_pattern))) { 114 | rcmail.contextmenu.show_one(e, this, matches[2], menu); 115 | } 116 | ``` 117 | * menu - (object) The menu object as created by `rcmail.contextmenu.init` 118 | 119 | ## Menu sources 120 | 121 | The menu_source parameter can be a string (for building the ContextMenu from a single source) or an array of jQuery selectors. To add custom elements to the ContextMenu a menu element must first be added to the IU, for example: 122 | ```php 123 | $rcube = rcube::get_instance(); 124 | $li = ''; 125 | $li .= html::tag('li', null, $rcube->output->button(['command' => 'plugin.myplugin.command1', 'type' => 'link', 'class' => 'myclass1', 'label' => 'myplugin.command1'])); 126 | $li .= html::tag('li', null, $rcube->output->button(['command' => 'plugin.myplugin.command2', 'type' => 'link', 'class' => 'myclass2', 'label' => 'myplugin.command2'])); 127 | $li .= html::tag('li', null, $rcube->output->button(['command' => 'plugin.myplugin.command3', 'type' => 'link', 'class' => 'myclass3', 'label' => 'myplugin.command3'])); 128 | $out = html::tag('ul', ['id' => 'mymenu'], $li); 129 | $rcube->output->add_footer(html::div(['style' => 'display: none;'], $out)); 130 | ``` 131 | The ContextMenu can then be invoked like this: 132 | ```js 133 | var menu = rcmail.contextmenu.init({menu_name: 'mymenu', menu_source: '#mymenu'}); 134 | ``` 135 | A JSON object can also be used instead of an element selector to add simple elements to the ContextMenu. For example: 136 | ```js 137 | var menu = rcmail.contextmenu.init({menu_name: 'mymenu', menu_source: ['#mymenu', {label: 'extra item', command: 'plugin.myplugin.command', props: '', class: 'myclass'}]}); 138 | ``` 139 | The JSON object can have: 140 | * `label` (string) required - text for the menu element 141 | * `command` (string) required - the Roundcube command to execute on click 142 | * `props` (string) optional - arguments to pass to the Roundcube command 143 | * `classes` (string) optional - classes to apply to the menu element 144 | 145 | ## Working with an existing ContextMenu 146 | 147 | A global event `contextmenu_init` is triggered when a new ContextMenu is initialised so other plugins can interact with it. 148 | ```js 149 | rcmail.addEventListener('contextmenu_init', function(menu) { 150 | // identify the folder list context menu 151 | if (menu.menu_name == 'folderlist') { 152 | // add a shortcut to the folder management screen to the end of the menu 153 | menu.menu_source.push({label: 'Manage folders', command: 'folders', props: '', classes: 'managefolders'}); 154 | 155 | // make sure this new shortcut is always active 156 | menu.addEventListener('activate', function(p) { 157 | if (p.command == 'folders') { 158 | return true; 159 | } 160 | }); 161 | } 162 | }); 163 | ``` 164 | 165 | The Contextmnu object is passed to the function allowing properities to be manipulated and/or new events to be attached. By default the following menus are created: 166 | 167 | On the mail screen: 168 | * messagelist - attached to rows in the message list 169 | * folderlist - attached to entries in the folder list 170 | 171 | On the message composing screen: 172 | * composeto - attached to contacts in the contacts search widget 173 | 174 | On the address book screen: 175 | * contactlist - attached to rows in the contacts list 176 | * abooklist - attached to addressbooks and groups 177 | 178 | To prevent an element from appearing in a ContextMenu give it the class `rcm-ignore`. 179 | 180 | To make sure an element in the ContextMenu is always active give it the class `rcm-active`. 181 | 182 | The environmental variable `rcmail.env.context_menu_source_id` contains the ID of the specific element that the ContextMenu was triggered on, this is the `source_id` passed to `rcmail.contextmenu.show` 183 | 184 | ## Events 185 | 186 | The following events are triggered by ContextMenu: 187 | 188 | `init` - Triggered once the ContextMenu object has been initalized 189 | * ref - The ContextMenu object 190 | 191 | `addmenuitem` - Triggered when an element is being added to the menu 192 | * ref - The ContextMenu object 193 | * el - The element being added 194 | This function can return the following in a JSON object: 195 | * result - The element to be added to the menu 196 | * abort - Boolean, abort the default command execution 197 | 198 | `beforecommand` - Triggered when an element in the menu is clicked 199 | * ref - The ContextMenu object 200 | * el - The HTML object being clicked 201 | * command - The Roundcube command to run 202 | * args - The arguments being passed to the Roundcube command 203 | This function can return the following in a JSON object: 204 | * abort - Boolean, abort the default command execution, other events like `command` and `aftercommand` will not be executed 205 | * result - Result of the command, if abort if true this is returned to the client 206 | 207 | `command` - Triggered when an element in the menu is clicked 208 | * ref - The ContextMenu object 209 | * el - The HTML object being clicked 210 | * command - The Roundcube command to run 211 | * args - The arguments being passed to the Roundcube command 212 | * evt - The JS event object 213 | This function can return the result of the command to pass back to the client 214 | 215 | By default the following function is used: 216 | 217 | ```js 218 | function(p) { 219 | if (!$(p.el).hasClass(rcmail.contextmenu.settings.classes.button_active)) 220 | return; 221 | 222 | // enable the required command 223 | var prev_command = rcmail.commands[p.command]; 224 | rcmail.enable_command(p.command, true); 225 | var result = rcmail.command(p.command, p.args, p.el, p.evt); 226 | rcmail.enable_command(p.command, prev_command); 227 | 228 | // leave commands in always_enable_commands enabled, they are disabled when menu is closes 229 | if ($.inArray(p.command, rcmail.contextmenu.settings.always_enable_commands) >= 0 && prev_command === false) { 230 | rcmail.contextmenu.vars.commands_disable_on_hide.push(p.command); 231 | } 232 | else { 233 | rcmail.enable_command(p.command, prev_command); 234 | } 235 | 236 | return result; 237 | } 238 | ``` 239 | 240 | The ContextMenu works by faking a message selection and calling the normal Roundcube command before putting everything back to normal. 241 | 242 | `aftercommand` - Triggered when an element in the menu is clicked 243 | * ref - The ContextMenu object 244 | * el - The HTML object being clicked 245 | * command - The Roundcube command to run 246 | * args - The arguments being passed to the Roundcube command 247 | 248 | `submenu_toggle` - Triggered when a hidden menu is being loaded for display in the ContextMenu 249 | * id - The ID of the menu being shown 250 | * ref - The ContextMenu object 251 | * show - Boolean, show the menu or not 252 | 253 | `beforeactivate` - Triggered when a ContextMenu is displayed 254 | * ref - The ContextMenu object 255 | * source - The element the ContextMenu has been triggered on 256 | * originalEvent - The triggering event 257 | This function can return the following in a JSON object: 258 | * abort - Boolean, abort the default activation process, other events like `activate` and `afteractivat` will not be executed 259 | * show - Boolean, show the menu or not 260 | 261 | `activate` - Triggered when a ContextMenu is displayed, a separate event is triggered for each menu item 262 | * el - The menu element being activated 263 | * btn - The ID of the button in the UI on which the menu element is based 264 | * source - The element the ContextMenu has been triggered on 265 | * command - The command the menu element executes 266 | * enabled - Boolean, if the menu element is active or not 267 | This function can return the following in a JSON object: 268 | * abort - Boolean, abort the default activation process 269 | * enabled - Boolean, set the menu element to active or not 270 | 271 | `afteractivate` - Triggered when a ContextMenu is displayed 272 | * ref - The ContextMenu object 273 | * source - The element the ContextMenu has been triggered on 274 | * originalEvent - The triggering event 275 | 276 | `insertitem` - Triggered each time an item is added to a ContextMenu 277 | * ref - The ContextMenu object 278 | * item - The HTML object to be added to the menu 279 | * originalElement - The original element from the UI 280 | 281 | `hide_menu` - Triggered a menu is hidden 282 | * ref - The ContextMenu object 283 | * originalElement - The original element from the UI 284 | 285 | For example permanently deactivating the delete option on the message list ContextMenu could be done like this: 286 | ```js 287 | rcmail.addEventListener('contextmenu_init', function(menu) { 288 | if (menu.menu_name == 'messagelist') { 289 | menu.addEventListener('activate', function(p) { 290 | var is_delete = false; 291 | 292 | $.each(rcmail.buttons['delete'], function() { 293 | if (this.id == p.btn) { 294 | is_delete = true; 295 | return false; 296 | } 297 | }); 298 | 299 | return is_delete ? false : null; 300 | }); 301 | } 302 | }); 303 | ``` 304 | 305 | ## ContextMenu and skins 306 | 307 | In the plugin folder there is a skins folder, and inside that there is a folder for each skin. Two files are needed for each skin: contextmenu.css - CSS for the menu, and functions.js containing the JavaScript to create ContextMenus in the skin. This plugin provides some helper functions for adding the default menus to the UI, they are: `rcmail.contextmenu.init_list()` for attaching a ContextMenu to a Roundcube list object, `rcmail.contextmenu.init_folder()` for attaching a ContextMenu to the folder list on the mail screen, `rcmail.contextmenu.init_addressbook()` for attaching a ContextMenu to the address book and groups list on the address book screen, and `rcmail.contextmenu.init_settings()` for attaching a ContextMenu to the settings list. Each function expects the same 3 parameters: 308 | * The HTML object or jQuery selector of the element to attach to. 309 | * A props object, see [Creating a new ContextMenu](#creating-a-new-contextmenu) 310 | * An events object, see [Events](#events) 311 | 312 | Roundcube, and this plugin, also support placing skin information for plugins' in the core skin's folder, see the [Roundcube Wiki](https://github.com/roundcube/roundcubemail/wiki/Skins#skinning-plugins) for more information. 313 | 314 | ContextMenus must be defined separately for each skin because they are built from the toolbar elements in the UI which may have different IDs as well as different construction on each skin. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Roundcube Webmail ContextMenu 2 | ============================= 3 | This plugin creates contextmenus for various parts of Roundcube using commands 4 | from the toolbars. 5 | 6 | ATTENTION 7 | --------- 8 | This is just a snapshot from the GIT repository and is **NOT A STABLE version 9 | of ContextMenu**. It is Intended for use with the **GIT-master** version of 10 | Roundcube and it may not be compatible with older versions. Stable versions of 11 | ContextMenu are available from the [Roundcube plugin repository][rcplugrepo] 12 | (for 1.0 and above) or the [releases section][releases] of the GitHub 13 | repository. 14 | 15 | License 16 | ------- 17 | This plugin is released under the [GNU General Public License Version 3+][gpl]. 18 | 19 | Even if skins might contain some programming work, they are not considered 20 | as a linked part of the plugin and therefore skins DO NOT fall under the 21 | provisions of the GPL license. See the README file located in the core skins 22 | folder for details on the skin license. 23 | 24 | Install 25 | ------- 26 | * Place this plugin folder into plugins directory of Roundcube 27 | * Add contextmenu to $config['plugins'] in your Roundcube config 28 | 29 | **NB:** When downloading the plugin from GitHub you will need to create a 30 | directory called contextmenu and place the files in there, ignoring the root 31 | directory in the downloaded archive. 32 | 33 | ContextMenu manual 34 | ------------------ 35 | The MANUAL.md file shipped with this plugin contains information for plugin and 36 | skin developers. The ContextMenu plugin can be extended by other plugins; new 37 | menus can be created and existing menus manipulated. 38 | 39 | Customizing the Elastic skin 40 | ---------------------------- 41 | The colors and styles used by this plugin can be overridden by adding a 42 | `_custom.less` file to the `skins/elastic` sub-folder of this plugin and 43 | then recompiling the CSS. 44 | 45 | [rcplugrepo]: https://plugins.roundcube.net/#/packages/johndoh/contextmenu 46 | [releases]: https://github.com/johndoh/roundcube-contextmenu/releases 47 | [gpl]: https://www.gnu.org/licenses/gpl.html -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "johndoh/contextmenu", 3 | "description": "Adds context menus with common tasks to various parts of Roundcube", 4 | "license": "GPL-3.0-or-later", 5 | "type": "roundcube-plugin", 6 | "keywords": [ 7 | "context", 8 | "menu", 9 | "right-click" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Philip Weir", 14 | "email": "roundcube@tehinterweb.co.uk", 15 | "role": "Developer" 16 | } 17 | ], 18 | "homepage": "https://github.com/johndoh/roundcube-contextmenu/", 19 | "require": { 20 | "php": ">=7.3.0", 21 | "roundcube/plugin-installer": "~0.3.5" 22 | }, 23 | "autoload": { 24 | "classmap": [ 25 | "contextmenu.php" 26 | ] 27 | }, 28 | "extra": { 29 | "roundcube": { 30 | "min-version": "1.5", 31 | "persistent-files": [ 32 | "skins/elastic/_custom.less" 33 | ] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /contextmenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ContextMenu plugin script 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this file. 6 | * 7 | * Copyright (C) Philip Weir 8 | * 9 | * The JavaScript code in this page is free software: you can redistribute it 10 | * and/or modify it under the terms of the GNU General Public License 11 | * as published by the Free Software Foundation, either version 3 of 12 | * the License, or (at your option) any later version. 13 | * 14 | * @licend The above is the entire license notice 15 | * for the JavaScript code in this file. 16 | */ 17 | 18 | rcube_webmail.prototype.contextmenu = { 19 | settings: { 20 | no_right_click_on_menu: true, 21 | skip_commands: ['mail-checkmail', 'mail-compose', 'addressbook-add', 'addressbook-import', 'addressbook-advanced-search', 'addressbook-search-create'], 22 | command_pattern: /rcmail\.command\(\u0027([^\u0027]+)\u0027,\s?\u0027((?:\\\u0027|[^\u0027])*)\u0027/, 23 | addressbook_pattern: /([A-Z0-9\-_]+(:[A-Z0-9\-_]+)?)/i, 24 | popup_attrib: 'onclick', 25 | popup_pattern: '', 26 | popup_func: '', 27 | classes: { 28 | container: 'contextmenu popupmenu', 29 | mainmenu: 'rcmmainmenu rcm-mainmenu', // rcmmainmenu class depreciated in v3.0 30 | submenu: 'rcmsubmenu submenu rcm-submenu', // rcmsubmenu submenu classes depreciated in v3.0 31 | button_remove: 'button', 32 | button_active: 'active', 33 | button_disabled: 'disabled', 34 | }, 35 | menu_defaults: {}, 36 | global_events: { 37 | submenu_toggle: function (p) { 38 | $(p.id).parent()[(p.show) ? 'show' : 'hide'](); 39 | }, 40 | }, 41 | menu_events: { 42 | command: function (p) { 43 | if (!$(p.el).is('.' + rcmail.contextmenu.settings.classes.button_active.replace(/ /g, ', .'))) { 44 | return; 45 | } 46 | 47 | var result = rcmail.command(p.command, p.args, p.el, p.evt, true); 48 | 49 | return result; 50 | }, 51 | }, 52 | }, 53 | 54 | vars: { 55 | popup_menus: [], 56 | popup_commands: {}, 57 | }, 58 | 59 | skin_funcs: {}, 60 | 61 | init_list: function (row, props, events) { 62 | if (!events) { 63 | events = {}; 64 | } 65 | 66 | // backwards compatibility, list_object changed to string in v3.0 67 | if (typeof props.list_object == 'object') { 68 | console.log('Roundcube ContextMenu plugin: init_list() list_object param object detected, expected string.'); 69 | var id = $(props.list_object.list).attr('id'); 70 | props.list_object = null; 71 | 72 | if (id == 'contacts-table') { 73 | props.list_object = 'contact_list'; 74 | } 75 | } 76 | 77 | if (!props.list_object) { 78 | props.list_object = 'message_list'; 79 | } 80 | 81 | var trigger_list_select = function (list) { 82 | var prev_contentframe = rcmail.env.contentframe; 83 | rcmail.env.contentframe = null; 84 | rcmail[list].triggerEvent('select'); 85 | rcmail.env.contentframe = prev_contentframe; 86 | }; 87 | 88 | var menu = rcmail.contextmenu.init(props, $.extend({ 89 | beforeactivate: function (p) { 90 | if (p.ref.is_submenu) { 91 | p.ref.menu_selection = p.ref.parent_menu.menu_selection; 92 | } 93 | else if (rcmail[p.ref.list_object].selection.length == 0 || !rcmail[p.ref.list_object].in_selection(rcmail.env.context_menu_source_id)) { 94 | p.ref.menu_selection = [rcmail.env.context_menu_source_id]; 95 | } 96 | else { 97 | p.ref.menu_selection = rcmail[p.ref.list_object].get_selection(); 98 | } 99 | 100 | if (!rcmail[p.ref.list_object].in_selection(rcmail.env.context_menu_source_id)) { 101 | rcmail.env.contextmenu_opening = 'beforeactivate'; 102 | trigger_list_select(p.ref.list_object); 103 | } 104 | 105 | if (rcmail.task == 'settings' && p.ref.menu_name != 'settingslist') { 106 | p.ref.container.find('a.openextwin').parent().remove(); 107 | } 108 | }, 109 | afteractivate: function (p) { 110 | rcmail.env.contextmenu_opening = 'afteractivate'; 111 | if (!rcmail[p.ref.list_object].in_selection(rcmail.env.context_menu_source_id)) { 112 | trigger_list_select(p.ref.list_object); 113 | } 114 | 115 | rcmail.env.contextmenu_opening = null; 116 | }, 117 | beforecommand: function (p) { 118 | if (rcmail.task == 'settings' && p.ref.menu_name != 'settingslist' && p.command == 'plugin.contextmenu.openinline') { 119 | rcmail[p.ref.list_object].select_row(rcmail.env.context_menu_source_id); 120 | return { abort: true, result: true }; 121 | } 122 | }, 123 | aftercommand: function (p) { 124 | if (p.command == 'group-remove-selected') { 125 | // update contactlist after contact remove (not done by core because there is no selection) 126 | rcmail.command('listgroup', { source: rcmail.env.source, id: rcmail.env.group }, p.el); 127 | } 128 | }, 129 | }, events)); 130 | 131 | $('#' + row).on('contextmenu', function (e) { 132 | var uid; 133 | if (uid = rcmail[props.list_object].get_row_uid(this)) { 134 | rcmail.contextmenu.hide_all(e); 135 | rcmail.contextmenu.show_one(e, this, uid, menu); 136 | } 137 | }); 138 | 139 | rcmail[props.list_object].addEventListener('getselection', function (p) { 140 | if (rcmail.env.contextmenu_opening == 'afteractivate') { 141 | return; 142 | } 143 | 144 | var uids = null; 145 | $.each(rcmail.env.contextmenus, function () { 146 | if (this.menu_selection.length > 0 && (rcmail.env.contextmenu_opening == 'beforeactivate' || $(this.container).is(':visible'))) { 147 | uids = this.menu_selection; 148 | return false; 149 | } 150 | }); 151 | 152 | if (uids) { 153 | p.res = uids; 154 | return false; 155 | } 156 | }); 157 | }, 158 | 159 | init_folder: function (el, props, events) { 160 | if (!events) { 161 | events = {}; 162 | } 163 | 164 | var menu = rcmail.contextmenu.init($.extend({ menu_name: 'folderlist', list_object: null }, props), $.extend({ 165 | beforeactivate: function () { 166 | if (rcmail.env.contextmenu_messagecount_request) { 167 | rcmail.env.contextmenu_messagecount_request.abort(); 168 | } 169 | rcmail.env.contextmenu_messagecount_request = null; 170 | }, 171 | activate: function (p) { 172 | return rcmail.contextmenu.activate_folder_commands(p); 173 | }, 174 | beforecommand: function (p) { 175 | if (rcmail.env.context_menu_source_id != rcmail.env.mailbox && $.inArray(p.command, ['expunge', 'purge']) >= 0) { 176 | rcmail[p.command + '_mailbox'](rcmail.env.context_menu_source_id); 177 | return { abort: true, result: true }; 178 | } 179 | if (rcmail.env.context_menu_source_id != rcmail.env.mailbox && p.command == 'mark-all-read') { 180 | rcmail.mark_all_read(rcmail.env.context_menu_source_id); 181 | return { abort: true, result: true }; 182 | } 183 | }, 184 | }, events)); 185 | 186 | $(el).on('click', function (e) { 187 | // hide menu when changing folder 188 | rcmail.contextmenu.hide_all(e); 189 | }) 190 | .on('contextmenu', function (e) { 191 | var source = $(this).find('a[rel][onclick]').filter(function () { return $(this).attr('onclick').match(rcmail.contextmenu.settings.command_pattern); }).first(); 192 | source.blur(); // remove focus (and keyboard nav highlighting) from source element 193 | 194 | rcmail.contextmenu.hide_all(e); 195 | rcmail.contextmenu.show_one(e, this, source.attr('rel'), menu); 196 | }); 197 | }, 198 | 199 | init_addressbook: function (el, props, events) { 200 | if (!events) { 201 | events = {}; 202 | } 203 | 204 | var menu = rcmail.contextmenu.init($.extend({ menu_name: 'abooklist' }, props), $.extend({ 205 | beforeactivate: function (p) { 206 | p.ref.container.find('li.' + rcmail.contextmenu.settings.classes.submenu.replace(/ /g, '.')).remove(); 207 | }, 208 | activate: function (p) { 209 | var ids = rcmail.env.context_menu_source_id.split(':', 2); 210 | var cur_source = ids[0]; 211 | 212 | if (p.command == 'group-create') { 213 | // addressbook 214 | if ($(p.source).hasClass('addressbook') && rcmail.env.address_sources[cur_source].groups && !rcmail.env.address_sources[cur_source].readonly) { 215 | p.enabled = true; 216 | } 217 | else { 218 | p.enabled = false; 219 | } 220 | } 221 | else if (p.command == 'group-rename' || p.command == 'group-delete') { 222 | // group 223 | if ($(p.source).hasClass('contactgroup') && !rcmail.env.address_sources[cur_source].readonly) { 224 | p.enabled = true; 225 | } 226 | else { 227 | p.enabled = false; 228 | } 229 | } 230 | else if (p.command == 'search-delete') { 231 | // saved search 232 | if ($(p.source).hasClass('contactsearch')) { 233 | p.enabled = true; 234 | } 235 | else { 236 | p.enabled = false; 237 | } 238 | } 239 | 240 | return p; 241 | }, 242 | command: function (p) { 243 | if (!$(p.el).is('.' + rcmail.contextmenu.settings.classes.button_active.replace(/ /g, ', .'))) { 244 | return; 245 | } 246 | 247 | var prev_source = rcmail.env.source; 248 | var prev_group = rcmail.env.group; 249 | var result = false; 250 | 251 | var ids = rcmail.env.context_menu_source_id.split(':', 2); 252 | var cur_source = ids[0]; 253 | var cur_id = ids[1]; 254 | 255 | rcmail.env.source = cur_source; 256 | rcmail.env.group = cur_id; 257 | 258 | switch (p.command) { 259 | case 'search-delete': 260 | if ($(p.ref.selected_object).children('a').attr('rel')) { 261 | var prev_search_id = rcmail.env.search_id; 262 | var prev_search_request = rcmail.env.search_request; 263 | rcmail.env.search_request = true; 264 | rcmail.env.search_id = $(p.ref.selected_object).children('a').attr('rel').replace('S', ''); 265 | 266 | result = rcmail.command(p.command, p.args, p.el, p.evt, true); 267 | 268 | rcmail.env.search_request = prev_search_request; 269 | rcmail.env.search_id = prev_search_id; 270 | } 271 | break; 272 | default: 273 | result = rcmail.command(p.command, p.args, p.el, p.evt, true); 274 | break; 275 | } 276 | 277 | rcmail.env.source = prev_source; 278 | rcmail.env.group = prev_group; 279 | 280 | return result; 281 | }, 282 | }, events)); 283 | 284 | $(el).on('click', function (e) { 285 | // hide menu when changing address book 286 | rcmail.contextmenu.hide_all(e); 287 | }) 288 | .on('contextmenu', function (e) { 289 | var source = $(this).find('a[rel]').filter(function () { return $(this).attr('rel').match(rcmail.contextmenu.settings.addressbook_pattern); }).first(), matches; 290 | source.blur(); // remove focus (and keyboard nav highlighting) from source element 291 | 292 | if (matches = source.attr('rel').match(rcmail.contextmenu.settings.addressbook_pattern)) { 293 | rcmail.contextmenu.hide_all(e); 294 | rcmail.contextmenu.show_one(e, this, matches[1], menu); 295 | } 296 | }); 297 | }, 298 | 299 | init_settings: function (el, props, events) { 300 | if (!events) { 301 | events = {}; 302 | } 303 | 304 | var menu = rcmail.contextmenu.init(props, $.extend({ 305 | beforeactivate: function (p) { 306 | if (p.ref.menu_name != 'settingslist') { 307 | p.ref.container.find('a.openextwin').parent().remove(); 308 | } 309 | 310 | if (p.ref.menu_name == 'folderlist') { 311 | if (rcmail.env.contextmenu_messagecount_request) { 312 | rcmail.env.contextmenu_messagecount_request.abort(); 313 | } 314 | rcmail.env.contextmenu_messagecount_request = null; 315 | } 316 | }, 317 | activate: function (p) { 318 | if (p.ref.menu_name == 'folderlist') { 319 | p = rcmail.contextmenu.activate_folder_commands(p); 320 | } 321 | 322 | return p; 323 | }, 324 | beforecommand: function (p) { 325 | if (p.ref.menu_name == 'folderlist') { 326 | var result; 327 | if ($.inArray(p.command, ['delete-folder', 'purge']) >= 0) { 328 | result = rcmail[p.command == 'delete-folder' ? 'delete_folder' : p.command + '_mailbox'](rcmail.env.context_menu_source_id); 329 | } 330 | else if (p.command == 'plugin.contextmenu.openinline') { 331 | rcmail[p.ref.list_object].select(rcmail.env.context_menu_source_id); 332 | } 333 | else { 334 | result = rcmail.command(p.command, p.args); 335 | } 336 | 337 | return { abort: true, result: true }; 338 | } 339 | }, 340 | }, events)); 341 | 342 | $(el).on('contextmenu', function (e) { 343 | var source = $(this).find('a').first(); 344 | source.blur(); // remove focus (and keyboard nav highlighting) from source element 345 | 346 | if (props.menu_name == 'settingslist') { 347 | var matches; 348 | if (matches = source.attr('onclick').match(rcmail.contextmenu.settings.command_pattern)) { 349 | rcmail.contextmenu.hide_all(e); 350 | rcmail.contextmenu.show_one(e, this, matches[2], menu); 351 | } 352 | } 353 | else if (props.menu_name == 'folderlist') { 354 | rcmail.contextmenu.hide_all(e); 355 | rcmail.contextmenu.show_one(e, this, rcmail.folder_id2name($(this).attr('id')), menu); 356 | } 357 | }); 358 | }, 359 | 360 | init: function (props, ext_events) { 361 | var events = $.extend({}, rcmail.contextmenu.settings.menu_events, ext_events), menu; 362 | 363 | if (!rcmail.env.contextmenus[props.menu_name]) { 364 | menu = new rcube_context_menu(props); 365 | 366 | $.each(rcmail.contextmenu.settings.global_events, function (trigger, func) { 367 | menu.addEventListener(trigger, function (p) { return func(p); }); 368 | }); 369 | 370 | $.each(events, function (trigger, func) { 371 | menu.addEventListener(trigger, function (p) { return func(p); }); 372 | }); 373 | 374 | menu.init(); 375 | rcmail.env.contextmenus[props.menu_name] = menu; 376 | } 377 | else { 378 | menu = rcmail.env.contextmenus[props.menu_name]; 379 | } 380 | 381 | return menu; 382 | }, 383 | 384 | show_one: function (e, src_elm, src_id, menu_obj) { 385 | // if contextmenus have been disabled then show browser context menu as normal 386 | if (!rcmail.env.contextmenu) { 387 | return true; 388 | } 389 | 390 | rcube_event.cancel(e); 391 | 392 | // hide any other open menus 393 | for (var i = 0; i < rcmail.contextmenu.vars.popup_menus.length; i++) { 394 | rcmail.hide_menu(rcmail.contextmenu.vars.popup_menus[i], e); 395 | } 396 | 397 | rcmail.env.context_menu_source_id = src_id; 398 | menu_obj.show_menu(src_elm, e); 399 | }, 400 | 401 | hide_all: function (e, sub_only, no_trigger) { 402 | var remove_menu = function (e, menu) { 403 | if (!menu.container.is(':visible')) { 404 | return; 405 | } 406 | 407 | menu.hide_menu(e); 408 | 409 | if (!no_trigger) { 410 | var menu_name = 'rcm_' + menu.menu_name; 411 | rcmail.triggerEvent('menu-close', { name: menu_name, props: { menu: menu_name }, originalEvent: e }); 412 | } 413 | }; 414 | 415 | $.each(rcmail.env.contextmenus, function () { 416 | if (!sub_only) { 417 | remove_menu(e, this); 418 | } 419 | else { 420 | $.each(this.submenus, function () { 421 | remove_menu(e, this); 422 | }); 423 | } 424 | }); 425 | 426 | // close popup menus opened by the contextmenu 427 | for (var i = rcmail.contextmenu.vars.popup_menus.length - 1; i >= 0; i--) { 428 | rcmail.hide_menu(rcmail.contextmenu.vars.popup_menus[i], e); 429 | } 430 | }, 431 | 432 | activate_folder_commands: function (p) { 433 | if ($.inArray(p.command, ['expunge', 'purge', 'mark-all-read']) >= 0) { 434 | // disable the commands by default 435 | p.enabled = false; 436 | 437 | // if menu is opened on current folder (or special mark-all-read command) then enable the commands same as in UI 438 | if ((rcmail.env.context_menu_source_id == rcmail.env.mailbox || p.command == 'mark-all-read') && rcmail.commands[p.command]) { 439 | p.enabled = true; 440 | } 441 | // if menu is opened on difference folder then get message count for the folder 442 | else if (rcmail.env.context_menu_source_id != rcmail.env.mailbox && !rcmail.env.contextmenu_messagecount_request) { 443 | p.enabled = false; 444 | 445 | // folder check called async to prevent slowdown on menu load 446 | rcmail.env.contextmenu_messagecount_request = $.ajax({ 447 | type: 'POST', 448 | url: rcmail.url('plugin.contextmenu.messagecount'), 449 | data: { _mbox: rcmail.env.context_menu_source_id }, 450 | dataType: 'json', 451 | async: true, 452 | success: function (data) { 453 | if (data.messagecount > 0 && $('#rcm_folderlist').is(':visible')) { 454 | // override the environment to check if commands should be abled 455 | var temp_exists = rcmail.env.exists; 456 | var temp_mailbox = rcmail.env.mailbox; 457 | rcmail.env.exists = data.messagecount; 458 | rcmail.env.mailbox = rcmail.env.context_menu_source_id; 459 | 460 | if (data.messagecount > 0) { 461 | $('#rcm_folderlist').find('a.cmd_expunge').addClass(rcmail.contextmenu.settings.classes.button_active).removeClass(rcmail.contextmenu.settings.classes.button_disabled); 462 | $('#rcm_folderlist').find('a.cmd_purge').addClass(rcmail.contextmenu.settings.classes.button_active).removeClass(rcmail.contextmenu.settings.classes.button_disabled); 463 | } 464 | 465 | rcmail.env.exists = temp_exists; 466 | rcmail.env.mailbox = temp_mailbox; 467 | } 468 | }, 469 | }); 470 | } 471 | } 472 | else if ($.inArray(p.command, ['plugin.contextmenu.collapseall', 'plugin.contextmenu.expandall']) >= 0) { 473 | var list_obj = rcmail.gui_objects[rcmail.env.task == 'settings' ? 'subscriptionlist' : 'mailboxlist']; 474 | var class_name = p.command == 'plugin.contextmenu.collapseall' ? 'expanded' : 'collapsed'; 475 | 476 | p.enabled = false; 477 | if ($(list_obj).find('div.' + class_name + ':visible').length > 0) { 478 | p.enabled = true; 479 | } 480 | } 481 | else if (rcmail.env.task == 'settings' && p.command == 'delete-folder') { 482 | p.enabled = false; 483 | // From rcmail::subscription_select() 484 | var folder; 485 | if ((folder = rcmail.env.subscriptionrows[rcmail.env.context_menu_source_id]) && !folder[2]) { 486 | p.enabled = true; 487 | } 488 | } 489 | 490 | return p; 491 | }, 492 | 493 | ui_button_check: function (btn, active) { 494 | var classes = (active ? rcmail.contextmenu.settings.classes.button_active : rcmail.contextmenu.settings.classes.button_disabled); 495 | classes = '.' + classes.replace(/ /g, ', .'); 496 | 497 | return $('#' + btn).is(classes); 498 | }, 499 | 500 | settings_menus: function (menus) { 501 | var default_events = { 502 | init: function (p) { 503 | // remove options like create, import etc from the settings menu 504 | // addgroup class is used in Classic skin 505 | p.ref.container.find('a.addgroup,a.create,a.search,a.import').parent().remove(); 506 | }, 507 | }; 508 | 509 | $.each(menus, function () { 510 | var menu = this; 511 | 512 | if ($('#' + menu.obj).length > 0) { 513 | rcmail.addEventListener('init', function () { 514 | if (menu.props.init_func) { 515 | rcmail.contextmenu[menu.props.init_func]('#' + menu.obj, menu.props, menu.events || default_events); 516 | } 517 | else if (rcmail[menu.props.list_object]) { 518 | rcmail.contextmenu.init_list(menu.obj, menu.props, menu.events || default_events); 519 | 520 | rcmail[menu.props.list_object].addEventListener('initrow', function (props) { 521 | rcmail.contextmenu.init_list(props.id, menu.props, menu.events || default_events); 522 | }); 523 | } 524 | }); 525 | } 526 | else if (menu.props.list_object && menu.props.list_id) { 527 | rcmail.addEventListener('initlist', function (props) { 528 | if ($(props.obj).attr('id') == menu.props.list_id) { 529 | rcmail[menu.props.list_object].addEventListener('initrow', function (props) { 530 | rcmail.contextmenu.init_list(props.id, menu.props, menu.events || default_events); 531 | }); 532 | } 533 | }); 534 | } 535 | }); 536 | }, 537 | }; 538 | 539 | // backwards compatibility, functions renamed in v3.0 540 | function rcm_listmenu_init(row, props, events) { rcm_log('rcm_listmenu_init'); rcmail.contextmenu.init_list(row, props, events); } 541 | function rcm_foldermenu_init(el, props, events) { rcm_log('rcm_foldermenu_init'); rcmail.contextmenu.init_folder(el, props, events); } 542 | function rcm_abookmenu_init(el, props, events) { rcm_log('rcm_abookmenu_init'); rcmail.contextmenu.init_addressbook(el, props, events); } 543 | function rcm_callbackmenu_init(props, ext_events) { rcm_log('rcm_callbackmenu_init'); return rcmail.contextmenu.init(props, ext_events); } 544 | function rcm_show_menu(e, obj, id, menu) { rcm_log('rcm_show_menu'); rcmail.contextmenu.show_one(e, obj, id, menu); } 545 | function rcm_hide_menu(e, sub_only, no_trigger) { rcm_log('rcm_hide_menu'); rcmail.contextmenu.hide_all(e, sub_only, no_trigger); } 546 | function rcm_check_button_state(btn, active) { rcm_log('rcm_check_button_state'); return rcmail.contextmenu.ui_button_check(btn, active); } 547 | function rcm_log(fname) { console.log('Roundcube ContextMenu plugin: Use of ' + fname + ' is depreciated. This will be removed in future versions.'); } 548 | 549 | function rcube_context_menu(p) { 550 | this.menu_name = null; 551 | this.menu_source = []; 552 | this.menu_source_obj = null; 553 | this.list_object = null; 554 | this.mouseover_timeout = rcmail.env.contextmenu_mouseover_timeout; 555 | this.classes = { 556 | source: 'contextRow context-source', // contextRow class depreciated in v3.0 557 | ul: 'toolbarmenu iconized', 558 | a: 'icon', 559 | span: 'icon', 560 | sub_button_a: null, 561 | sub_button_span: 'right-arrow sub-button', // right-arrow class depreciated in v3.0 562 | }; 563 | 564 | this.modal = false; 565 | this.is_submenu = false; 566 | this.submenu_position = 'right'; 567 | this.skinable = false; 568 | this.parents = 0; 569 | this.parent_menu = this; 570 | this.parent_object = null; 571 | this.selected_object = null; 572 | this.container = null; 573 | this.menu_selection = []; 574 | this.submenus = {}; 575 | this.timers = {}; 576 | 577 | // add global config defaults and instance config 578 | $.extend(true, this, rcmail.contextmenu.settings.menu_defaults, p); 579 | 580 | // ensure manu_source option is always an array 581 | if (typeof this.menu_source == 'string') { 582 | this.menu_source = [this.menu_source]; 583 | } 584 | 585 | var ref = this; 586 | 587 | this.init = function () { 588 | if (!this.container) { 589 | rcmail.triggerEvent('contextmenu_init', this); 590 | 591 | this.container = $('
').attr('id', 'rcm_' + this.menu_name).css('display', 'none'); 592 | this.container.addClass(rcmail.contextmenu.settings.classes.container); 593 | this.container.addClass(this.is_submenu ? rcmail.contextmenu.settings.classes.submenu : rcmail.contextmenu.settings.classes.mainmenu); 594 | 595 | var rows = [], ul = $('