WebUI: prefer arrow functions whenever applicable
[qBittorrent.git] / src / webui / www / private / scripts / contextmenu.js
blob758783411eb964b61d8d5f2839075ff822a48bcf
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             StatusesFilterContextMenu: StatusesFilterContextMenu,
38             CategoriesFilterContextMenu: CategoriesFilterContextMenu,
39             TagsFilterContextMenu: TagsFilterContextMenu,
40             TrackersFilterContextMenu: TrackersFilterContextMenu,
41             SearchPluginsTableContextMenu: SearchPluginsTableContextMenu,
42             RssFeedContextMenu: RssFeedContextMenu,
43             RssArticleContextMenu: RssArticleContextMenu,
44             RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu
45         };
46     };
48     let lastShownContextMenu = null;
49     class ContextMenu {
50         constructor(options) {
51             this.options = {
52                 actions: {},
53                 menu: "menu_id",
54                 stopEvent: true,
55                 targets: "body",
56                 offsets: {
57                     x: 0,
58                     y: 0
59                 },
60                 onShow: () => {},
61                 onHide: () => {},
62                 onClick: () => {},
63                 fadeSpeed: 200,
64                 touchTimer: 600,
65                 ...options
66             };
68             // option diffs menu
69             this.menu = $(this.options.menu);
70             this.targets = $$(this.options.targets);
72             // fx
73             this.fx = new Fx.Tween(this.menu, {
74                 property: "opacity",
75                 duration: this.options.fadeSpeed,
76                 onComplete: () => {
77                     this.menu.style.visibility = (getComputedStyle(this.menu).opacity > 0) ? "visible" : "hidden";
78                 }
79             });
81             // hide and begin the listener
82             this.hide().startListener();
84             // hide the menu
85             this.menu.style.position = "absolute";
86             this.menu.style.top = "-900000px";
87             this.menu.style.display = "block";
88         }
90         adjustMenuPosition(e) {
91             this.updateMenuItems();
93             const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75;
95             if (this.menu.hasClass("scrollableMenu"))
96                 this.menu.style.maxHeight = `${scrollableMenuMaxHeight}px`;
98             // draw the menu off-screen to know the menu dimensions
99             this.menu.style.left = "-999em";
100             this.menu.style.top = "-999em";
102             // position the menu
103             let xPosMenu = e.pageX + this.options.offsets.x;
104             let yPosMenu = e.pageY + this.options.offsets.y;
105             if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
106                 xPosMenu -= this.menu.offsetWidth;
107             if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
108                 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
109             if (xPosMenu < 0)
110                 xPosMenu = 0;
111             if (yPosMenu < 0)
112                 yPosMenu = 0;
113             this.menu.style.left = `${xPosMenu}px`;
114             this.menu.style.top = `${yPosMenu}px`;
115             this.menu.style.position = "absolute";
116             this.menu.style.zIndex = "2000";
118             // position the sub-menu
119             const uls = this.menu.getElementsByTagName("ul");
120             for (let i = 0; i < uls.length; ++i) {
121                 const ul = uls[i];
122                 if (ul.hasClass("scrollableMenu"))
123                     ul.style.maxHeight = `${scrollableMenuMaxHeight}px`;
124                 const rectParent = ul.parentNode.getBoundingClientRect();
125                 const xPosOrigin = rectParent.left;
126                 const yPosOrigin = rectParent.bottom;
127                 let xPos = xPosOrigin + rectParent.width - 1;
128                 let yPos = yPosOrigin - rectParent.height - 1;
129                 if ((xPos + ul.offsetWidth) > document.documentElement.clientWidth)
130                     xPos -= (ul.offsetWidth + rectParent.width - 2);
131                 if ((yPos + ul.offsetHeight) > document.documentElement.clientHeight)
132                     yPos = document.documentElement.clientHeight - ul.offsetHeight;
133                 if (xPos < 0)
134                     xPos = 0;
135                 if (yPos < 0)
136                     yPos = 0;
137                 ul.style.marginLeft = `${xPos - xPosOrigin}px`;
138                 ul.style.marginTop = `${yPos - yPosOrigin}px`;
139             }
140         }
142         setupEventListeners(elem) {
143             elem.addEventListener("contextmenu", (e) => {
144                 this.triggerMenu(e, elem);
145             });
146             elem.addEventListener("click", (e) => {
147                 this.hide();
148             });
150             elem.addEventListener("touchstart", (e) => {
151                 this.hide();
152                 this.touchStartAt = performance.now();
153                 this.touchStartEvent = e;
154             }, { passive: true });
155             elem.addEventListener("touchend", (e) => {
156                 const now = performance.now();
157                 const touchStartAt = this.touchStartAt;
158                 const touchStartEvent = this.touchStartEvent;
160                 this.touchStartAt = null;
161                 this.touchStartEvent = null;
163                 const isTargetUnchanged = (Math.abs(e.event.pageX - touchStartEvent.event.pageX) <= 10) && (Math.abs(e.event.pageY - touchStartEvent.event.pageY) <= 10);
164                 if (((now - touchStartAt) >= this.options.touchTimer) && isTargetUnchanged)
165                     this.triggerMenu(touchStartEvent, elem);
166             }, { passive: true });
167         }
169         addTarget(t) {
170             // prevent long press from selecting this text
171             t.style.userSelect = "none";
173             this.targets[this.targets.length] = t;
174             this.setupEventListeners(t);
175         }
177         searchAndAddTargets() {
178             document.querySelectorAll(this.options.targets).forEach((target) => { this.addTarget(target); });
179         }
181         triggerMenu(e, el) {
182             if (this.options.disabled)
183                 return;
185             // prevent default, if told to
186             if (this.options.stopEvent) {
187                 e.preventDefault();
188                 e.stopPropagation();
189             }
190             // record this as the trigger
191             this.options.element = $(el);
192             this.adjustMenuPosition(e);
193             // show the menu
194             this.show();
195         }
197         // get things started
198         startListener() {
199             /* all elements */
200             this.targets.each((el) => {
201                 this.setupEventListeners(el);
202             }, this);
204             /* menu items */
205             this.menu.addEventListener("click", (e) => {
206                 const menuItem = e.target.closest("li");
207                 if (!menuItem)
208                     return;
210                 e.preventDefault();
211                 if (!menuItem.classList.contains("disabled")) {
212                     const anchor = menuItem.firstElementChild;
213                     this.execute(anchor.href.split("#")[1], this.options.element);
214                     this.options.onClick.call(this, anchor, e);
215                 }
216                 else {
217                     e.stopPropagation();
218                 }
219             });
221             // hide on body click
222             $(document.body).addEventListener("click", () => {
223                 this.hide();
224             });
225         }
227         updateMenuItems() {}
229         // show menu
230         show(trigger) {
231             if (lastShownContextMenu && (lastShownContextMenu !== this))
232                 lastShownContextMenu.hide();
233             this.fx.start(1);
234             this.options.onShow.call(this);
235             lastShownContextMenu = this;
236             return this;
237         }
239         // hide the menu
240         hide(trigger) {
241             if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
242                 this.fx.start(0);
243                 this.options.onHide.call(this);
244             }
245             return this;
246         }
248         setItemChecked(item, checked) {
249             this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity =
250                 checked ? "1" : "0";
251             return this;
252         }
254         getItemChecked(item) {
255             return this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity !== "0";
256         }
258         // hide an item
259         hideItem(item) {
260             this.menu.getElement("a[href$=" + item + "]").parentNode.addClass("invisible");
261             return this;
262         }
264         // show an item
265         showItem(item) {
266             this.menu.getElement("a[href$=" + item + "]").parentNode.removeClass("invisible");
267             return this;
268         }
270         // enable/disable an item
271         setEnabled(item, enabled) {
272             this.menu.querySelector(`:scope a[href$="${item}"]`).parentElement.classList.toggle("disabled", !enabled);
273             return this;
274         }
276         // disable the entire menu
277         disable() {
278             this.options.disabled = true;
279             return this;
280         }
282         // enable the entire menu
283         enable() {
284             this.options.disabled = false;
285             return this;
286         }
288         // execute an action
289         execute(action, element) {
290             if (this.options.actions[action])
291                 this.options.actions[action](element, this, action);
292             return this;
293         }
294     };
296     class FilterListContextMenu extends ContextMenu {
297         constructor(options) {
298             super(options);
299             this.torrentObserver = new MutationObserver((records, observer) => {
300                 this.updateTorrentActions();
301             });
302         }
304         startTorrentObserver() {
305             this.torrentObserver.observe(torrentsTable.tableBody, { childList: true });
306         }
308         stopTorrentObserver() {
309             this.torrentObserver.disconnect();
310         }
312         updateTorrentActions() {
313             const torrentsVisible = torrentsTable.tableBody.children.length > 0;
314             this.setEnabled("startTorrents", torrentsVisible)
315                 .setEnabled("stopTorrents", torrentsVisible)
316                 .setEnabled("deleteTorrents", torrentsVisible);
317         }
318     };
320     class TorrentsTableContextMenu extends ContextMenu {
321         updateMenuItems() {
322             let all_are_seq_dl = true;
323             let there_are_seq_dl = false;
324             let all_are_f_l_piece_prio = true;
325             let there_are_f_l_piece_prio = false;
326             let all_are_downloaded = true;
327             let all_are_stopped = true;
328             let there_are_stopped = false;
329             let all_are_force_start = true;
330             let there_are_force_start = false;
331             let all_are_super_seeding = true;
332             let all_are_auto_tmm = true;
333             let there_are_auto_tmm = false;
334             let thereAreV1Hashes = false;
335             let thereAreV2Hashes = false;
336             const tagCount = new Map();
337             const categoryCount = new Map();
339             const selectedRows = torrentsTable.selectedRowsIds();
340             selectedRows.forEach((item, index) => {
341                 const data = torrentsTable.getRow(item).full_data;
343                 if (data["seq_dl"] !== true)
344                     all_are_seq_dl = false;
345                 else
346                     there_are_seq_dl = true;
348                 if (data["f_l_piece_prio"] !== true)
349                     all_are_f_l_piece_prio = false;
350                 else
351                     there_are_f_l_piece_prio = true;
353                 if (data["progress"] !== 1.0) // not downloaded
354                     all_are_downloaded = false;
355                 else if (data["super_seeding"] !== true)
356                     all_are_super_seeding = false;
358                 if ((data["state"] !== "stoppedUP") && (data["state"] !== "stoppedDL"))
359                     all_are_stopped = false;
360                 else
361                     there_are_stopped = true;
363                 if (data["force_start"] !== true)
364                     all_are_force_start = false;
365                 else
366                     there_are_force_start = true;
368                 if (data["auto_tmm"] === true)
369                     there_are_auto_tmm = true;
370                 else
371                     all_are_auto_tmm = false;
373                 if (data["infohash_v1"] !== "")
374                     thereAreV1Hashes = true;
376                 if (data["infohash_v2"] !== "")
377                     thereAreV2Hashes = true;
379                 const torrentTags = data["tags"].split(", ");
380                 for (const tag of torrentTags) {
381                     const count = tagCount.get(tag);
382                     tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
383                 }
385                 const torrentCategory = data["category"];
386                 const count = categoryCount.get(torrentCategory);
387                 categoryCount.set(torrentCategory, ((count !== undefined) ? (count + 1) : 1));
388             });
390             // hide renameFiles when more than 1 torrent is selected
391             if (selectedRows.length === 1) {
392                 const data = torrentsTable.getRow(selectedRows[0]).full_data;
393                 const metadata_downloaded = !((data["state"] === "metaDL") || (data["state"] === "forcedMetaDL") || (data["total_size"] === -1));
395                 // hide renameFiles when metadata hasn't been downloaded yet
396                 metadata_downloaded
397                     ? this.showItem("renameFiles")
398                     : this.hideItem("renameFiles");
399             }
400             else {
401                 this.hideItem("renameFiles");
402             }
404             if (all_are_downloaded) {
405                 this.hideItem("downloadLimit");
406                 this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
407                 this.hideItem("sequentialDownload");
408                 this.hideItem("firstLastPiecePrio");
409                 this.showItem("superSeeding");
410                 this.setItemChecked("superSeeding", all_are_super_seeding);
411             }
412             else {
413                 const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
414                 const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
416                 if (!show_seq_dl && show_f_l_piece_prio)
417                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
418                 else
419                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
421                 if (show_seq_dl)
422                     this.showItem("sequentialDownload");
423                 else
424                     this.hideItem("sequentialDownload");
426                 if (show_f_l_piece_prio)
427                     this.showItem("firstLastPiecePrio");
428                 else
429                     this.hideItem("firstLastPiecePrio");
431                 this.setItemChecked("sequentialDownload", all_are_seq_dl);
432                 this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
434                 this.showItem("downloadLimit");
435                 this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
436                 this.hideItem("superSeeding");
437             }
439             this.showItem("start");
440             this.showItem("stop");
441             this.showItem("forceStart");
442             if (all_are_stopped)
443                 this.hideItem("stop");
444             else if (all_are_force_start)
445                 this.hideItem("forceStart");
446             else if (!there_are_stopped && !there_are_force_start)
447                 this.hideItem("start");
449             if (!all_are_auto_tmm && there_are_auto_tmm) {
450                 this.hideItem("autoTorrentManagement");
451             }
452             else {
453                 this.showItem("autoTorrentManagement");
454                 this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
455             }
457             this.setEnabled("copyInfohash1", thereAreV1Hashes);
458             this.setEnabled("copyInfohash2", thereAreV2Hashes);
460             const contextTagList = $("contextTagList");
461             tagList.forEach((tag, tagHash) => {
462                 const checkbox = contextTagList.getElement(`a[href="#Tag/${tag.name}"] input[type="checkbox"]`);
463                 const count = tagCount.get(tag.name);
464                 const hasCount = (count !== undefined);
465                 const isLesser = (count < selectedRows.length);
466                 checkbox.indeterminate = (hasCount ? isLesser : false);
467                 checkbox.checked = (hasCount ? !isLesser : false);
468             });
470             const contextCategoryList = document.getElementById("contextCategoryList");
471             category_list.forEach((category, categoryHash) => {
472                 const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category.name}"] img`);
473                 const count = categoryCount.get(category.name);
474                 const isEqual = ((count !== undefined) && (count === selectedRows.length));
475                 categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual);
476             });
477         }
479         updateCategoriesSubMenu(categoryList) {
480             const contextCategoryList = $("contextCategoryList");
481             contextCategoryList.getChildren().each(c => c.destroy());
483             const createMenuItem = (text, imgURL, clickFn) => {
484                 const anchor = document.createElement("a");
485                 anchor.textContent = text;
486                 anchor.addEventListener("click", () => { clickFn(); });
488                 const img = document.createElement("img");
489                 img.src = imgURL;
490                 img.alt = text;
491                 anchor.prepend(img);
493                 const item = document.createElement("li");
494                 item.appendChild(anchor);
496                 return item;
497             };
498             contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN));
499             contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(0); }));
501             const sortedCategories = [];
502             categoryList.forEach((category, hash) => sortedCategories.push({
503                 categoryName: category.name,
504                 categoryHash: hash
505             }));
506             sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
507                 left.categoryName, right.categoryName));
509             let first = true;
510             for (const { categoryName, categoryHash } of sortedCategories) {
511                 const anchor = document.createElement("a");
512                 anchor.href = `#Category/${categoryName}`;
513                 anchor.textContent = categoryName;
514                 anchor.addEventListener("click", (event) => {
515                     event.preventDefault();
516                     torrentSetCategoryFN(categoryHash);
517                 });
519                 const img = document.createElement("img");
520                 img.src = "images/view-categories.svg";
521                 anchor.prepend(img);
523                 const setCategoryItem = document.createElement("li");
524                 setCategoryItem.appendChild(anchor);
525                 if (first) {
526                     setCategoryItem.addClass("separator");
527                     first = false;
528                 }
530                 contextCategoryList.appendChild(setCategoryItem);
531             }
532         }
534         updateTagsSubMenu(tagList) {
535             const contextTagList = $("contextTagList");
536             while (contextTagList.firstChild !== null)
537                 contextTagList.removeChild(contextTagList.firstChild);
539             const createMenuItem = (text, imgURL, clickFn) => {
540                 const anchor = document.createElement("a");
541                 anchor.textContent = text;
542                 anchor.addEventListener("click", () => { clickFn(); });
544                 const img = document.createElement("img");
545                 img.src = imgURL;
546                 img.alt = text;
547                 anchor.prepend(img);
549                 const item = document.createElement("li");
550                 item.appendChild(anchor);
552                 return item;
553             };
554             contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN));
555             contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN));
557             const sortedTags = [];
558             tagList.forEach((tag, hash) => sortedTags.push({
559                 tagName: tag.name,
560                 tagHash: hash
561             }));
562             sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
564             for (let i = 0; i < sortedTags.length; ++i) {
565                 const { tagName, tagHash } = sortedTags[i];
567                 const input = document.createElement("input");
568                 input.type = "checkbox";
569                 input.addEventListener("click", (event) => {
570                     input.checked = !input.checked;
571                 });
573                 const anchor = document.createElement("a");
574                 anchor.href = `#Tag/${tagName}`;
575                 anchor.textContent = tagName;
576                 anchor.addEventListener("click", (event) => {
577                     event.preventDefault();
578                     torrentSetTagsFN(tagHash, !input.checked);
579                 });
580                 anchor.prepend(input);
582                 const setTagItem = document.createElement("li");
583                 setTagItem.appendChild(anchor);
584                 if (i === 0)
585                     setTagItem.addClass("separator");
587                 contextTagList.appendChild(setTagItem);
588             }
589         }
590     };
592     class StatusesFilterContextMenu extends FilterListContextMenu {
593         updateMenuItems() {
594             this.updateTorrentActions();
595         }
596     };
598     class CategoriesFilterContextMenu extends FilterListContextMenu {
599         updateMenuItems() {
600             const id = Number(this.options.element.id);
601             if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
602                 this.showItem("editCategory");
603                 this.showItem("deleteCategory");
604                 if (useSubcategories)
605                     this.showItem("createSubcategory");
606                 else
607                     this.hideItem("createSubcategory");
608             }
609             else {
610                 this.hideItem("editCategory");
611                 this.hideItem("deleteCategory");
612                 this.hideItem("createSubcategory");
613             }
615             this.updateTorrentActions();
616         }
617     };
619     class TagsFilterContextMenu extends FilterListContextMenu {
620         updateMenuItems() {
621             const id = Number(this.options.element.id);
622             if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
623                 this.showItem("deleteTag");
624             else
625                 this.hideItem("deleteTag");
627             this.updateTorrentActions();
628         }
629     };
631     class TrackersFilterContextMenu extends FilterListContextMenu {
632         updateMenuItems() {
633             const id = Number(this.options.element.id);
634             if ((id !== TRACKERS_ALL) && (id !== TRACKERS_TRACKERLESS))
635                 this.showItem("deleteTracker");
636             else
637                 this.hideItem("deleteTracker");
639             this.updateTorrentActions();
640         }
641     };
643     class SearchPluginsTableContextMenu extends ContextMenu {
644         updateMenuItems() {
645             const enabledColumnIndex = (text) => {
646                 const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
647                 for (let i = 0; i < columns.length; ++i) {
648                     if (columns[i].textContent === "Enabled")
649                         return i;
650                 }
651             };
653             this.showItem("Enabled");
654             this.setItemChecked("Enabled", (this.options.element.getChildren("td")[enabledColumnIndex()].textContent === "Yes"));
656             this.showItem("Uninstall");
657         }
658     };
660     class RssFeedContextMenu extends ContextMenu {
661         updateMenuItems() {
662             const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
663             this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
664             switch (selectedRows.length) {
665                 case 0:
666                     // remove separator on top of newSubscription entry to avoid double line
667                     this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
668                     // menu when nothing selected
669                     this.hideItem("update");
670                     this.hideItem("markRead");
671                     this.hideItem("rename");
672                     this.hideItem("edit");
673                     this.hideItem("delete");
674                     this.showItem("newSubscription");
675                     this.showItem("newFolder");
676                     this.showItem("updateAll");
677                     this.hideItem("copyFeedURL");
678                     break;
679                 case 1:
680                     if (selectedRows[0] === "0") {
681                         // menu when "unread" feed selected
682                         this.showItem("update");
683                         this.showItem("markRead");
684                         this.hideItem("rename");
685                         this.hideItem("edit");
686                         this.hideItem("delete");
687                         this.showItem("newSubscription");
688                         this.hideItem("newFolder");
689                         this.hideItem("updateAll");
690                         this.hideItem("copyFeedURL");
691                     }
692                     else if (window.qBittorrent.Rss.rssFeedTable.getRow(selectedRows[0]).full_data.dataUid === "") {
693                         // menu when single folder selected
694                         this.showItem("update");
695                         this.showItem("markRead");
696                         this.showItem("rename");
697                         this.hideItem("edit");
698                         this.showItem("delete");
699                         this.showItem("newSubscription");
700                         this.showItem("newFolder");
701                         this.hideItem("updateAll");
702                         this.hideItem("copyFeedURL");
703                     }
704                     else {
705                         // menu when single feed selected
706                         this.showItem("update");
707                         this.showItem("markRead");
708                         this.showItem("rename");
709                         this.showItem("edit");
710                         this.showItem("delete");
711                         this.showItem("newSubscription");
712                         this.hideItem("newFolder");
713                         this.hideItem("updateAll");
714                         this.showItem("copyFeedURL");
715                     }
716                     break;
717                 default:
718                     // menu when multiple items selected
719                     this.showItem("update");
720                     this.showItem("markRead");
721                     this.hideItem("rename");
722                     this.hideItem("edit");
723                     this.showItem("delete");
724                     this.hideItem("newSubscription");
725                     this.hideItem("newFolder");
726                     this.hideItem("updateAll");
727                     this.showItem("copyFeedURL");
728                     break;
729             }
730         }
731     };
733     class RssArticleContextMenu extends ContextMenu {};
735     class RssDownloaderRuleContextMenu extends ContextMenu {
736         adjustMenuPosition(e) {
737             this.updateMenuItems();
739             // draw the menu off-screen to know the menu dimensions
740             this.menu.style.left = "-999em";
741             this.menu.style.top = "-999em";
742             // position the menu
743             let xPosMenu = e.pageX + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
744             let yPosMenu = e.pageY + this.options.offsets.y - $("rssdownloaderpage").offsetTop;
745             if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
746                 xPosMenu -= this.menu.offsetWidth;
747             if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
748                 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
749             xPosMenu = Math.max(xPosMenu, 0);
750             yPosMenu = Math.max(yPosMenu, 0);
752             this.menu.style.left = `${xPosMenu}px`;
753             this.menu.style.top = `${yPosMenu}px`;
754             this.menu.style.position = "absolute";
755             this.menu.style.zIndex = "2000";
756         }
757         updateMenuItems() {
758             const selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
759             this.showItem("addRule");
760             switch (selectedRows.length) {
761                 case 0:
762                     // menu when nothing selected
763                     this.hideItem("deleteRule");
764                     this.hideItem("renameRule");
765                     this.hideItem("clearDownloadedEpisodes");
766                     break;
767                 case 1:
768                     // menu when single item selected
769                     this.showItem("deleteRule");
770                     this.showItem("renameRule");
771                     this.showItem("clearDownloadedEpisodes");
772                     break;
773                 default:
774                     // menu when multiple items selected
775                     this.showItem("deleteRule");
776                     this.hideItem("renameRule");
777                     this.showItem("clearDownloadedEpisodes");
778                     break;
779             }
780         }
781     };
783     return exports();
784 })();
785 Object.freeze(window.qBittorrent.ContextMenu);