Rename WebUI content files
[qBittorrent.git] / src / webui / www / private / scripts / contextmenu.js
blob310f520f87bdaa5997d0e4f806b2d6519b06e696
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2009 Christophe Dumez <chris@qbittorrent.org>
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.
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.
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.
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.
29 'use strict';
31 let lastShownContextMenu = null;
32 const ContextMenu = new Class({
33 //implements
34 Implements: [Options, Events],
36 //options
37 options: {
38 actions: {},
39 menu: 'menu_id',
40 stopEvent: true,
41 targets: 'body',
42 offsets: {
43 x: 0,
44 y: 0
46 onShow: $empty,
47 onHide: $empty,
48 onClick: $empty,
49 fadeSpeed: 200,
50 touchTimer: 600
53 //initialization
54 initialize: function(options) {
55 //set options
56 this.setOptions(options);
58 //option diffs menu
59 this.menu = $(this.options.menu);
60 this.targets = $$(this.options.targets);
62 //fx
63 this.fx = new Fx.Tween(this.menu, {
64 property: 'opacity',
65 duration: this.options.fadeSpeed,
66 onComplete: function() {
67 if (this.getStyle('opacity')) {
68 this.setStyle('visibility', 'visible');
70 else {
71 this.setStyle('visibility', 'hidden');
73 }.bind(this.menu)
74 });
76 //hide and begin the listener
77 this.hide().startListener();
79 //hide the menu
80 this.menu.setStyles({
81 'position': 'absolute',
82 'top': '-900000px',
83 'display': 'block'
84 });
87 adjustMenuPosition: function(e) {
88 this.updateMenuItems();
90 const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75;
92 if (this.menu.hasClass('scrollableMenu'))
93 this.menu.setStyle('max-height', scrollableMenuMaxHeight);
95 // draw the menu off-screen to know the menu dimensions
96 this.menu.setStyles({
97 left: '-999em',
98 top: '-999em'
99 });
101 // position the menu
102 let xPosMenu = e.page.x + this.options.offsets.x;
103 let yPosMenu = e.page.y + this.options.offsets.y;
104 if (xPosMenu + this.menu.offsetWidth > document.documentElement.clientWidth)
105 xPosMenu -= this.menu.offsetWidth;
106 if (yPosMenu + this.menu.offsetHeight > document.documentElement.clientHeight)
107 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
108 if (xPosMenu < 0)
109 xPosMenu = 0;
110 if (yPosMenu < 0)
111 yPosMenu = 0;
112 this.menu.setStyles({
113 left: xPosMenu,
114 top: yPosMenu,
115 position: 'absolute',
116 'z-index': '2000'
119 // position the sub-menu
120 const uls = this.menu.getElementsByTagName('ul');
121 for (let i = 0; i < uls.length; ++i) {
122 const ul = uls[i];
123 if (ul.hasClass('scrollableMenu'))
124 ul.setStyle('max-height', scrollableMenuMaxHeight);
125 const rectParent = ul.parentNode.getBoundingClientRect();
126 const xPosOrigin = rectParent.left;
127 const yPosOrigin = rectParent.bottom;
128 let xPos = xPosOrigin + rectParent.width - 1;
129 let yPos = yPosOrigin - rectParent.height - 1;
130 if (xPos + ul.offsetWidth > document.documentElement.clientWidth)
131 xPos -= (ul.offsetWidth + rectParent.width - 2);
132 if (yPos + ul.offsetHeight > document.documentElement.clientHeight)
133 yPos = document.documentElement.clientHeight - ul.offsetHeight;
134 if (xPos < 0)
135 xPos = 0;
136 if (yPos < 0)
137 yPos = 0;
138 ul.setStyles({
139 'margin-left': xPos - xPosOrigin,
140 'margin-top': yPos - yPosOrigin
145 setupEventListeners: function(elem) {
146 elem.addEvent('contextmenu', function(e) {
147 this.triggerMenu(e, elem);
148 }.bind(this));
149 elem.addEvent('click', function(e) {
150 this.hide();
151 }.bind(this));
153 elem.addEvent('touchstart', function(e) {
154 e.preventDefault();
155 clearTimeout(this.touchstartTimer);
156 this.hide();
158 const touchstartEvent = e;
159 this.touchstartTimer = setTimeout(function() {
160 this.triggerMenu(touchstartEvent, elem);
161 }.bind(this), this.options.touchTimer);
162 }.bind(this));
163 elem.addEvent('touchend', function(e) {
164 e.preventDefault();
165 clearTimeout(this.touchstartTimer);
166 }.bind(this));
169 addTarget: function(t) {
170 this.targets[this.targets.length] = t;
171 this.setupEventListeners(t);
174 triggerMenu: function(e, el) {
175 if (this.options.disabled)
176 return;
178 //prevent default, if told to
179 if (this.options.stopEvent) {
180 e.stop();
182 //record this as the trigger
183 this.options.element = $(el);
184 this.adjustMenuPosition(e);
185 //show the menu
186 this.show();
189 //get things started
190 startListener: function() {
191 /* all elements */
192 this.targets.each(function(el) {
193 this.setupEventListeners(el);
194 }.bind(this), this);
196 /* menu items */
197 this.menu.getElements('a').each(function(item) {
198 item.addEvent('click', function(e) {
199 e.preventDefault();
200 if (!item.hasClass('disabled')) {
201 this.execute(item.get('href').split('#')[1], $(this.options.element));
202 this.fireEvent('click', [item, e]);
204 }.bind(this));
205 }, this);
207 //hide on body click
208 $(document.body).addEvent('click', function() {
209 this.hide();
210 }.bind(this));
213 updateMenuItems: function() {},
215 //show menu
216 show: function(trigger) {
217 if (lastShownContextMenu && lastShownContextMenu != this)
218 lastShownContextMenu.hide();
219 this.fx.start(1);
220 this.fireEvent('show');
221 this.shown = true;
222 lastShownContextMenu = this;
223 return this;
226 //hide the menu
227 hide: function(trigger) {
228 if (this.shown) {
229 this.fx.start(0);
230 //this.menu.fade('out');
231 this.fireEvent('hide');
232 this.shown = false;
234 return this;
237 setItemChecked: function(item, checked) {
238 this.menu.getElement('a[href$=' + item + ']').firstChild.style.opacity =
239 checked ? '1' : '0';
240 return this;
243 getItemChecked: function(item) {
244 return '0' != this.menu.getElement('a[href$=' + item + ']').firstChild.style.opacity;
247 //hide an item
248 hideItem: function(item) {
249 this.menu.getElement('a[href$=' + item + ']').parentNode.addClass('invisible');
250 return this;
253 //show an item
254 showItem: function(item) {
255 this.menu.getElement('a[href$=' + item + ']').parentNode.removeClass('invisible');
256 return this;
259 //disable the entire menu
260 disable: function() {
261 this.options.disabled = true;
262 return this;
265 //enable the entire menu
266 enable: function() {
267 this.options.disabled = false;
268 return this;
271 //execute an action
272 execute: function(action, element) {
273 if (this.options.actions[action]) {
274 this.options.actions[action](element, this, action);
276 return this;
280 const TorrentsTableContextMenu = new Class({
281 Extends: ContextMenu,
283 updateMenuItems: function() {
284 let all_are_seq_dl = true;
285 let there_are_seq_dl = false;
286 let all_are_f_l_piece_prio = true;
287 let there_are_f_l_piece_prio = false;
288 let all_are_downloaded = true;
289 let all_are_paused = true;
290 let there_are_paused = false;
291 let all_are_force_start = true;
292 let there_are_force_start = false;
293 let all_are_super_seeding = true;
294 let all_are_auto_tmm = true;
295 let there_are_auto_tmm = false;
296 const tagsSelectionState = Object.clone(tagList);
298 const h = torrentsTable.selectedRowsIds();
299 h.each(function(item, index) {
300 const data = torrentsTable.rows.get(item).full_data;
302 if (data['seq_dl'] !== true)
303 all_are_seq_dl = false;
304 else
305 there_are_seq_dl = true;
307 if (data['f_l_piece_prio'] !== true)
308 all_are_f_l_piece_prio = false;
309 else
310 there_are_f_l_piece_prio = true;
312 if (data['progress'] != 1.0) // not downloaded
313 all_are_downloaded = false;
314 else if (data['super_seeding'] !== true)
315 all_are_super_seeding = false;
317 if (data['state'] != 'pausedUP' && data['state'] != 'pausedDL')
318 all_are_paused = false;
319 else
320 there_are_paused = true;
322 if (data['force_start'] !== true)
323 all_are_force_start = false;
324 else
325 there_are_force_start = true;
327 if (data['auto_tmm'] === true)
328 there_are_auto_tmm = true;
329 else
330 all_are_auto_tmm = false;
332 const torrentTags = data['tags'].split(', ');
333 for (const key in tagsSelectionState) {
334 const tag = tagsSelectionState[key];
335 const tagExists = torrentTags.contains(tag.name);
336 if ((tag.checked !== undefined) && (tag.checked != tagExists))
337 tag.indeterminate = true;
338 if (tag.checked === undefined)
339 tag.checked = tagExists;
340 else
341 tag.checked = tag.checked && tagExists;
345 let show_seq_dl = true;
347 if (!all_are_seq_dl && there_are_seq_dl)
348 show_seq_dl = false;
350 let show_f_l_piece_prio = true;
352 if (!all_are_f_l_piece_prio && there_are_f_l_piece_prio)
353 show_f_l_piece_prio = false;
355 if (all_are_downloaded) {
356 this.hideItem('downloadLimit');
357 this.menu.getElement('a[href$=uploadLimit]').parentNode.addClass('separator');
358 this.hideItem('sequentialDownload');
359 this.hideItem('firstLastPiecePrio');
360 this.showItem('superSeeding');
361 this.setItemChecked('superSeeding', all_are_super_seeding);
363 else {
364 if (!show_seq_dl && show_f_l_piece_prio)
365 this.menu.getElement('a[href$=firstLastPiecePrio]').parentNode.addClass('separator');
366 else
367 this.menu.getElement('a[href$=firstLastPiecePrio]').parentNode.removeClass('separator');
369 if (show_seq_dl)
370 this.showItem('sequentialDownload');
371 else
372 this.hideItem('sequentialDownload');
374 if (show_f_l_piece_prio)
375 this.showItem('firstLastPiecePrio');
376 else
377 this.hideItem('firstLastPiecePrio');
379 this.setItemChecked('sequentialDownload', all_are_seq_dl);
380 this.setItemChecked('firstLastPiecePrio', all_are_f_l_piece_prio);
382 this.showItem('downloadLimit');
383 this.menu.getElement('a[href$=uploadLimit]').parentNode.removeClass('separator');
384 this.hideItem('superSeeding');
387 this.showItem('start');
388 this.showItem('pause');
389 this.showItem('forceStart');
390 if (all_are_paused)
391 this.hideItem('pause');
392 else if (all_are_force_start)
393 this.hideItem('forceStart');
394 else if (!there_are_paused && !there_are_force_start)
395 this.hideItem('start');
397 if (!all_are_auto_tmm && there_are_auto_tmm) {
398 this.hideItem('autoTorrentManagement');
400 else {
401 this.showItem('autoTorrentManagement');
402 this.setItemChecked('autoTorrentManagement', all_are_auto_tmm);
405 const contextTagList = $('contextTagList');
406 for (const tagHash in tagList) {
407 const checkbox = contextTagList.getElement('a[href=#Tag/' + tagHash + '] input[type=checkbox]');
408 const checkboxState = tagsSelectionState[tagHash];
409 checkbox.indeterminate = checkboxState.indeterminate;
410 checkbox.checked = checkboxState.checked;
414 updateCategoriesSubMenu: function(category_list) {
415 const categoryList = $('contextCategoryList');
416 categoryList.empty();
417 categoryList.appendChild(new Element('li', {
418 html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/qbt-theme/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>'
419 }));
420 categoryList.appendChild(new Element('li', {
421 html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/qbt-theme/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>'
422 }));
424 const sortedCategories = [];
425 Object.each(category_list, function(category) {
426 sortedCategories.push(category.name);
428 sortedCategories.sort();
430 let first = true;
431 Object.each(sortedCategories, function(categoryName) {
432 const categoryHash = genHash(categoryName);
433 const el = new Element('li', {
434 html: '<a href="javascript:torrentSetCategoryFN(\'' + categoryHash + '\');"><img src="images/qbt-theme/inode-directory.svg"/> ' + escapeHtml(categoryName) + '</a>'
436 if (first) {
437 el.addClass('separator');
438 first = false;
440 categoryList.appendChild(el);
444 updateTagsSubMenu: function(tagList) {
445 const contextTagList = $('contextTagList');
446 while (contextTagList.firstChild !== null)
447 contextTagList.removeChild(contextTagList.firstChild);
449 contextTagList.appendChild(new Element('li', {
450 html: '<a href="javascript:torrentAddTagsFN();">'
451 + '<img src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>'
452 + ' QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]'
453 + '</a>'
454 }));
455 contextTagList.appendChild(new Element('li', {
456 html: '<a href="javascript:torrentRemoveAllTagsFN();">'
457 + '<img src="images/qbt-theme/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>'
458 + ' QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]'
459 + '</a>'
460 }));
462 const sortedTags = [];
463 for (const key in tagList)
464 sortedTags.push(tagList[key].name);
465 sortedTags.sort();
467 for (let i = 0; i < sortedTags.length; ++i) {
468 const tagName = sortedTags[i];
469 const tagHash = genHash(tagName);
470 const el = new Element('li', {
471 html: '<a href="#Tag/' + tagHash + '" onclick="event.preventDefault(); torrentSetTagsFN(\'' + tagHash + '\', !event.currentTarget.getElement(\'input[type=checkbox]\').checked);">'
472 + '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + escapeHtml(tagName)
473 + '</a>'
475 if (i === 0)
476 el.addClass('separator');
477 contextTagList.appendChild(el);
482 const CategoriesFilterContextMenu = new Class({
483 Extends: ContextMenu,
484 updateMenuItems: function() {
485 const id = this.options.element.id;
486 if ((id != CATEGORIES_ALL) && (id != CATEGORIES_UNCATEGORIZED)) {
487 this.showItem('editCategory');
488 this.showItem('deleteCategory');
490 else {
491 this.hideItem('editCategory');
492 this.hideItem('deleteCategory');
497 const TagsFilterContextMenu = new Class({
498 Extends: ContextMenu,
499 updateMenuItems: function() {
500 const id = this.options.element.id;
501 if ((id !== TAGS_ALL.toString()) && (id !== TAGS_UNTAGGED.toString()))
502 this.showItem('deleteTag');
503 else
504 this.hideItem('deleteTag');
508 const SearchPluginsTableContextMenu = new Class({
509 Extends: ContextMenu,
511 updateMenuItems: function() {
512 const enabledColumnIndex = function(text) {
513 const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
514 for (let i = 0; i < columns.length; ++i)
515 if (columns[i].get("html") === "Enabled")
516 return i;
519 this.showItem('Enabled');
520 this.setItemChecked('Enabled', this.options.element.getChildren("td")[enabledColumnIndex()].get("html") === "Yes");
522 this.showItem('Uninstall');