WebUI: Show 'Rename...' context menu item only when one torrent is selected
[qBittorrent.git] / src / webui / www / private / scripts / contextmenu.js
blob876a3792a281adc0dc47546ac1f8229be9f87566
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                 this.showItem("rename");
396                 // hide renameFiles when metadata hasn't been downloaded yet
397                 metadata_downloaded
398                     ? this.showItem("renameFiles")
399                     : this.hideItem("renameFiles");
400             }
401             else {
402                 this.hideItem("renameFiles");
403                 this.hideItem("rename");
404             }
406             if (all_are_downloaded) {
407                 this.hideItem("downloadLimit");
408                 this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
409                 this.hideItem("sequentialDownload");
410                 this.hideItem("firstLastPiecePrio");
411                 this.showItem("superSeeding");
412                 this.setItemChecked("superSeeding", all_are_super_seeding);
413             }
414             else {
415                 const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
416                 const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
418                 if (!show_seq_dl && show_f_l_piece_prio)
419                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
420                 else
421                     this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
423                 if (show_seq_dl)
424                     this.showItem("sequentialDownload");
425                 else
426                     this.hideItem("sequentialDownload");
428                 if (show_f_l_piece_prio)
429                     this.showItem("firstLastPiecePrio");
430                 else
431                     this.hideItem("firstLastPiecePrio");
433                 this.setItemChecked("sequentialDownload", all_are_seq_dl);
434                 this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
436                 this.showItem("downloadLimit");
437                 this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
438                 this.hideItem("superSeeding");
439             }
441             this.showItem("start");
442             this.showItem("stop");
443             this.showItem("forceStart");
444             if (all_are_stopped)
445                 this.hideItem("stop");
446             else if (all_are_force_start)
447                 this.hideItem("forceStart");
448             else if (!there_are_stopped && !there_are_force_start)
449                 this.hideItem("start");
451             if (!all_are_auto_tmm && there_are_auto_tmm) {
452                 this.hideItem("autoTorrentManagement");
453             }
454             else {
455                 this.showItem("autoTorrentManagement");
456                 this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
457             }
459             this.setEnabled("copyInfohash1", thereAreV1Hashes);
460             this.setEnabled("copyInfohash2", thereAreV2Hashes);
462             const contextTagList = $("contextTagList");
463             tagList.forEach((tag, tagHash) => {
464                 const checkbox = contextTagList.getElement(`a[href="#Tag/${tag.name}"] input[type="checkbox"]`);
465                 const count = tagCount.get(tag.name);
466                 const hasCount = (count !== undefined);
467                 const isLesser = (count < selectedRows.length);
468                 checkbox.indeterminate = (hasCount ? isLesser : false);
469                 checkbox.checked = (hasCount ? !isLesser : false);
470             });
472             const contextCategoryList = document.getElementById("contextCategoryList");
473             category_list.forEach((category, categoryHash) => {
474                 const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category.name}"] img`);
475                 const count = categoryCount.get(category.name);
476                 const isEqual = ((count !== undefined) && (count === selectedRows.length));
477                 categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual);
478             });
479         }
481         updateCategoriesSubMenu(categoryList) {
482             const contextCategoryList = $("contextCategoryList");
483             contextCategoryList.getChildren().each(c => c.destroy());
485             const createMenuItem = (text, imgURL, clickFn) => {
486                 const anchor = document.createElement("a");
487                 anchor.textContent = text;
488                 anchor.addEventListener("click", () => { clickFn(); });
490                 const img = document.createElement("img");
491                 img.src = imgURL;
492                 img.alt = text;
493                 anchor.prepend(img);
495                 const item = document.createElement("li");
496                 item.appendChild(anchor);
498                 return item;
499             };
500             contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN));
501             contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(0); }));
503             const sortedCategories = [];
504             categoryList.forEach((category, hash) => sortedCategories.push({
505                 categoryName: category.name,
506                 categoryHash: hash
507             }));
508             sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
509                 left.categoryName, right.categoryName));
511             let first = true;
512             for (const { categoryName, categoryHash } of sortedCategories) {
513                 const anchor = document.createElement("a");
514                 anchor.href = `#Category/${categoryName}`;
515                 anchor.textContent = categoryName;
516                 anchor.addEventListener("click", (event) => {
517                     event.preventDefault();
518                     torrentSetCategoryFN(categoryHash);
519                 });
521                 const img = document.createElement("img");
522                 img.src = "images/view-categories.svg";
523                 anchor.prepend(img);
525                 const setCategoryItem = document.createElement("li");
526                 setCategoryItem.appendChild(anchor);
527                 if (first) {
528                     setCategoryItem.addClass("separator");
529                     first = false;
530                 }
532                 contextCategoryList.appendChild(setCategoryItem);
533             }
534         }
536         updateTagsSubMenu(tagList) {
537             const contextTagList = $("contextTagList");
538             while (contextTagList.firstChild !== null)
539                 contextTagList.removeChild(contextTagList.firstChild);
541             const createMenuItem = (text, imgURL, clickFn) => {
542                 const anchor = document.createElement("a");
543                 anchor.textContent = text;
544                 anchor.addEventListener("click", () => { clickFn(); });
546                 const img = document.createElement("img");
547                 img.src = imgURL;
548                 img.alt = text;
549                 anchor.prepend(img);
551                 const item = document.createElement("li");
552                 item.appendChild(anchor);
554                 return item;
555             };
556             contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN));
557             contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN));
559             const sortedTags = [];
560             tagList.forEach((tag, hash) => sortedTags.push({
561                 tagName: tag.name,
562                 tagHash: hash
563             }));
564             sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
566             for (let i = 0; i < sortedTags.length; ++i) {
567                 const { tagName, tagHash } = sortedTags[i];
569                 const input = document.createElement("input");
570                 input.type = "checkbox";
571                 input.addEventListener("click", (event) => {
572                     input.checked = !input.checked;
573                 });
575                 const anchor = document.createElement("a");
576                 anchor.href = `#Tag/${tagName}`;
577                 anchor.textContent = tagName;
578                 anchor.addEventListener("click", (event) => {
579                     event.preventDefault();
580                     torrentSetTagsFN(tagHash, !input.checked);
581                 });
582                 anchor.prepend(input);
584                 const setTagItem = document.createElement("li");
585                 setTagItem.appendChild(anchor);
586                 if (i === 0)
587                     setTagItem.addClass("separator");
589                 contextTagList.appendChild(setTagItem);
590             }
591         }
592     };
594     class StatusesFilterContextMenu extends FilterListContextMenu {
595         updateMenuItems() {
596             this.updateTorrentActions();
597         }
598     };
600     class CategoriesFilterContextMenu extends FilterListContextMenu {
601         updateMenuItems() {
602             const id = Number(this.options.element.id);
603             if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
604                 this.showItem("editCategory");
605                 this.showItem("deleteCategory");
606                 if (useSubcategories)
607                     this.showItem("createSubcategory");
608                 else
609                     this.hideItem("createSubcategory");
610             }
611             else {
612                 this.hideItem("editCategory");
613                 this.hideItem("deleteCategory");
614                 this.hideItem("createSubcategory");
615             }
617             this.updateTorrentActions();
618         }
619     };
621     class TagsFilterContextMenu extends FilterListContextMenu {
622         updateMenuItems() {
623             const id = Number(this.options.element.id);
624             if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
625                 this.showItem("deleteTag");
626             else
627                 this.hideItem("deleteTag");
629             this.updateTorrentActions();
630         }
631     };
633     class TrackersFilterContextMenu extends FilterListContextMenu {
634         updateMenuItems() {
635             const id = Number(this.options.element.id);
636             if ((id !== TRACKERS_ALL) && (id !== TRACKERS_TRACKERLESS))
637                 this.showItem("deleteTracker");
638             else
639                 this.hideItem("deleteTracker");
641             this.updateTorrentActions();
642         }
643     };
645     class SearchPluginsTableContextMenu extends ContextMenu {
646         updateMenuItems() {
647             const enabledColumnIndex = (text) => {
648                 const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
649                 for (let i = 0; i < columns.length; ++i) {
650                     if (columns[i].textContent === "Enabled")
651                         return i;
652                 }
653             };
655             this.showItem("Enabled");
656             this.setItemChecked("Enabled", (this.options.element.getChildren("td")[enabledColumnIndex()].textContent === "Yes"));
658             this.showItem("Uninstall");
659         }
660     };
662     class RssFeedContextMenu extends ContextMenu {
663         updateMenuItems() {
664             const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
665             this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
666             switch (selectedRows.length) {
667                 case 0:
668                     // remove separator on top of newSubscription entry to avoid double line
669                     this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
670                     // menu when nothing selected
671                     this.hideItem("update");
672                     this.hideItem("markRead");
673                     this.hideItem("rename");
674                     this.hideItem("edit");
675                     this.hideItem("delete");
676                     this.showItem("newSubscription");
677                     this.showItem("newFolder");
678                     this.showItem("updateAll");
679                     this.hideItem("copyFeedURL");
680                     break;
681                 case 1:
682                     if (selectedRows[0] === "0") {
683                         // menu when "unread" feed selected
684                         this.showItem("update");
685                         this.showItem("markRead");
686                         this.hideItem("rename");
687                         this.hideItem("edit");
688                         this.hideItem("delete");
689                         this.showItem("newSubscription");
690                         this.hideItem("newFolder");
691                         this.hideItem("updateAll");
692                         this.hideItem("copyFeedURL");
693                     }
694                     else if (window.qBittorrent.Rss.rssFeedTable.getRow(selectedRows[0]).full_data.dataUid === "") {
695                         // menu when single folder selected
696                         this.showItem("update");
697                         this.showItem("markRead");
698                         this.showItem("rename");
699                         this.hideItem("edit");
700                         this.showItem("delete");
701                         this.showItem("newSubscription");
702                         this.showItem("newFolder");
703                         this.hideItem("updateAll");
704                         this.hideItem("copyFeedURL");
705                     }
706                     else {
707                         // menu when single feed selected
708                         this.showItem("update");
709                         this.showItem("markRead");
710                         this.showItem("rename");
711                         this.showItem("edit");
712                         this.showItem("delete");
713                         this.showItem("newSubscription");
714                         this.hideItem("newFolder");
715                         this.hideItem("updateAll");
716                         this.showItem("copyFeedURL");
717                     }
718                     break;
719                 default:
720                     // menu when multiple items selected
721                     this.showItem("update");
722                     this.showItem("markRead");
723                     this.hideItem("rename");
724                     this.hideItem("edit");
725                     this.showItem("delete");
726                     this.hideItem("newSubscription");
727                     this.hideItem("newFolder");
728                     this.hideItem("updateAll");
729                     this.showItem("copyFeedURL");
730                     break;
731             }
732         }
733     };
735     class RssArticleContextMenu extends ContextMenu {};
737     class RssDownloaderRuleContextMenu extends ContextMenu {
738         adjustMenuPosition(e) {
739             this.updateMenuItems();
741             // draw the menu off-screen to know the menu dimensions
742             this.menu.style.left = "-999em";
743             this.menu.style.top = "-999em";
744             // position the menu
745             let xPosMenu = e.pageX + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
746             let yPosMenu = e.pageY + this.options.offsets.y - $("rssdownloaderpage").offsetTop;
747             if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
748                 xPosMenu -= this.menu.offsetWidth;
749             if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
750                 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
751             xPosMenu = Math.max(xPosMenu, 0);
752             yPosMenu = Math.max(yPosMenu, 0);
754             this.menu.style.left = `${xPosMenu}px`;
755             this.menu.style.top = `${yPosMenu}px`;
756             this.menu.style.position = "absolute";
757             this.menu.style.zIndex = "2000";
758         }
759         updateMenuItems() {
760             const selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
761             this.showItem("addRule");
762             switch (selectedRows.length) {
763                 case 0:
764                     // menu when nothing selected
765                     this.hideItem("deleteRule");
766                     this.hideItem("renameRule");
767                     this.hideItem("clearDownloadedEpisodes");
768                     break;
769                 case 1:
770                     // menu when single item selected
771                     this.showItem("deleteRule");
772                     this.showItem("renameRule");
773                     this.showItem("clearDownloadedEpisodes");
774                     break;
775                 default:
776                     // menu when multiple items selected
777                     this.showItem("deleteRule");
778                     this.hideItem("renameRule");
779                     this.showItem("clearDownloadedEpisodes");
780                     break;
781             }
782         }
783     };
785     return exports();
786 })();
787 Object.freeze(window.qBittorrent.ContextMenu);