WebUI: Use Map instead of Mootools Hash in Torrents table
[qBittorrent.git] / src / webui / www / private / scripts / contextmenu.js
blobbe523c278ee6f2c3c509cc0880c7006b37fd8741
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2009  Christophe Dumez <chris@qbittorrent.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL".  If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
29 "use strict";
31 window.qBittorrent ??= {};
32 window.qBittorrent.ContextMenu ??= (() => {
33     const exports = () => {
34         return {
35             ContextMenu: ContextMenu,
36             TorrentsTableContextMenu: TorrentsTableContextMenu,
37             CategoriesFilterContextMenu: CategoriesFilterContextMenu,
38             TagsFilterContextMenu: TagsFilterContextMenu,
39             SearchPluginsTableContextMenu: SearchPluginsTableContextMenu,
40             RssFeedContextMenu: RssFeedContextMenu,
41             RssArticleContextMenu: RssArticleContextMenu,
42             RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu
43         };
44     };
46     let lastShownContextMenu = null;
47     const ContextMenu = new Class({
48         // implements
49         Implements: [Options, Events],
51         // options
52         options: {
53             actions: {},
54             menu: "menu_id",
55             stopEvent: true,
56             targets: "body",
57             offsets: {
58                 x: 0,
59                 y: 0
60             },
61             onShow: () => {},
62             onHide: () => {},
63             onClick: () => {},
64             fadeSpeed: 200,
65             touchTimer: 600
66         },
68         // initialization
69         initialize: function(options) {
70             // set options
71             this.setOptions(options);
73             // option diffs menu
74             this.menu = $(this.options.menu);
75             this.targets = $$(this.options.targets);
77             // fx
78             this.fx = new Fx.Tween(this.menu, {
79                 property: "opacity",
80                 duration: this.options.fadeSpeed,
81                 onComplete: () => {
82                     this.menu.style.visibility = (getComputedStyle(this.menu).opacity > 0) ? "visible" : "hidden";
83                 }
84             });
86             // hide and begin the listener
87             this.hide().startListener();
89             // hide the menu
90             this.menu.style.position = "absolute";
91             this.menu.style.top = "-900000px";
92             this.menu.style.display = "block";
93         },
95         adjustMenuPosition: function(e) {
96             this.updateMenuItems();
98             const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75;
100             if (this.menu.hasClass("scrollableMenu"))
101                 this.menu.style.maxHeight = `${scrollableMenuMaxHeight}px`;
103             // draw the menu off-screen to know the menu dimensions
104             this.menu.style.left = "-999em";
105             this.menu.style.top = "-999em";
107             // position the menu
108             let xPosMenu = e.pageX + this.options.offsets.x;
109             let yPosMenu = e.pageY + this.options.offsets.y;
110             if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
111                 xPosMenu -= this.menu.offsetWidth;
112             if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
113                 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
114             if (xPosMenu < 0)
115                 xPosMenu = 0;
116             if (yPosMenu < 0)
117                 yPosMenu = 0;
118             this.menu.style.left = `${xPosMenu}px`;
119             this.menu.style.top = `${yPosMenu}px`;
120             this.menu.style.position = "absolute";
121             this.menu.style.zIndex = "2000";
123             // position the sub-menu
124             const uls = this.menu.getElementsByTagName("ul");
125             for (let i = 0; i < uls.length; ++i) {
126                 const ul = uls[i];
127                 if (ul.hasClass("scrollableMenu"))
128                     ul.style.maxHeight = `${scrollableMenuMaxHeight}px`;
129                 const rectParent = ul.parentNode.getBoundingClientRect();
130                 const xPosOrigin = rectParent.left;
131                 const yPosOrigin = rectParent.bottom;
132                 let xPos = xPosOrigin + rectParent.width - 1;
133                 let yPos = yPosOrigin - rectParent.height - 1;
134                 if ((xPos + ul.offsetWidth) > document.documentElement.clientWidth)
135                     xPos -= (ul.offsetWidth + rectParent.width - 2);
136                 if ((yPos + ul.offsetHeight) > document.documentElement.clientHeight)
137                     yPos = document.documentElement.clientHeight - ul.offsetHeight;
138                 if (xPos < 0)
139                     xPos = 0;
140                 if (yPos < 0)
141                     yPos = 0;
142                 ul.style.marginLeft = `${xPos - xPosOrigin}px`;
143                 ul.style.marginTop = `${yPos - yPosOrigin}px`;
144             }
145         },
147         setupEventListeners: function(elem) {
148             elem.addEventListener("contextmenu", (e) => {
149                 this.triggerMenu(e, elem);
150             });
151             elem.addEventListener("click", (e) => {
152                 this.hide();
153             });
155             elem.addEventListener("touchstart", (e) => {
156                 this.hide();
157                 this.touchStartAt = performance.now();
158                 this.touchStartEvent = e;
159             }, { passive: true });
160             elem.addEventListener("touchend", (e) => {
161                 const now = performance.now();
162                 const touchStartAt = this.touchStartAt;
163                 const touchStartEvent = this.touchStartEvent;
165                 this.touchStartAt = null;
166                 this.touchStartEvent = null;
168                 const isTargetUnchanged = (Math.abs(e.event.pageX - touchStartEvent.event.pageX) <= 10) && (Math.abs(e.event.pageY - touchStartEvent.event.pageY) <= 10);
169                 if (((now - touchStartAt) >= this.options.touchTimer) && isTargetUnchanged)
170                     this.triggerMenu(touchStartEvent, elem);
171             }, { passive: true });
172         },
174         addTarget: function(t) {
175             // prevent long press from selecting this text
176             t.style.userSelect = "none";
178             this.targets[this.targets.length] = t;
179             this.setupEventListeners(t);
180         },
182         searchAndAddTargets: function() {
183             document.querySelectorAll(this.options.targets).forEach((target) => { this.addTarget(target); });
184         },
186         triggerMenu: function(e, el) {
187             if (this.options.disabled)
188                 return;
190             // prevent default, if told to
191             if (this.options.stopEvent) {
192                 e.preventDefault();
193                 e.stopPropagation();
194             }
195             // record this as the trigger
196             this.options.element = $(el);
197             this.adjustMenuPosition(e);
198             // show the menu
199             this.show();
200         },
202         // get things started
203         startListener: function() {
204             /* all elements */
205             this.targets.each((el) => {
206                 this.setupEventListeners(el);
207             }, this);
209             /* menu items */
210             this.menu.getElements("a").each(function(item) {
211                 item.addEventListener("click", (e) => {
212                     e.preventDefault();
213                     if (!item.hasClass("disabled")) {
214                         this.execute(item.href.split("#")[1], $(this.options.element));
215                         this.fireEvent("click", [item, e]);
216                     }
217                 });
218             }, this);
220             // hide on body click
221             $(document.body).addEventListener("click", () => {
222                 this.hide();
223             });
224         },
226         updateMenuItems: function() {},
228         // show menu
229         show: function(trigger) {
230             if (lastShownContextMenu && (lastShownContextMenu !== this))
231                 lastShownContextMenu.hide();
232             this.fx.start(1);
233             this.fireEvent("show");
234             lastShownContextMenu = this;
235             return this;
236         },
238         // hide the menu
239         hide: function(trigger) {
240             if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
241                 this.fx.start(0);
242                 // this.menu.fade('out');
243                 this.fireEvent("hide");
244             }
245             return this;
246         },
248         setItemChecked: function(item, checked) {
249             this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity =
250                 checked ? "1" : "0";
251             return this;
252         },
254         getItemChecked: function(item) {
255             return this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity !== "0";
256         },
258         // hide an item
259         hideItem: function(item) {
260             this.menu.getElement("a[href$=" + item + "]").parentNode.addClass("invisible");
261             return this;
262         },
264         // show an item
265         showItem: function(item) {
266             this.menu.getElement("a[href$=" + item + "]").parentNode.removeClass("invisible");
267             return this;
268         },
270         // disable the entire menu
271         disable: function() {
272             this.options.disabled = true;
273             return this;
274         },
276         // enable the entire menu
277         enable: function() {
278             this.options.disabled = false;
279             return this;
280         },
282         // execute an action
283         execute: function(action, element) {
284             if (this.options.actions[action])
285                 this.options.actions[action](element, this, action);
286             return this;
287         }
288     });
290     const TorrentsTableContextMenu = new Class({
291         Extends: ContextMenu,
293         updateMenuItems: function() {
294             let all_are_seq_dl = true;
295             let there_are_seq_dl = false;
296             let all_are_f_l_piece_prio = true;
297             let there_are_f_l_piece_prio = false;
298             let all_are_downloaded = true;
299             let all_are_stopped = true;
300             let there_are_stopped = false;
301             let all_are_force_start = true;
302             let there_are_force_start = false;
303             let all_are_super_seeding = true;
304             let all_are_auto_tmm = true;
305             let there_are_auto_tmm = false;
306             const tagCount = new Map();
307             const categoryCount = new Map();
309             const selectedRows = torrentsTable.selectedRowsIds();
310             selectedRows.forEach((item, index) => {
311                 const data = torrentsTable.rows.get(item).full_data;
313                 if (data["seq_dl"] !== true)
314                     all_are_seq_dl = false;
315                 else
316                     there_are_seq_dl = true;
318                 if (data["f_l_piece_prio"] !== true)
319                     all_are_f_l_piece_prio = false;
320                 else
321                     there_are_f_l_piece_prio = true;
323                 if (data["progress"] !== 1.0) // not downloaded
324                     all_are_downloaded = false;
325                 else if (data["super_seeding"] !== true)
326                     all_are_super_seeding = false;
328                 if ((data["state"] !== "stoppedUP") && (data["state"] !== "stoppedDL"))
329                     all_are_stopped = false;
330                 else
331                     there_are_stopped = true;
333                 if (data["force_start"] !== true)
334                     all_are_force_start = false;
335                 else
336                     there_are_force_start = true;
338                 if (data["auto_tmm"] === true)
339                     there_are_auto_tmm = true;
340                 else
341                     all_are_auto_tmm = false;
343                 const torrentTags = data["tags"].split(", ");
344                 for (const tag of torrentTags) {
345                     const count = tagCount.get(tag);
346                     tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
347                 }
349                 const torrentCategory = data["category"];
350                 const count = categoryCount.get(torrentCategory);
351                 categoryCount.set(torrentCategory, ((count !== undefined) ? (count + 1) : 1));
352             });
354             // hide renameFiles when more than 1 torrent is selected
355             if (selectedRows.length === 1) {
356                 const data = torrentsTable.rows.get(selectedRows[0]).full_data;
357                 const metadata_downloaded = !((data["state"] === "metaDL") || (data["state"] === "forcedMetaDL") || (data["total_size"] === -1));
359                 // hide renameFiles when metadata hasn't been downloaded yet
360                 metadata_downloaded
361                     ? this.showItem("renameFiles")
362                     : this.hideItem("renameFiles");
363             }
364             else {
365                 this.hideItem("renameFiles");
366             }
368             if (all_are_downloaded) {
369                 this.hideItem("downloadLimit");
370                 this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
371                 this.hideItem("sequentialDownload");
372                 this.hideItem("firstLastPiecePrio");
373                 this.showItem("superSeeding");
374                 this.setItemChecked("superSeeding", all_are_super_seeding);
375             }
376             else {
377                 const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
378                 const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
380                 if (!show_seq_dl && show_f_l_piece_prio)
381                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
382                 else
383                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
385                 if (show_seq_dl)
386                     this.showItem("sequentialDownload");
387                 else
388                     this.hideItem("sequentialDownload");
390                 if (show_f_l_piece_prio)
391                     this.showItem("firstLastPiecePrio");
392                 else
393                     this.hideItem("firstLastPiecePrio");
395                 this.setItemChecked("sequentialDownload", all_are_seq_dl);
396                 this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
398                 this.showItem("downloadLimit");
399                 this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
400                 this.hideItem("superSeeding");
401             }
403             this.showItem("start");
404             this.showItem("stop");
405             this.showItem("forceStart");
406             if (all_are_stopped)
407                 this.hideItem("stop");
408             else if (all_are_force_start)
409                 this.hideItem("forceStart");
410             else if (!there_are_stopped && !there_are_force_start)
411                 this.hideItem("start");
413             if (!all_are_auto_tmm && there_are_auto_tmm) {
414                 this.hideItem("autoTorrentManagement");
415             }
416             else {
417                 this.showItem("autoTorrentManagement");
418                 this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
419             }
421             const contextTagList = $("contextTagList");
422             tagList.forEach((tag, tagHash) => {
423                 const checkbox = contextTagList.getElement(`a[href="#Tag/${tag.name}"] input[type="checkbox"]`);
424                 const count = tagCount.get(tag.name);
425                 const hasCount = (count !== undefined);
426                 const isLesser = (count < selectedRows.length);
427                 checkbox.indeterminate = (hasCount ? isLesser : false);
428                 checkbox.checked = (hasCount ? !isLesser : false);
429             });
431             const contextCategoryList = document.getElementById("contextCategoryList");
432             category_list.forEach((category, categoryHash) => {
433                 const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category.name}"] img`);
434                 const count = categoryCount.get(category.name);
435                 const isEqual = ((count !== undefined) && (count === selectedRows.length));
436                 categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual);
437             });
438         },
440         updateCategoriesSubMenu: function(categoryList) {
441             const contextCategoryList = $("contextCategoryList");
442             contextCategoryList.getChildren().each(c => c.destroy());
444             const createMenuItem = (text, imgURL, clickFn) => {
445                 const anchor = document.createElement("a");
446                 anchor.textContent = text;
447                 anchor.addEventListener("click", () => { clickFn(); });
449                 const img = document.createElement("img");
450                 img.src = imgURL;
451                 img.alt = text;
452                 anchor.prepend(img);
454                 const item = document.createElement("li");
455                 item.appendChild(anchor);
457                 return item;
458             };
459             contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN));
460             contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(0); }));
462             const sortedCategories = [];
463             categoryList.forEach((category, hash) => sortedCategories.push({
464                 categoryName: category.name,
465                 categoryHash: hash
466             }));
467             sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
468                 left.categoryName, right.categoryName));
470             let first = true;
471             for (const { categoryName, categoryHash } of sortedCategories) {
472                 const anchor = document.createElement("a");
473                 anchor.href = `#Category/${categoryName}`;
474                 anchor.textContent = categoryName;
475                 anchor.addEventListener("click", (event) => {
476                     event.preventDefault();
477                     torrentSetCategoryFN(categoryHash);
478                 });
480                 const img = document.createElement("img");
481                 img.src = "images/view-categories.svg";
482                 anchor.prepend(img);
484                 const setCategoryItem = document.createElement("li");
485                 setCategoryItem.appendChild(anchor);
486                 if (first) {
487                     setCategoryItem.addClass("separator");
488                     first = false;
489                 }
491                 contextCategoryList.appendChild(setCategoryItem);
492             }
493         },
495         updateTagsSubMenu: function(tagList) {
496             const contextTagList = $("contextTagList");
497             while (contextTagList.firstChild !== null)
498                 contextTagList.removeChild(contextTagList.firstChild);
500             const createMenuItem = (text, imgURL, clickFn) => {
501                 const anchor = document.createElement("a");
502                 anchor.textContent = text;
503                 anchor.addEventListener("click", () => { clickFn(); });
505                 const img = document.createElement("img");
506                 img.src = imgURL;
507                 img.alt = text;
508                 anchor.prepend(img);
510                 const item = document.createElement("li");
511                 item.appendChild(anchor);
513                 return item;
514             };
515             contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN));
516             contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN));
518             const sortedTags = [];
519             tagList.forEach((tag, hash) => sortedTags.push({
520                 tagName: tag.name,
521                 tagHash: hash
522             }));
523             sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
525             for (let i = 0; i < sortedTags.length; ++i) {
526                 const { tagName, tagHash } = sortedTags[i];
528                 const input = document.createElement("input");
529                 input.type = "checkbox";
530                 input.addEventListener("click", (event) => {
531                     input.checked = !input.checked;
532                 });
534                 const anchor = document.createElement("a");
535                 anchor.href = `#Tag/${tagName}`;
536                 anchor.textContent = tagName;
537                 anchor.addEventListener("click", (event) => {
538                     event.preventDefault();
539                     torrentSetTagsFN(tagHash, !input.checked);
540                 });
541                 anchor.prepend(input);
543                 const setTagItem = document.createElement("li");
544                 setTagItem.appendChild(anchor);
545                 if (i === 0)
546                     setTagItem.addClass("separator");
548                 contextTagList.appendChild(setTagItem);
549             }
550         }
551     });
553     const CategoriesFilterContextMenu = new Class({
554         Extends: ContextMenu,
555         updateMenuItems: function() {
556             const id = Number(this.options.element.id);
557             if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
558                 this.showItem("editCategory");
559                 this.showItem("deleteCategory");
560                 if (useSubcategories)
561                     this.showItem("createSubcategory");
562                 else
563                     this.hideItem("createSubcategory");
564             }
565             else {
566                 this.hideItem("editCategory");
567                 this.hideItem("deleteCategory");
568                 this.hideItem("createSubcategory");
569             }
570         }
571     });
573     const TagsFilterContextMenu = new Class({
574         Extends: ContextMenu,
575         updateMenuItems: function() {
576             const id = Number(this.options.element.id);
577             if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
578                 this.showItem("deleteTag");
579             else
580                 this.hideItem("deleteTag");
581         }
582     });
584     const SearchPluginsTableContextMenu = new Class({
585         Extends: ContextMenu,
587         updateMenuItems: function() {
588             const enabledColumnIndex = function(text) {
589                 const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
590                 for (let i = 0; i < columns.length; ++i) {
591                     if (columns[i].textContent === "Enabled")
592                         return i;
593                 }
594             };
596             this.showItem("Enabled");
597             this.setItemChecked("Enabled", (this.options.element.getChildren("td")[enabledColumnIndex()].textContent === "Yes"));
599             this.showItem("Uninstall");
600         }
601     });
603     const RssFeedContextMenu = new Class({
604         Extends: ContextMenu,
605         updateMenuItems: function() {
606             const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
607             this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
608             switch (selectedRows.length) {
609                 case 0:
610                     // remove separator on top of newSubscription entry to avoid double line
611                     this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
612                     // menu when nothing selected
613                     this.hideItem("update");
614                     this.hideItem("markRead");
615                     this.hideItem("rename");
616                     this.hideItem("delete");
617                     this.showItem("newSubscription");
618                     this.showItem("newFolder");
619                     this.showItem("updateAll");
620                     this.hideItem("copyFeedURL");
621                     break;
622                 case 1:
623                     if (selectedRows[0] === 0) {
624                         // menu when "unread" feed selected
625                         this.showItem("update");
626                         this.showItem("markRead");
627                         this.hideItem("rename");
628                         this.hideItem("delete");
629                         this.showItem("newSubscription");
630                         this.hideItem("newFolder");
631                         this.hideItem("updateAll");
632                         this.hideItem("copyFeedURL");
633                     }
634                     else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === "") {
635                         // menu when single folder selected
636                         this.showItem("update");
637                         this.showItem("markRead");
638                         this.showItem("rename");
639                         this.showItem("delete");
640                         this.showItem("newSubscription");
641                         this.showItem("newFolder");
642                         this.hideItem("updateAll");
643                         this.hideItem("copyFeedURL");
644                     }
645                     else {
646                         // menu when single feed selected
647                         this.showItem("update");
648                         this.showItem("markRead");
649                         this.showItem("rename");
650                         this.showItem("delete");
651                         this.showItem("newSubscription");
652                         this.hideItem("newFolder");
653                         this.hideItem("updateAll");
654                         this.showItem("copyFeedURL");
655                     }
656                     break;
657                 default:
658                     // menu when multiple items selected
659                     this.showItem("update");
660                     this.showItem("markRead");
661                     this.hideItem("rename");
662                     this.showItem("delete");
663                     this.hideItem("newSubscription");
664                     this.hideItem("newFolder");
665                     this.hideItem("updateAll");
666                     this.showItem("copyFeedURL");
667                     break;
668             }
669         }
670     });
672     const RssArticleContextMenu = new Class({
673         Extends: ContextMenu
674     });
676     const RssDownloaderRuleContextMenu = new Class({
677         Extends: ContextMenu,
678         adjustMenuPosition: function(e) {
679             this.updateMenuItems();
681             // draw the menu off-screen to know the menu dimensions
682             this.menu.style.left = "-999em";
683             this.menu.style.top = "-999em";
684             // position the menu
685             let xPosMenu = e.pageX + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
686             let yPosMenu = e.pageY + this.options.offsets.y - $("rssdownloaderpage").offsetTop;
687             if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
688                 xPosMenu -= this.menu.offsetWidth;
689             if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
690                 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
691             xPosMenu = Math.max(xPosMenu, 0);
692             yPosMenu = Math.max(yPosMenu, 0);
694             this.menu.style.left = `${xPosMenu}px`;
695             this.menu.style.top = `${yPosMenu}px`;
696             this.menu.style.position = "absolute";
697             this.menu.style.zIndex = "2000";
698         },
699         updateMenuItems: function() {
700             const selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
701             this.showItem("addRule");
702             switch (selectedRows.length) {
703                 case 0:
704                     // menu when nothing selected
705                     this.hideItem("deleteRule");
706                     this.hideItem("renameRule");
707                     this.hideItem("clearDownloadedEpisodes");
708                     break;
709                 case 1:
710                     // menu when single item selected
711                     this.showItem("deleteRule");
712                     this.showItem("renameRule");
713                     this.showItem("clearDownloadedEpisodes");
714                     break;
715                 default:
716                     // menu when multiple items selected
717                     this.showItem("deleteRule");
718                     this.hideItem("renameRule");
719                     this.showItem("clearDownloadedEpisodes");
720                     break;
721             }
722         }
723     });
725     return exports();
726 })();
727 Object.freeze(window.qBittorrent.ContextMenu);