Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 1 | // Copyright 2018 The Chromium OS Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | 'use strict'; |
| 6 | |
| 7 | /** |
| 8 | * @fileoverview Context menu handling. |
| 9 | */ |
| 10 | |
| 11 | /** |
| 12 | * Manage the context menu usually shown when right clicking. |
Joel Hockey | 9247930 | 2019-09-20 14:58:37 -0700 | [diff] [blame] | 13 | * |
| 14 | * @constructor |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 15 | */ |
| 16 | hterm.ContextMenu = function() { |
| 17 | // The document that contains this context menu. |
| 18 | this.document_ = null; |
| 19 | // The generated context menu (i.e. HTML elements). |
| 20 | this.element_ = null; |
| 21 | // The structured menu (i.e. JS objects). |
Joel Hockey | 9247930 | 2019-09-20 14:58:37 -0700 | [diff] [blame] | 22 | /** @type {!Array<!hterm.ContextMenu.Item>} */ |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 23 | this.menu_ = []; |
| 24 | }; |
| 25 | |
Joel Hockey | 9247930 | 2019-09-20 14:58:37 -0700 | [diff] [blame] | 26 | /** @typedef {{name:(string|symbol), action:function(!Event)}} */ |
| 27 | hterm.ContextMenu.Item; |
| 28 | |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 29 | /** |
| 30 | * Constant to add a separator to the context menu. |
| 31 | */ |
Joel Hockey | 9247930 | 2019-09-20 14:58:37 -0700 | [diff] [blame] | 32 | hterm.ContextMenu.SEPARATOR = Symbol('-'); |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 33 | |
| 34 | /** |
| 35 | * Bind context menu to a specific document element. |
| 36 | * |
Joel Hockey | 0f93358 | 2019-08-27 18:01:51 -0700 | [diff] [blame] | 37 | * @param {!Document} document The document to use when creating elements. |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 38 | */ |
| 39 | hterm.ContextMenu.prototype.setDocument = function(document) { |
| 40 | if (this.element_) { |
| 41 | this.element_.remove(); |
| 42 | this.element_ = null; |
| 43 | } |
| 44 | this.document_ = document; |
| 45 | this.regenerate_(); |
| 46 | this.document_.body.appendChild(this.element_); |
| 47 | }; |
| 48 | |
| 49 | /** |
| 50 | * Regenerate the HTML elements based on internal menu state. |
| 51 | */ |
| 52 | hterm.ContextMenu.prototype.regenerate_ = function() { |
| 53 | if (!this.element_) { |
| 54 | this.element_ = this.document_.createElement('menu'); |
| 55 | this.element_.id = 'hterm:context-menu'; |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 56 | } else { |
| 57 | this.hide(); |
| 58 | } |
| 59 | |
| 60 | // Clear out existing menu entries. |
| 61 | while (this.element_.firstChild) { |
| 62 | this.element_.removeChild(this.element_.firstChild); |
| 63 | } |
| 64 | |
Joel Hockey | 9247930 | 2019-09-20 14:58:37 -0700 | [diff] [blame] | 65 | this.menu_.forEach(({name, action}) => { |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 66 | const menuitem = this.document_.createElement('menuitem'); |
| 67 | if (name === hterm.ContextMenu.SEPARATOR) { |
| 68 | menuitem.innerHTML = '<hr>'; |
| 69 | menuitem.className = 'separator'; |
| 70 | } else { |
| 71 | menuitem.innerText = name; |
| 72 | menuitem.addEventListener('mousedown', function(e) { |
| 73 | e.preventDefault(); |
| 74 | action(e); |
| 75 | }); |
| 76 | } |
| 77 | this.element_.appendChild(menuitem); |
| 78 | }); |
| 79 | }; |
| 80 | |
| 81 | /** |
| 82 | * Set all the entries in the context menu. |
| 83 | * |
| 84 | * This is an array of arrays. The first element in the array is the string to |
| 85 | * display while the second element is the function to call. |
| 86 | * |
| 87 | * The first element may also be the SEPARATOR constant to add a separator. |
| 88 | * |
| 89 | * This resets all existing menu entries. |
| 90 | * |
Joel Hockey | 9247930 | 2019-09-20 14:58:37 -0700 | [diff] [blame] | 91 | * @param {!Array<!hterm.ContextMenu.Item>} items The menu entries. |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 92 | */ |
| 93 | hterm.ContextMenu.prototype.setItems = function(items) { |
| 94 | this.menu_ = items; |
| 95 | this.regenerate_(); |
| 96 | }; |
| 97 | |
| 98 | /** |
| 99 | * Show the context menu. |
| 100 | * |
| 101 | * The event is used to determine where to show the menu. |
| 102 | * |
| 103 | * If no menu entries are defined, then nothing will be shown. |
| 104 | * |
Joel Hockey | 0f93358 | 2019-08-27 18:01:51 -0700 | [diff] [blame] | 105 | * @param {!Event} e The event triggering this display. |
| 106 | * @param {!hterm.Terminal=} terminal The terminal object to get style info |
| 107 | * from. |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 108 | */ |
| 109 | hterm.ContextMenu.prototype.show = function(e, terminal) { |
| 110 | // If there are no menu entries, then don't try to show anything. |
| 111 | if (this.menu_.length == 0) { |
| 112 | return; |
| 113 | } |
| 114 | |
| 115 | // If we have the terminal, sync the style preferences over. |
| 116 | if (terminal) { |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 117 | this.element_.style.fontSize = terminal.getFontSize(); |
| 118 | this.element_.style.fontFamily = terminal.getFontFamily(); |
| 119 | } |
| 120 | |
| 121 | this.element_.style.top = `${e.clientY}px`; |
| 122 | this.element_.style.left = `${e.clientX}px`; |
Mike Frysinger | ccca855 | 2020-10-22 21:18:44 -0400 | [diff] [blame] | 123 | const docSize = this.document_.body.getBoundingClientRect(); |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 124 | |
| 125 | this.element_.style.display = 'block'; |
| 126 | |
| 127 | // We can't calculate sizes until after it's displayed. |
Mike Frysinger | ccca855 | 2020-10-22 21:18:44 -0400 | [diff] [blame] | 128 | const eleSize = this.element_.getBoundingClientRect(); |
Mike Frysinger | cc11451 | 2017-09-11 21:39:17 -0400 | [diff] [blame] | 129 | // Make sure the menu isn't clipped outside of the current element. |
| 130 | const minY = Math.max(0, docSize.height - eleSize.height); |
| 131 | const minX = Math.max(0, docSize.width - eleSize.width); |
| 132 | if (minY < e.clientY) { |
| 133 | this.element_.style.top = `${minY}px`; |
| 134 | } |
| 135 | if (minX < e.clientX) { |
| 136 | this.element_.style.left = `${minX}px`; |
| 137 | } |
| 138 | }; |
| 139 | |
| 140 | /** |
| 141 | * Hide the context menu. |
| 142 | */ |
| 143 | hterm.ContextMenu.prototype.hide = function() { |
| 144 | if (!this.element_) { |
| 145 | return; |
| 146 | } |
| 147 | |
| 148 | this.element_.style.display = 'none'; |
| 149 | }; |