blob: f2fb2e4a243ca594aa42d7614ef66048b16baba4 [file] [log] [blame]
Mike Frysingercc114512017-09-11 21:39:17 -04001// 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 Hockey92479302019-09-20 14:58:37 -070013 *
14 * @constructor
Mike Frysingercc114512017-09-11 21:39:17 -040015 */
16hterm.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 Hockey92479302019-09-20 14:58:37 -070022 /** @type {!Array<!hterm.ContextMenu.Item>} */
Mike Frysingercc114512017-09-11 21:39:17 -040023 this.menu_ = [];
24};
25
Joel Hockey92479302019-09-20 14:58:37 -070026/** @typedef {{name:(string|symbol), action:function(!Event)}} */
27hterm.ContextMenu.Item;
28
Mike Frysingercc114512017-09-11 21:39:17 -040029/**
30 * Constant to add a separator to the context menu.
31 */
Joel Hockey92479302019-09-20 14:58:37 -070032hterm.ContextMenu.SEPARATOR = Symbol('-');
Mike Frysingercc114512017-09-11 21:39:17 -040033
34/**
35 * Bind context menu to a specific document element.
36 *
Joel Hockey0f933582019-08-27 18:01:51 -070037 * @param {!Document} document The document to use when creating elements.
Mike Frysingercc114512017-09-11 21:39:17 -040038 */
39hterm.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 */
52hterm.ContextMenu.prototype.regenerate_ = function() {
53 if (!this.element_) {
54 this.element_ = this.document_.createElement('menu');
55 this.element_.id = 'hterm:context-menu';
Mike Frysingercc114512017-09-11 21:39:17 -040056 } 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 Hockey92479302019-09-20 14:58:37 -070065 this.menu_.forEach(({name, action}) => {
Mike Frysingercc114512017-09-11 21:39:17 -040066 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 Hockey92479302019-09-20 14:58:37 -070091 * @param {!Array<!hterm.ContextMenu.Item>} items The menu entries.
Mike Frysingercc114512017-09-11 21:39:17 -040092 */
93hterm.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 Hockey0f933582019-08-27 18:01:51 -0700105 * @param {!Event} e The event triggering this display.
106 * @param {!hterm.Terminal=} terminal The terminal object to get style info
107 * from.
Mike Frysingercc114512017-09-11 21:39:17 -0400108 */
109hterm.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 Frysingercc114512017-09-11 21:39:17 -0400117 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 Frysingerccca8552020-10-22 21:18:44 -0400123 const docSize = this.document_.body.getBoundingClientRect();
Mike Frysingercc114512017-09-11 21:39:17 -0400124
125 this.element_.style.display = 'block';
126
127 // We can't calculate sizes until after it's displayed.
Mike Frysingerccca8552020-10-22 21:18:44 -0400128 const eleSize = this.element_.getBoundingClientRect();
Mike Frysingercc114512017-09-11 21:39:17 -0400129 // 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 */
143hterm.ContextMenu.prototype.hide = function() {
144 if (!this.element_) {
145 return;
146 }
147
148 this.element_.style.display = 'none';
149};