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. |
| 13 | */ |
| 14 | hterm.ContextMenu = function() { |
| 15 | // The document that contains this context menu. |
| 16 | this.document_ = null; |
| 17 | // The generated context menu (i.e. HTML elements). |
| 18 | this.element_ = null; |
| 19 | // The structured menu (i.e. JS objects). |
| 20 | this.menu_ = []; |
| 21 | }; |
| 22 | |
| 23 | /** |
| 24 | * Constant to add a separator to the context menu. |
| 25 | */ |
| 26 | hterm.ContextMenu.SEPARATOR = {}; |
| 27 | |
| 28 | /** |
| 29 | * Bind context menu to a specific document element. |
| 30 | * |
| 31 | * @param {HTMLDocument} document The document to use when creating elements. |
| 32 | */ |
| 33 | hterm.ContextMenu.prototype.setDocument = function(document) { |
| 34 | if (this.element_) { |
| 35 | this.element_.remove(); |
| 36 | this.element_ = null; |
| 37 | } |
| 38 | this.document_ = document; |
| 39 | this.regenerate_(); |
| 40 | this.document_.body.appendChild(this.element_); |
| 41 | }; |
| 42 | |
| 43 | /** |
| 44 | * Regenerate the HTML elements based on internal menu state. |
| 45 | */ |
| 46 | hterm.ContextMenu.prototype.regenerate_ = function() { |
| 47 | if (!this.element_) { |
| 48 | this.element_ = this.document_.createElement('menu'); |
| 49 | this.element_.id = 'hterm:context-menu'; |
| 50 | this.element_.style.cssText = ` |
| 51 | display: none; |
| 52 | border: solid 1px; |
| 53 | position: absolute; |
| 54 | `; |
| 55 | } else { |
| 56 | this.hide(); |
| 57 | } |
| 58 | |
| 59 | // Clear out existing menu entries. |
| 60 | while (this.element_.firstChild) { |
| 61 | this.element_.removeChild(this.element_.firstChild); |
| 62 | } |
| 63 | |
| 64 | this.menu_.forEach(([name, action]) => { |
| 65 | const menuitem = this.document_.createElement('menuitem'); |
| 66 | if (name === hterm.ContextMenu.SEPARATOR) { |
| 67 | menuitem.innerHTML = '<hr>'; |
| 68 | menuitem.className = 'separator'; |
| 69 | } else { |
| 70 | menuitem.innerText = name; |
| 71 | menuitem.addEventListener('mousedown', function(e) { |
| 72 | e.preventDefault(); |
| 73 | action(e); |
| 74 | }); |
| 75 | } |
| 76 | this.element_.appendChild(menuitem); |
| 77 | }); |
| 78 | }; |
| 79 | |
| 80 | /** |
| 81 | * Set all the entries in the context menu. |
| 82 | * |
| 83 | * This is an array of arrays. The first element in the array is the string to |
| 84 | * display while the second element is the function to call. |
| 85 | * |
| 86 | * The first element may also be the SEPARATOR constant to add a separator. |
| 87 | * |
| 88 | * This resets all existing menu entries. |
| 89 | * |
| 90 | * @param {Array<Array<string, function(Event)>>} items The menu entries. |
| 91 | */ |
| 92 | hterm.ContextMenu.prototype.setItems = function(items) { |
| 93 | this.menu_ = items; |
| 94 | this.regenerate_(); |
| 95 | }; |
| 96 | |
| 97 | /** |
| 98 | * Show the context menu. |
| 99 | * |
| 100 | * The event is used to determine where to show the menu. |
| 101 | * |
| 102 | * If no menu entries are defined, then nothing will be shown. |
| 103 | * |
| 104 | * @param {Event} e The event triggering this display. |
| 105 | * @param {hterm.Terminal=} terminal The terminal object to get style info from. |
| 106 | */ |
| 107 | hterm.ContextMenu.prototype.show = function(e, terminal) { |
| 108 | // If there are no menu entries, then don't try to show anything. |
| 109 | if (this.menu_.length == 0) { |
| 110 | return; |
| 111 | } |
| 112 | |
| 113 | // If we have the terminal, sync the style preferences over. |
| 114 | if (terminal) { |
| 115 | this.element_.style.backgroundColor = terminal.getBackgroundColor(); |
| 116 | this.element_.style.color = terminal.getForegroundColor(); |
| 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`; |
| 123 | const docSize = hterm.getClientSize(this.document_.body); |
| 124 | |
| 125 | this.element_.style.display = 'block'; |
| 126 | |
| 127 | // We can't calculate sizes until after it's displayed. |
| 128 | const eleSize = hterm.getClientSize(this.element_); |
| 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 | }; |