WebUI: prevent passing wrong parameter
[qBittorrent.git] / src / webui / www / private / scripts / contextmenu.js
blobad1b92d021e0958b2110e012360fd753d670a772
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             });
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             });
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         triggerMenu: function(e, el) {
183             if (this.options.disabled)
184                 return;
186             // prevent default, if told to
187             if (this.options.stopEvent) {
188                 e.preventDefault();
189                 e.stopPropagation();
190             }
191             // record this as the trigger
192             this.options.element = $(el);
193             this.adjustMenuPosition(e);
194             // show the menu
195             this.show();
196         },
198         // get things started
199         startListener: function() {
200             /* all elements */
201             this.targets.each((el) => {
202                 this.setupEventListeners(el);
203             }, this);
205             /* menu items */
206             this.menu.getElements("a").each(function(item) {
207                 item.addEventListener("click", (e) => {
208                     e.preventDefault();
209                     if (!item.hasClass("disabled")) {
210                         this.execute(item.href.split("#")[1], $(this.options.element));
211                         this.fireEvent("click", [item, e]);
212                     }
213                 });
214             }, this);
216             // hide on body click
217             $(document.body).addEventListener("click", () => {
218                 this.hide();
219             });
220         },
222         updateMenuItems: function() {},
224         // show menu
225         show: function(trigger) {
226             if (lastShownContextMenu && (lastShownContextMenu !== this))
227                 lastShownContextMenu.hide();
228             this.fx.start(1);
229             this.fireEvent("show");
230             lastShownContextMenu = this;
231             return this;
232         },
234         // hide the menu
235         hide: function(trigger) {
236             if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
237                 this.fx.start(0);
238                 // this.menu.fade('out');
239                 this.fireEvent("hide");
240             }
241             return this;
242         },
244         setItemChecked: function(item, checked) {
245             this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity =
246                 checked ? "1" : "0";
247             return this;
248         },
250         getItemChecked: function(item) {
251             return this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity !== "0";
252         },
254         // hide an item
255         hideItem: function(item) {
256             this.menu.getElement("a[href$=" + item + "]").parentNode.addClass("invisible");
257             return this;
258         },
260         // show an item
261         showItem: function(item) {
262             this.menu.getElement("a[href$=" + item + "]").parentNode.removeClass("invisible");
263             return this;
264         },
266         // disable the entire menu
267         disable: function() {
268             this.options.disabled = true;
269             return this;
270         },
272         // enable the entire menu
273         enable: function() {
274             this.options.disabled = false;
275             return this;
276         },
278         // execute an action
279         execute: function(action, element) {
280             if (this.options.actions[action])
281                 this.options.actions[action](element, this, action);
282             return this;
283         }
284     });
286     const TorrentsTableContextMenu = new Class({
287         Extends: ContextMenu,
289         updateMenuItems: function() {
290             let all_are_seq_dl = true;
291             let there_are_seq_dl = false;
292             let all_are_f_l_piece_prio = true;
293             let there_are_f_l_piece_prio = false;
294             let all_are_downloaded = true;
295             let all_are_stopped = true;
296             let there_are_stopped = false;
297             let all_are_force_start = true;
298             let there_are_force_start = false;
299             let all_are_super_seeding = true;
300             let all_are_auto_tmm = true;
301             let there_are_auto_tmm = false;
302             const tagCount = new Map();
303             const categoryCount = new Map();
305             const selectedRows = torrentsTable.selectedRowsIds();
306             selectedRows.forEach((item, index) => {
307                 const data = torrentsTable.rows.get(item).full_data;
309                 if (data["seq_dl"] !== true)
310                     all_are_seq_dl = false;
311                 else
312                     there_are_seq_dl = true;
314                 if (data["f_l_piece_prio"] !== true)
315                     all_are_f_l_piece_prio = false;
316                 else
317                     there_are_f_l_piece_prio = true;
319                 if (data["progress"] !== 1.0) // not downloaded
320                     all_are_downloaded = false;
321                 else if (data["super_seeding"] !== true)
322                     all_are_super_seeding = false;
324                 if ((data["state"] !== "stoppedUP") && (data["state"] !== "stoppedDL"))
325                     all_are_stopped = false;
326                 else
327                     there_are_stopped = true;
329                 if (data["force_start"] !== true)
330                     all_are_force_start = false;
331                 else
332                     there_are_force_start = true;
334                 if (data["auto_tmm"] === true)
335                     there_are_auto_tmm = true;
336                 else
337                     all_are_auto_tmm = false;
339                 const torrentTags = data["tags"].split(", ");
340                 for (const tag of torrentTags) {
341                     const count = tagCount.get(tag);
342                     tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
343                 }
345                 const torrentCategory = data["category"];
346                 const count = categoryCount.get(torrentCategory);
347                 categoryCount.set(torrentCategory, ((count !== undefined) ? (count + 1) : 1));
348             });
350             // hide renameFiles when more than 1 torrent is selected
351             if (selectedRows.length === 1) {
352                 const data = torrentsTable.rows.get(selectedRows[0]).full_data;
353                 const metadata_downloaded = !((data["state"] === "metaDL") || (data["state"] === "forcedMetaDL") || (data["total_size"] === -1));
355                 // hide renameFiles when metadata hasn't been downloaded yet
356                 metadata_downloaded
357                     ? this.showItem("renameFiles")
358                     : this.hideItem("renameFiles");
359             }
360             else {
361                 this.hideItem("renameFiles");
362             }
364             if (all_are_downloaded) {
365                 this.hideItem("downloadLimit");
366                 this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
367                 this.hideItem("sequentialDownload");
368                 this.hideItem("firstLastPiecePrio");
369                 this.showItem("superSeeding");
370                 this.setItemChecked("superSeeding", all_are_super_seeding);
371             }
372             else {
373                 const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
374                 const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
376                 if (!show_seq_dl && show_f_l_piece_prio)
377                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
378                 else
379                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
381                 if (show_seq_dl)
382                     this.showItem("sequentialDownload");
383                 else
384                     this.hideItem("sequentialDownload");
386                 if (show_f_l_piece_prio)
387                     this.showItem("firstLastPiecePrio");
388                 else
389                     this.hideItem("firstLastPiecePrio");
391                 this.setItemChecked("sequentialDownload", all_are_seq_dl);
392                 this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
394                 this.showItem("downloadLimit");
395                 this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
396                 this.hideItem("superSeeding");
397             }
399             this.showItem("start");
400             this.showItem("stop");
401             this.showItem("forceStart");
402             if (all_are_stopped)
403                 this.hideItem("stop");
404             else if (all_are_force_start)
405                 this.hideItem("forceStart");
406             else if (!there_are_stopped && !there_are_force_start)
407                 this.hideItem("start");
409             if (!all_are_auto_tmm && there_are_auto_tmm) {
410                 this.hideItem("autoTorrentManagement");
411             }
412             else {
413                 this.showItem("autoTorrentManagement");
414                 this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
415             }
417             const contextTagList = $("contextTagList");
418             tagList.forEach((tag, tagHash) => {
419                 const checkbox = contextTagList.getElement(`a[href="#Tag/${tag.name}"] input[type="checkbox"]`);
420                 const count = tagCount.get(tag.name);
421                 const hasCount = (count !== undefined);
422                 const isLesser = (count < selectedRows.length);
423                 checkbox.indeterminate = (hasCount ? isLesser : false);
424                 checkbox.checked = (hasCount ? !isLesser : false);
425             });
427             const contextCategoryList = document.getElementById("contextCategoryList");
428             category_list.forEach((category, categoryHash) => {
429                 const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category.name}"] img`);
430                 const count = categoryCount.get(category.name);
431                 const isEqual = ((count !== undefined) && (count === selectedRows.length));
432                 categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual);
433             });
434         },
436         updateCategoriesSubMenu: function(categoryList) {
437             const contextCategoryList = $("contextCategoryList");
438             contextCategoryList.getChildren().each(c => c.destroy());
440             const createMenuItem = (text, imgURL, clickFn) => {
441                 const anchor = document.createElement("a");
442                 anchor.textContent = text;
443                 anchor.addEventListener("click", () => { clickFn(); });
445                 const img = document.createElement("img");
446                 img.src = imgURL;
447                 img.alt = text;
448                 anchor.prepend(img);
450                 const item = document.createElement("li");
451                 item.appendChild(anchor);
453                 return item;
454             };
455             contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN));
456             contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(0); }));
458             const sortedCategories = [];
459             categoryList.forEach((category, hash) => sortedCategories.push({
460                 categoryName: category.name,
461                 categoryHash: hash
462             }));
463             sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
464                 left.categoryName, right.categoryName));
466             let first = true;
467             for (const { categoryName, categoryHash } of sortedCategories) {
468                 const anchor = document.createElement("a");
469                 anchor.href = `#Category/${categoryName}`;
470                 anchor.textContent = categoryName;
471                 anchor.addEventListener("click", (event) => {
472                     torrentSetCategoryFN(categoryHash);
473                 });
475                 const img = document.createElement("img");
476                 img.src = "images/view-categories.svg";
477                 anchor.prepend(img);
479                 const setCategoryItem = document.createElement("li");
480                 setCategoryItem.appendChild(anchor);
481                 if (first) {
482                     setCategoryItem.addClass("separator");
483                     first = false;
484                 }
486                 contextCategoryList.appendChild(setCategoryItem);
487             }
488         },
490         updateTagsSubMenu: function(tagList) {
491             const contextTagList = $("contextTagList");
492             while (contextTagList.firstChild !== null)
493                 contextTagList.removeChild(contextTagList.firstChild);
495             const createMenuItem = (text, imgURL, clickFn) => {
496                 const anchor = document.createElement("a");
497                 anchor.textContent = text;
498                 anchor.addEventListener("click", () => { clickFn(); });
500                 const img = document.createElement("img");
501                 img.src = imgURL;
502                 img.alt = text;
503                 anchor.prepend(img);
505                 const item = document.createElement("li");
506                 item.appendChild(anchor);
508                 return item;
509             };
510             contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN));
511             contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN));
513             const sortedTags = [];
514             tagList.forEach((tag, hash) => sortedTags.push({
515                 tagName: tag.name,
516                 tagHash: hash
517             }));
518             sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
520             for (let i = 0; i < sortedTags.length; ++i) {
521                 const { tagName, tagHash } = sortedTags[i];
523                 const input = document.createElement("input");
524                 input.type = "checkbox";
525                 input.addEventListener("click", (event) => {
526                     input.checked = !input.checked;
527                 });
529                 const anchor = document.createElement("a");
530                 anchor.href = `#Tag/${tagName}`;
531                 anchor.textContent = tagName;
532                 anchor.addEventListener("click", (event) => {
533                     event.preventDefault();
534                     torrentSetTagsFN(tagHash, !input.checked);
535                 });
536                 anchor.prepend(input);
538                 const setTagItem = document.createElement("li");
539                 setTagItem.appendChild(anchor);
540                 if (i === 0)
541                     setTagItem.addClass("separator");
543                 contextTagList.appendChild(setTagItem);
544             }
545         }
546     });
548     const CategoriesFilterContextMenu = new Class({
549         Extends: ContextMenu,
550         updateMenuItems: function() {
551             const id = Number(this.options.element.id);
552             if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
553                 this.showItem("editCategory");
554                 this.showItem("deleteCategory");
555                 if (useSubcategories)
556                     this.showItem("createSubcategory");
557                 else
558                     this.hideItem("createSubcategory");
559             }
560             else {
561                 this.hideItem("editCategory");
562                 this.hideItem("deleteCategory");
563                 this.hideItem("createSubcategory");
564             }
565         }
566     });
568     const TagsFilterContextMenu = new Class({
569         Extends: ContextMenu,
570         updateMenuItems: function() {
571             const id = Number(this.options.element.id);
572             if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
573                 this.showItem("deleteTag");
574             else
575                 this.hideItem("deleteTag");
576         }
577     });
579     const SearchPluginsTableContextMenu = new Class({
580         Extends: ContextMenu,
582         updateMenuItems: function() {
583             const enabledColumnIndex = function(text) {
584                 const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
585                 for (let i = 0; i < columns.length; ++i) {
586                     if (columns[i].textContent === "Enabled")
587                         return i;
588                 }
589             };
591             this.showItem("Enabled");
592             this.setItemChecked("Enabled", (this.options.element.getChildren("td")[enabledColumnIndex()].textContent === "Yes"));
594             this.showItem("Uninstall");
595         }
596     });
598     const RssFeedContextMenu = new Class({
599         Extends: ContextMenu,
600         updateMenuItems: function() {
601             const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
602             this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
603             switch (selectedRows.length) {
604                 case 0:
605                     // remove separator on top of newSubscription entry to avoid double line
606                     this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
607                     // menu when nothing selected
608                     this.hideItem("update");
609                     this.hideItem("markRead");
610                     this.hideItem("rename");
611                     this.hideItem("delete");
612                     this.showItem("newSubscription");
613                     this.showItem("newFolder");
614                     this.showItem("updateAll");
615                     this.hideItem("copyFeedURL");
616                     break;
617                 case 1:
618                     if (selectedRows[0] === 0) {
619                         // menu when "unread" feed selected
620                         this.showItem("update");
621                         this.showItem("markRead");
622                         this.hideItem("rename");
623                         this.hideItem("delete");
624                         this.showItem("newSubscription");
625                         this.hideItem("newFolder");
626                         this.hideItem("updateAll");
627                         this.hideItem("copyFeedURL");
628                     }
629                     else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === "") {
630                         // menu when single folder selected
631                         this.showItem("update");
632                         this.showItem("markRead");
633                         this.showItem("rename");
634                         this.showItem("delete");
635                         this.showItem("newSubscription");
636                         this.showItem("newFolder");
637                         this.hideItem("updateAll");
638                         this.hideItem("copyFeedURL");
639                     }
640                     else {
641                         // menu when single feed selected
642                         this.showItem("update");
643                         this.showItem("markRead");
644                         this.showItem("rename");
645                         this.showItem("delete");
646                         this.showItem("newSubscription");
647                         this.hideItem("newFolder");
648                         this.hideItem("updateAll");
649                         this.showItem("copyFeedURL");
650                     }
651                     break;
652                 default:
653                     // menu when multiple items selected
654                     this.showItem("update");
655                     this.showItem("markRead");
656                     this.hideItem("rename");
657                     this.showItem("delete");
658                     this.hideItem("newSubscription");
659                     this.hideItem("newFolder");
660                     this.hideItem("updateAll");
661                     this.showItem("copyFeedURL");
662                     break;
663             }
664         }
665     });
667     const RssArticleContextMenu = new Class({
668         Extends: ContextMenu
669     });
671     const RssDownloaderRuleContextMenu = new Class({
672         Extends: ContextMenu,
673         adjustMenuPosition: function(e) {
674             this.updateMenuItems();
676             // draw the menu off-screen to know the menu dimensions
677             this.menu.style.left = "-999em";
678             this.menu.style.top = "-999em";
679             // position the menu
680             let xPosMenu = e.pageX + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
681             let yPosMenu = e.pageY + this.options.offsets.y - $("rssdownloaderpage").offsetTop;
682             if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
683                 xPosMenu -= this.menu.offsetWidth;
684             if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
685                 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
686             xPosMenu = Math.max(xPosMenu, 0);
687             yPosMenu = Math.max(yPosMenu, 0);
689             this.menu.style.left = `${xPosMenu}px`;
690             this.menu.style.top = `${yPosMenu}px`;
691             this.menu.style.position = "absolute";
692             this.menu.style.zIndex = "2000";
693         },
694         updateMenuItems: function() {
695             const selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
696             this.showItem("addRule");
697             switch (selectedRows.length) {
698                 case 0:
699                     // menu when nothing selected
700                     this.hideItem("deleteRule");
701                     this.hideItem("renameRule");
702                     this.hideItem("clearDownloadedEpisodes");
703                     break;
704                 case 1:
705                     // menu when single item selected
706                     this.showItem("deleteRule");
707                     this.showItem("renameRule");
708                     this.showItem("clearDownloadedEpisodes");
709                     break;
710                 default:
711                     // menu when multiple items selected
712                     this.showItem("deleteRule");
713                     this.hideItem("renameRule");
714                     this.showItem("clearDownloadedEpisodes");
715                     break;
716             }
717         }
718     });
720     return exports();
721 })();
722 Object.freeze(window.qBittorrent.ContextMenu);