diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de284a051..76a59ff43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,6 +34,7 @@ repos: exclude: | (?x)^( compile_commands.json | + src/webui/www/private/css/lib/.* | src/webui/www/private/scripts/lib/.* )$ @@ -43,6 +44,7 @@ repos: (?x)^( compile_commands.json | configure | + src/webui/www/private/css/lib/.* | src/webui/www/private/scripts/lib/.* )$ exclude_types: @@ -53,6 +55,7 @@ repos: name: Check trailing whitespaces exclude: | (?x)^( + src/webui/www/private/css/lib/.* | src/webui/www/private/scripts/lib/.* )$ exclude_types: diff --git a/AUTHORS b/AUTHORS index 8bf542e10..a3a21b4fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,6 +29,10 @@ Code from other projects: copyright: Dan Haim license: BSD +* files src/webui/www/private/css/lib/vanillaSelectBox.css src/webui/www/private/scripts/lib/vanillaSelectBox.js + copyright: Philippe Meyer + license: MIT + Images Authors: * files: src/icons/qbittorrent-tray.svg copyright: Provided by HVS (raster first proposal) and Atif Afzal(@atfzl github) (vectorized and modified) diff --git a/src/webui/www/.prettierignore b/src/webui/www/.prettierignore new file mode 100644 index 000000000..7dfd44f24 --- /dev/null +++ b/src/webui/www/.prettierignore @@ -0,0 +1 @@ +private/css/lib/*.css diff --git a/src/webui/www/.stylelintrc.json b/src/webui/www/.stylelintrc.json index 76a8c17b5..f677e2fbf 100644 --- a/src/webui/www/.stylelintrc.json +++ b/src/webui/www/.stylelintrc.json @@ -3,6 +3,7 @@ "plugins": [ "stylelint-order" ], + "ignoreFiles": ["private/css/lib/*.css"], "rules": { "color-hex-length": null, "comment-empty-line-before": null, diff --git a/src/webui/www/private/css/lib/vanillaSelectBox.css b/src/webui/www/private/css/lib/vanillaSelectBox.css new file mode 100644 index 000000000..b12a75766 --- /dev/null +++ b/src/webui/www/private/css/lib/vanillaSelectBox.css @@ -0,0 +1,271 @@ +.hidden-search { + display: none !important; +} + + +li[data-parent].closed{ + display:none !important; +} + +li[data-parent].open:not(.hidden-search){ + display:block !important; +} + +.vsb-menu{ + cursor:pointer; + z-index:1000; + display:block; + visibility: hidden; + position:absolute;/*Don't change*/ + border:1px solid #B2B2B2; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0,0,0,.15); + box-shadow: 0 6px 12px rgba(0,0,0,.175); + border-radius:4px; + font-size : 11px; +} + +.vsb-js-search-zone{ + position:absolute;/*Don't change*/ + z-index:1001; + width: 80%; + min-height:1.8em; + padding: 2px; + background-color: #fff; +} + +.vsb-js-search-zone input{ + border: 1px solid grey; + margin-left: 2px; + width: 96%; + border-radius: 4px; + height: 25px !important; +} + +.vsb-main{ + position: relative;/*Don't change*/ + display: inline-block; + vertical-align: middle; + text-align:left; +} + +.vsb-menu li:hover { + background: linear-gradient(#f5f5f5, #e8e8e8); +} + +.vsb-menu ul{ + user-select:none; + list-style:none; + white-space: nowrap; + margin:0px; + margin-top:4px; + padding-left:10px; + padding-right:10px; + padding-bottom:3px; + color: #333; + cursor:pointer; + overflow-y:auto; +} + +li.disabled{ + cursor:not-allowed; + opacity:0.3; + background-color: #999; +} + +li.overflow{ + cursor:not-allowed; + opacity:0.3; + background-color: #999; +} + +li.short{ + overflow:hidden; + text-overflow: ellipsis; +} + +.vsb-main button{ + min-width: 120px; + border-radius: 0; + width: 100%; + text-align: left; + z-index: 1; + color: #333; + background: white !important; + border: 1px solid #999 !important; + line-height:20px; + font-size:14px; + padding:6px 12px; +} + +.vsb-main button.disabled{ + cursor:not-allowed; + opacity:0.65; +} + +.vsb-main .title { + margin-right: 6px; + user-select:none; +} + +.vsb-main li:hover { + background: linear-gradient(#f5f5f5, #e8e8e8); +} + +.vsb-main ul{ + white-space: nowrap; +} + +.vsb-menu li { + font-size: 14px; + background-color: #fff; + min-height:1.4em; + padding: 0.2em 2em 0.2em 1em; +} + +.vsb-menu li.grouped-option b { + display: inline-block; + font-size: 15px; + margin-left:10px; + transform: translate(-18px); +} + +.vsb-menu li.grouped-option.open span { + display: inline-block; + font-size: inherit; + margin-top:-2px; + height: 8px; + width: 8px; + transform: translate(-38px) rotate(45deg); + border-bottom: 3px solid black; + border-right: 3px solid black; + border-radius:2px; +} + +.vsb-menu li.grouped-option.closed span { + display: inline-block; + font-size: inherit; + height: 8px; + width: 8px; + transform: translate(-38px) rotate(-45deg); + border-bottom: 3px solid black; + border-right: 3px solid black; + border-radius:2px; +} + +.vsb-menu li.grouped-option i { + display: inline-block; + font-size: inherit; + float:left; + font-weight:bold; + margin-left:22px; + margin-right:2px; + height: 11px; + width: 8px; + border : 1px solid; + border-radius : 3px; + padding: 1px 3px 2px 3px; + margin-top:0px; + color:black; +} + +.vsb-menu li.grouped-option.checked i::after { + content: ""; + display: inline-block; + font-size: inherit; + color: #333; + float:left; + margin-left:0px; + display: inline-block; + transform: rotate(45deg); + height: 8px; + width: 5px; + border-bottom: 3px solid black; + border-right: 3px solid black; +} + +.vsb-menu :not(.multi) li.active { + margin-left:7px; +} + +.vsb-menu :not(.multi) li.active::before { + content: ""; + display: inline-block; + font-size: inherit; + margin-left:-18px; + transform: rotate(45deg); + height: 10px; + width: 5px; + border-bottom: 3px solid black; + border-right: 3px solid black; + border-radius:2px; +} + +.vsb-menu .multi li { + font-size: 14px; + background-color: #fff; + min-height:1.4em; + padding: 0.2em 2em 0.2em 26px; +} + +.vsb-menu .multi li.grouped-option { + font-size: 15px; + padding-left: 5px; +} + + +.vsb-menu .multi li.grouped-option:hover { + font-weight: bold; + text-decoration: underline; + color:rgb(52, 31, 112); +} + +.vsb-menu .multi li:not(.grouped-option)::before{ + content: ""; + display: inline-block; + font-size: inherit; + float:left; + font-weight:bold; + margin-left:-22px; + margin-right:2px; + border : 1px solid; + border-radius : 3px; + padding : 7px; + margin-top:0px; + color:black; +} + +.vsb-menu .multi li:not(.grouped-option).active::after { + content: ""; + display: inline-block; + font-size: inherit; + color: #333; + float:left; + margin-left:-18px; + display: inline-block; + transform: rotate(45deg); + margin-top:1px; + height: 8px; + width: 5px; + border-bottom: 3px solid black; + border-right: 3px solid black; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + + +li[data-parent]{ + padding-left: 50px !important; +} + diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index 2b4c089e3..bbdea7f61 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -77,6 +77,7 @@
  • QBT_TR(Speed in Title Bar)QBT_TR[CONTEXT=MainWindow]QBT_TR(Speed in Title Bar)QBT_TR[CONTEXT=MainWindow]
  • QBT_TR(Search Engine)QBT_TR[CONTEXT=MainWindow]QBT_TR(Search Engine)QBT_TR[CONTEXT=MainWindow]
  • QBT_TR(RSS)QBT_TR[CONTEXT=MainWindow]QBT_TR(RSS Reader)QBT_TR[CONTEXT=MainWindow]
  • +
  • QBT_TR(Log)QBT_TR[CONTEXT=MainWindow]QBT_TR(Log)QBT_TR[CONTEXT=MainWindow]
  • QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]QBT_TR(Statistics)QBT_TR[CONTEXT=MainWindow]
  • @@ -116,6 +117,7 @@ +
    diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js index c1e3a2c68..296c00399 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -35,6 +35,7 @@ let serverSyncMainDataInterval = 1500; let customSyncMainDataInterval = null; let searchTabInitialized = false; let rssTabInitialized = false; +let logTabInitialized = false; let syncRequestInProgress = false; @@ -190,9 +191,21 @@ window.addEvent('load', function() { $("rssTabColumn").addClass("invisible"); }; + const buildLogTab = function() { + new MochaUI.Column({ + id: 'logTabColumn', + placement: 'main', + width: null + }); + + // start off hidden + $('logTabColumn').addClass('invisible'); + }; + buildTransfersTab(); buildSearchTab(); buildRssTab(); + buildLogTab(); MochaUI.initializeTabs('mainWindowTabsList'); setCategoryFilter = function(hash) { @@ -304,6 +317,7 @@ window.addEvent('load', function() { // After showing/hiding the toolbar + status bar let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false"; let showRssReader = LocalPreferences.get('show_rss_reader') !== "false"; + let showLogViewer = LocalPreferences.get('show_log_viewer') === 'true'; // After Show Top Toolbar MochaUI.Desktop.setDesktopSize(); @@ -912,6 +926,12 @@ window.addEvent('load', function() { updateTabDisplay(); }); + $('showLogViewerLink').addEvent('click', function(e) { + showLogViewer = !showLogViewer; + LocalPreferences.set('show_log_viewer', showLogViewer.toString()); + updateTabDisplay(); + }); + const updateTabDisplay = function() { if (showRssReader) { $('showRssReaderLink').firstChild.style.opacity = '1'; @@ -941,8 +961,22 @@ window.addEvent('load', function() { $("transfersTabLink").click(); } + if (showLogViewer) { + $('showLogViewerLink').firstChild.style.opacity = '1'; + $('mainWindowTabs').removeClass('invisible'); + $('logTabLink').removeClass('invisible'); + if (!MochaUI.Panels.instances.LogPanel) + addLogPanel(); + } + else { + $('showLogViewerLink').firstChild.style.opacity = '0'; + $('logTabLink').addClass('invisible'); + if ($('logTabLink').hasClass('selected')) + $("transfersTabLink").click(); + } + // display no tabs - if (!showRssReader && !showSearchEngine) + if (!showRssReader && !showSearchEngine && !showLogViewer) $('mainWindowTabs').addClass('invisible'); }; @@ -954,18 +988,21 @@ window.addEvent('load', function() { $("filtersColumn").removeClass("invisible"); $("filtersColumn_handle").removeClass("invisible"); $("mainColumn").removeClass("invisible"); + $('torrentsFilterToolbar').removeClass("invisible"); customSyncMainDataInterval = null; syncData(100); hideSearchTab(); hideRssTab(); + hideLogTab(); }; const hideTransfersTab = function() { $("filtersColumn").addClass("invisible"); $("filtersColumn_handle").addClass("invisible"); $("mainColumn").addClass("invisible"); + $('torrentsFilterToolbar').addClass("invisible"); MochaUI.Desktop.resizePanels(); }; @@ -979,6 +1016,7 @@ window.addEvent('load', function() { customSyncMainDataInterval = 30000; hideTransfersTab(); hideRssTab(); + hideLogTab(); }; const hideSearchTab = function() { @@ -999,14 +1037,37 @@ window.addEvent('load', function() { customSyncMainDataInterval = 30000; hideTransfersTab(); hideSearchTab(); + hideLogTab(); }; const hideRssTab = function() { $("rssTabColumn").addClass("invisible"); - window.qBittorrent.Rss.unload(); + window.qBittorrent.Rss && window.qBittorrent.Rss.unload(); MochaUI.Desktop.resizePanels(); }; + const showLogTab = function() { + if (!logTabInitialized) { + window.qBittorrent.Log.init(); + logTabInitialized = true; + } + else { + window.qBittorrent.Log.load(); + } + + $('logTabColumn').removeClass('invisible'); + customSyncMainDataInterval = 30000; + hideTransfersTab(); + hideSearchTab(); + hideRssTab(); + }; + + const hideLogTab = function() { + $('logTabColumn').addClass('invisible'); + MochaUI.Desktop.resizePanels(); + window.qBittorrent.Log && window.qBittorrent.Log.unload(); + }; + const addSearchPanel = function() { new MochaUI.Panel({ id: 'SearchPanel', @@ -1045,6 +1106,42 @@ window.addEvent('load', function() { }); }; + var addLogPanel = function() { + new MochaUI.Panel({ + id: 'LogPanel', + title: 'Log', + header: true, + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + }, + loadMethod: 'xhr', + contentURL: 'views/log.html', + require: { + css: ['css/lib/vanillaSelectBox.css'], + js: ['scripts/lib/vanillaSelectBox.js'], + }, + tabsURL: 'views/logTabs.html', + tabsOnload: function() { + MochaUI.initializeTabs('panelTabs'); + + $('logMessageLink').addEvent('click', function(e) { + window.qBittorrent.Log.setCurrentTab('main'); + }); + + $('logPeerLink').addEvent('click', function(e) { + window.qBittorrent.Log.setCurrentTab('peer'); + }); + }, + collapsible: false, + content: '', + column: 'logTabColumn', + height: null + }); + }; + new MochaUI.Panel({ id: 'transferList', title: 'Panel', @@ -1185,6 +1282,7 @@ window.addEvent('load', function() { $('transfersTabLink').addEvent('click', showTransfersTab); $('searchTabLink').addEvent('click', showSearchTab); $('rssTabLink').addEvent('click', showRssTab); + $('logTabLink').addEvent('click', showLogTab); updateTabDisplay(); const registerDragAndDrop = () => { diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index 6f72009ab..426c88c0f 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -46,6 +46,8 @@ window.qBittorrent.DynamicTable = (function() { SearchPluginsTable: SearchPluginsTable, TorrentTrackersTable: TorrentTrackersTable, TorrentFilesTable: TorrentFilesTable, + LogMessageTable: LogMessageTable, + LogPeerTable: LogPeerTable, RssFeedTable: RssFeedTable, RssArticleTable: RssArticleTable, RssDownloaderRulesTable: RssDownloaderRulesTable, @@ -2610,6 +2612,153 @@ window.qBittorrent.DynamicTable = (function() { } }); + const LogMessageTable = new Class({ + Extends: DynamicTable, + + filterText: '', + + filterdLength: function() { + return this.tableBody.getElements('tr').length; + }, + + initColumns: function() { + this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true); + this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]', 350, true); + this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true); + this.newColumn('type', '', 'QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]', 100, true); + this.initColumnsFunctions(); + }, + + initColumnsFunctions: function() { + this.columns['timestamp'].updateTd = function(td, row) { + const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); + td.set({ 'text': date, 'title': date }); + }; + + this.columns['type'].updateTd = function(td, row) { + //Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8 + let logLevel, addClass; + switch (this.getRowValue(row).toInt()) { + case 1: + logLevel = 'QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'logNormal'; + break; + case 2: + logLevel = 'QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'logInfo'; + break; + case 4: + logLevel = 'QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'logWarning'; + break; + case 8: + logLevel = 'QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'logCritical'; + break; + default: + logLevel = 'QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'logUnknown'; + break; + } + td.set({ 'text': logLevel, 'title': logLevel }); + td.getParent('tr').set('class', 'logTableRow ' + addClass); + }; + }, + + getFilteredAndSortedRows: function() { + let filteredRows = []; + const rows = this.rows.getValues(); + this.filterText = window.qBittorrent.Log.getFilterText(); + const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : []; + const logLevels = window.qBittorrent.Log.getSelectedLevels(); + if (filterTerms.length > 0 || logLevels.length < 4) { + for (let i = 0; i < rows.length; ++i) { + if (logLevels.indexOf(rows[i].full_data.type.toString()) == -1) + continue; + + if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms)) + continue; + + filteredRows.push(rows[i]); + } + } + else { + filteredRows = rows; + } + + filteredRows.sort(function(row1, row2) { + const column = this.columns[this.sortedColumn]; + const res = column.compareRows(row1, row2); + return (this.reverseSort == '0') ? res : -res; + }.bind(this)); + + return filteredRows; + }, + + setupCommonEvents: function() {}, + + setupTr: function(tr) { + tr.addClass('logTableRow'); + } + }); + + const LogPeerTable = new Class({ + Extends: LogMessageTable, + + initColumns: function() { + this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true); + this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true); + this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true); + this.newColumn('blocked', '', 'QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true); + this.newColumn('reason', '', 'QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true); + + this.columns['timestamp'].updateTd = function(td, row) { + const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); + td.set({ 'text': date, 'title': date }); + }; + + this.columns['blocked'].updateTd = function(td, row) { + let status, addClass; + if (this.getRowValue(row)) { + status = 'QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'peerBlocked'; + } + else { + status = 'QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]'; + addClass = 'peerBanned'; + } + td.set({ 'text': status, 'title': status }); + td.getParent('tr').set('class', 'logTableRow ' + addClass); + }; + }, + + getFilteredAndSortedRows: function() { + let filteredRows = []; + const rows = this.rows.getValues(); + this.filterText = window.qBittorrent.Log.getFilterText(); + const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : []; + if (filterTerms.length > 0) { + for (let i = 0; i < rows.length; ++i) { + if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms)) + continue; + + filteredRows.push(rows[i]); + } + } + else { + filteredRows = rows; + } + + filteredRows.sort(function(row1, row2) { + const column = this.columns[this.sortedColumn]; + const res = column.compareRows(row1, row2); + return (this.reverseSort == '0') ? res : -res; + }.bind(this)); + + return filteredRows; + } + }); + return exports(); })(); diff --git a/src/webui/www/private/scripts/lib/vanillaSelectBox.js b/src/webui/www/private/scripts/lib/vanillaSelectBox.js new file mode 100644 index 000000000..73e33703a --- /dev/null +++ b/src/webui/www/private/scripts/lib/vanillaSelectBox.js @@ -0,0 +1,1403 @@ +/* +Copyright (C) Philippe Meyer 2019-2021 +Distributed under the MIT License + +vanillaSelectBox : v1.05 : setValue() bug correction on single mode. You could not set the value +vanillaSelectBox : v1.04 : select all issue fixed by https://github.com/arthur911016 +vanillaSelectBox : v1.03 : getResult() an new fonction to get the selected values in an array +vanillaSelectBox : v1.02 : Adding 2 new options "itemsSeparator" to change the default "," item separator showing in the button and translations.item to show the item in singular if there is only one. +vanillaSelectBox : v1.01 : Removing useless code line 550,551 issue 71 by chchch +vanillaSelectBox : v1.00 : Adding a package.json file +vanillaSelectBox : v0.78 : Stop using inline styles in the main button. You can steal use keepInlineStyles:true to use the legacy behaviour +vanillaSelectBox : v0.77 : Work on place holder with bastoune help => still seems to lose placeholder value on multiple dropdown checkall +vanillaSelectBox : v0.76 : New changeTree function : to rebuild the original tree with new data + correcting empty() function +vanillaSelectBox : v0.75 : Remote search ready + local search modification : when a check on optgroup checks children only + if they not excluded from search. +vanillaSelectBox : v0.72 : Remote search (WIP) bugfix [x] Select all duplicated +vanillaSelectBox : v0.71 : Remote search (WIP) better code +vanillaSelectBox : v0.70 : Remote search (WIP) for users to test +vanillaSelectBox : v0.65 : Two levels: bug fix : groups are checked/unchecked when check all/uncheck all is clicked +vanillaSelectBox : v0.64 : Two levels: groups are now checkable to check/uncheck the children options +vanillaSelectBox : v0.63 : Two levels: one click on the group selects / unselects children +vanillaSelectBox : v0.62 : New option: maxOptionWidth set a maximum width for each option for narrow menus +vanillaSelectBox : v0.61 : New option: maxSelect, set a maximum to the selectable options in a multiple choice menu +vanillaSelectBox : v0.60 : Two levels: Optgroups are now used to show two level dropdowns +vanillaSelectBox : v0.59 : Bug fix : search box was overlapping first item in single selects +vanillaSelectBox : v0.58 : Bug fixes +vanillaSelectBox : v0.57 : Bug fix (minWidth option not honored) +vanillaSelectBox : v0.56 : The multiselect checkboxes are a little smaller, maxWidth option is now working + added minWidth option as well + The button has now a style attribute to protect its appearance +vanillaSelectBox : v0.55 : All attributes from the original select options are copied to the selectBox element +vanillaSelectBox : v0.54 : if all the options of the select are selected by the user then the check all checkbox is checked +vanillaSelectBox : v0.53 : if all the options of the select are selected then the check all checkbox is checked +vanillaSelectBox : v0.52 : Better support of select('all') => command is consistent with checkbox and selecting / deselecting while searching select / uncheck only the found items +vanillaSelectBox : v0.51 : Translations for select all/clear all + minor css corrections + don't select disabled items +vanillaSelectBox : v0.50 : PR by jaguerra2017 adding a select all/clear all check button + optgroup support ! +vanillaSelectBox : v0.41 : Bug corrected, the menu content was misplaced if a css transform was applied on a parent +vanillaSelectBox : v0.40 : A click on one selectBox close the other opened boxes +vanillaSelectBox : v0.35 : You can enable and disable items +vanillaSelectBox : v0.30 : The menu stops moving around on window resize and scroll + z-index in order of creation for multiple instances +vanillaSelectBox : v0.26 : Corrected bug in stayOpen mode with disable() function +vanillaSelectBox : v0.25 : New option stayOpen, and the dropbox is no longer a dropbox but a nice multi-select +previous version : v0.24 : corrected bug affecting options with more than one class +https://github.com/PhilippeMarcMeyer/vanillaSelectBox +*/ + +let VSBoxCounter = function () { + let count = 0; + let instances = []; + return { + set: function (instancePtr) { + instances.push({ offset: ++count, ptr: instancePtr }); + return instances[instances.length - 1].offset; + }, + remove: function (instanceNr) { + let temp = instances.filter(function (x) { + return x.offset != instanceNr; + }) + instances = temp.splice(0); + }, + closeAllButMe: function (instanceNr) { + instances.forEach(function (x) { + if (x.offset != instanceNr) { + x.ptr.closeOrder(); + } + }); + } + }; +}(); + +function vanillaSelectBox(domSelector, options) { + let self = this; + this.instanceOffset = VSBoxCounter.set(self); + this.domSelector = domSelector; + this.root = document.querySelector(domSelector); + this.rootToken = null; + this.main; + this.button; + this.title; + this.isMultiple = this.root.hasAttribute("multiple"); + this.multipleSize = this.isMultiple && this.root.hasAttribute("size") ? parseInt(this.root.getAttribute("size")) : -1; + this.isOptgroups = false; + this.currentOptgroup = 0; + this.drop; + this.top; + this.left; + this.options; + this.listElements; + this.isDisabled = false; + this.search = false; + this.searchZone = null; + this.inputBox = null; + this.disabledItems = []; + this.ulminWidth = 140; + this.ulmaxWidth = 280; + this.ulminHeight = 25; + this.maxOptionWidth = Infinity; + this.maxSelect = Infinity; + this.isInitRemote = false; + this.isSearchRemote = false; + this.onInit = null; + this.onSearch = null; // if isRemote is true : a user defined function that loads more options from the back + this.onInitSize = null; + this.forbidenAttributes = ["class", "selected", "disabled", "data-text", "data-value", "style"]; + this.forbidenClasses = ["active", "disabled"]; + this.userOptions = { + maxWidth: 500, + minWidth: -1, + maxHeight: 400, + translations: { "all": "All", "item": "item","items": "items", "selectAll": "Select All", "clearAll": "Clear All" }, + search: false, + placeHolder: "", + stayOpen: false, + disableSelectAll: false, + buttonItemsSeparator : "," + } + this.keepInlineStyles = true; + this.keepInlineCaretStyles = true; + if (options) { + if(options.itemsSeparator!= undefined){ + this.userOptions.buttonItemsSeparator = options.itemsSeparator; + } + if (options.maxWidth != undefined) { + this.userOptions.maxWidth = options.maxWidth; + } + if (options.minWidth != undefined) { + this.userOptions.minWidth = options.minWidth; + } + if (options.maxHeight != undefined) { + this.userOptions.maxHeight = options.maxHeight; + } + if (options.translations != undefined) { + for (var property in options.translations) { + if (options.translations.hasOwnProperty(property)) { + if (this.userOptions.translations[property]) { + this.userOptions.translations[property] = options.translations[property]; + } + } + } + } + if (options.placeHolder != undefined) { + this.userOptions.placeHolder = options.placeHolder; + } + if (options.search != undefined) { + this.search = options.search; + } + if (options.remote != undefined && options.remote) { + + // user defined onInit function + if (options.remote.onInit!= undefined && typeof options.remote.onInit === 'function') { + this.onInit = options.remote.onInit; + this.isInitRemote = true; + } + if (options.remote.onInitSize != undefined) { + this.onInitSize = options.remote.onInitSize; + if (this.onInitSize < 3) this.onInitSize = 3; + } + // user defined remote search function + if (options.remote.onSearch != undefined && typeof options.remote.onSearch === 'function') { + this.onSearch = options.remote.onSearch; + this.isSearchRemote = true; + } + } + + if (options.stayOpen != undefined) { + this.userOptions.stayOpen = options.stayOpen; + } + + if (options.disableSelectAll != undefined) { + this.userOptions.disableSelectAll = options.disableSelectAll; + } + + if (options.maxSelect != undefined && !isNaN(options.maxSelect) && options.maxSelect >= 1) { + this.maxSelect = options.maxSelect; + this.userOptions.disableSelectAll = true; + } + + if (options.maxOptionWidth != undefined && !isNaN(options.maxOptionWidth) && options.maxOptionWidth >= 20) { + this.maxOptionWidth = options.maxOptionWidth; + this.ulminWidth = options.maxOptionWidth + 60; + this.ulmaxWidth = options.maxOptionWidth + 60; + } + + if(options.keepInlineStyles != undefined ) { + this.keepInlineStyles = options.keepInlineStyles; + } + if(options.keepInlineCaretStyles != undefined ) { + this.keepInlineCaretStyles = options.keepInlineCaretStyles; + } + + } + + this.closeOrder = function () { + let self = this; + if (!self.userOptions.stayOpen) { + self.drop.style.visibility = "hidden"; + if (self.search) { + self.inputBox.value = ""; + Array.prototype.slice.call(self.listElements).forEach(function (x) { + x.classList.remove("hide"); + }); + } + } + } + + this.getCssArray = function (selector) { + // Why inline css ? To protect the button display from foreign css files + let cssArray = []; + if (selector === ".vsb-main button") { + cssArray = [ + { "key": "min-width", "value": "120px" }, + { "key": "border-radius", "value": "0" }, + { "key": "width", "value": "100%" }, + { "key": "text-align", "value": "left" }, + { "key": "z-index", "value": "1" }, + { "key": "color", "value": "#333" }, + { "key": "background", "value": "white !important" }, + { "key": "border", "value": "1px solid #999 !important" }, + { "key": "line-height", "value": "20px" }, + { "key": "font-size", "value": "14px" }, + { "key": "padding", "value": "6px 12px" } + ] + } + + return cssArrayToString(cssArray); + + function cssArrayToString(cssList) { + let list = ""; + cssList.forEach(function (x) { + list += x.key + ":" + x.value + ";"; + }); + return list; + } + } + + this.init = function () { + let self = this; + if (self.isInitRemote) { + self.onInit("",self.onInitSize) + .then(function (data) { + self.buildSelect(data); + self.createTree(); + }); + } else { + self.createTree(); + } + } + + this.getResult = function () { + let self = this; + let result = []; + let collection = self.root.querySelectorAll("option"); + collection.forEach(function (x) { + if (x.selected) { + result.push(x.value); + } + }); + return result; + } + + this.createTree = function () { + + this.rootToken = self.domSelector.replace(/[^A-Za-z0-9]+/, "") + this.root.style.display = "none"; + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + already.remove(); + } + this.main = document.createElement("div"); + this.root.parentNode.insertBefore(this.main, this.root.nextSibling); + this.main.classList.add("vsb-main"); + this.main.setAttribute("id", "btn-group-" + this.rootToken); + this.main.style.marginLeft = this.main.style.marginLeft; + if (self.userOptions.stayOpen) { + this.main.style.minHeight = (this.userOptions.maxHeight + 10) + "px"; + } + + if (self.userOptions.stayOpen) { + this.button = document.createElement("div"); + } else { + this.button = document.createElement("button"); + if(this.keepInlineStyles) { + var cssList = self.getCssArray(".vsb-main button"); + this.button.setAttribute("style", cssList); + } + } + this.button.style.maxWidth = this.userOptions.maxWidth + "px"; + if (this.userOptions.minWidth !== -1) { + this.button.style.minWidth = this.userOptions.minWidth + "px"; + } + + this.main.appendChild(this.button); + this.title = document.createElement("span"); + this.button.appendChild(this.title); + this.title.classList.add("title"); + let caret = document.createElement("span"); + this.button.appendChild(caret); + + caret.classList.add("caret"); + if(this.keepInlineCaretStyles) { + caret.style.position = "absolute"; + caret.style.right = "8px"; + caret.style.marginTop = "8px"; + } + + if (self.userOptions.stayOpen) { + caret.style.display = "none"; + this.title.style.paddingLeft = "20px"; + this.title.style.fontStyle = "italic"; + this.title.style.verticalAlign = "20%"; + } + + this.drop = document.createElement("div"); + this.main.appendChild(this.drop); + this.drop.classList.add("vsb-menu"); + this.drop.style.zIndex = 2000 - this.instanceOffset; + this.ul = document.createElement("ul"); + this.drop.appendChild(this.ul); + + this.ul.style.maxHeight = this.userOptions.maxHeight + "px"; + this.ul.style.minWidth = this.ulminWidth + "px"; + this.ul.style.maxWidth = this.ulmaxWidth + "px"; + this.ul.style.minHeight = this.ulminHeight + "px"; + if (this.isMultiple) { + this.ul.classList.add("multi"); + if (!self.userOptions.disableSelectAll) { + let selectAll = document.createElement("option"); + selectAll.setAttribute("value", 'all'); + selectAll.innerText = self.userOptions.translations.selectAll; + this.root.insertBefore(selectAll, (this.root.hasChildNodes()) + ? this.root.childNodes[0] + : null); + } + } + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + + if (this.search) { + this.searchZone = document.createElement("div"); + this.ul.appendChild(this.searchZone); + this.searchZone.classList.add("vsb-js-search-zone"); + this.searchZone.style.zIndex = 2001 - this.instanceOffset; + this.inputBox = document.createElement("input"); + this.searchZone.appendChild(this.inputBox); + this.inputBox.setAttribute("type", "text"); + this.inputBox.setAttribute("id", "search_" + this.rootToken); + if (this.maxOptionWidth < Infinity) { + this.searchZone.style.maxWidth = self.maxOptionWidth + 30 + "px"; + this.inputBox.style.maxWidth = self.maxOptionWidth + 30 + "px"; + } + + var para = document.createElement("p"); + this.ul.appendChild(para); + para.style.fontSize = "12px"; + para.innerHTML = " "; + this.ul.addEventListener("scroll", function (e) { + var y = this.scrollTop; + self.searchZone.parentNode.style.top = y + "px"; + }); + } + + this.options = document.querySelectorAll(this.domSelector + " > option"); + Array.prototype.slice.call(this.options).forEach(function (x) { + let text = x.textContent; + let value = x.value; + let originalAttrs; + if (x.hasAttributes()) { + originalAttrs = Array.prototype.slice.call(x.attributes) + .filter(function (a) { + return self.forbidenAttributes.indexOf(a.name) === -1 + }); + } + let classes = x.getAttribute("class"); + if (classes) { + classes = classes + .split(" ") + .filter(function (c) { + return self.forbidenClasses.indexOf(c) === -1 + }); + } else { + classes = []; + } + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + let isDisabled = x.hasAttribute("disabled"); + + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + + if (originalAttrs !== undefined) { + originalAttrs.forEach(function (a) { + li.setAttribute(a.name, a.value); + }); + } + + classes.forEach(function (x) { + li.classList.add(x); + }); + + if (self.maxOptionWidth < Infinity) { + li.classList.add("short"); + li.style.maxWidth = self.maxOptionWidth + "px"; + } + + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = self.userOptions.buttonItemsSeparator; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + if (isDisabled) { + li.classList.add("disabled"); + } + li.appendChild(document.createTextNode(" " + text)); + }); + + if (document.querySelector(self.domSelector + ' optgroup') !== null) { + self.isOptgroups = true; + self.options = document.querySelectorAll(self.domSelector + " option"); + let groups = document.querySelectorAll(self.domSelector + ' optgroup'); + Array.prototype.slice.call(groups).forEach(function (group) { + let groupOptions = group.querySelectorAll('option'); + let li = document.createElement("li"); + let span = document.createElement("span"); + let iCheck = document.createElement("i"); + let labelElement = document.createElement("b"); + let dataWay = group.getAttribute("data-way"); + if (!dataWay) dataWay = "closed"; + if (!dataWay || (dataWay !== "closed" && dataWay !== "open")) dataWay = "closed"; + li.appendChild(span); + li.appendChild(iCheck); + self.ul.appendChild(li); + li.classList.add('grouped-option'); + li.classList.add(dataWay); + self.currentOptgroup++; + let optId = self.rootToken + "-opt-" + self.currentOptgroup; + li.id = optId; + li.appendChild(labelElement); + labelElement.appendChild(document.createTextNode(group.label)); + li.setAttribute("data-text", group.label); + self.ul.appendChild(li); + + Array.prototype.slice.call(groupOptions).forEach(function (x) { + let text = x.textContent; + let value = x.value; + let classes = x.getAttribute("class"); + if (classes) { + classes = classes.split(" "); + } + else { + classes = []; + } + classes.push(dataWay); + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + li.setAttribute("data-parent", optId); + if (classes.length != 0) { + classes.forEach(function (x) { + li.classList.add(x); + }); + } + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = self.userOptions.buttonItemsSeparator; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + li.appendChild(document.createTextNode(text)); + }) + }) + } + + let optionsLength = self.options.length - Number(!self.userOptions.disableSelectAll); + + if (optionsLength == nrActives) { // Bastoune idea to preserve the placeholder + let wordForAll = self.userOptions.translations.all; + selectedTexts = wordForAll; + } else if (self.multipleSize != -1) { + if (nrActives > self.multipleSize) { + let wordForItems = nrActives === 1 ? self.userOptions.translations.item : self.userOptions.translations.items; + selectedTexts = nrActives + " " + wordForItems; + } + } + if (self.isMultiple) { + self.title.innerHTML = selectedTexts; + } + if (self.userOptions.placeHolder != "" && self.title.textContent == "") { + self.title.textContent = self.userOptions.placeHolder; + } + self.listElements = self.drop.querySelectorAll("li:not(.grouped-option)"); + if (self.search) { + self.inputBox.addEventListener("keyup", function (e) { + let searchValue = e.target.value.toUpperCase(); + let searchValueLength = searchValue.length; + let nrFound = 0; + let nrChecked = 0; + let selectAll = null; + if (self.isSearchRemote) { + if (searchValueLength == 0) { + self.remoteSearchIntegrate(null); + } else if (searchValueLength >= 3) { + self.onSearch(searchValue) + .then(function (data) { + self.remoteSearchIntegrate(data); + }); + } + } else { + if (searchValueLength < 3) { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.getAttribute('data-value') === 'all') { + selectAll = x; + } else { + x.classList.remove("hidden-search"); + nrFound++; + nrChecked += x.classList.contains('active'); + } + }); + } else { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.getAttribute('data-value') !== 'all') { + let text = x.getAttribute("data-text").toUpperCase(); + if (text.indexOf(searchValue) === -1 && x.getAttribute('data-value') !== 'all') { + x.classList.add("hidden-search"); + } else { + nrFound++; + x.classList.remove("hidden-search"); + nrChecked += x.classList.contains('active'); + } + } else { + selectAll = x; + } + }); + } + if (selectAll) { + if (nrFound === 0) { + selectAll.classList.add('disabled'); + } else { + selectAll.classList.remove('disabled'); + } + if (nrChecked !== nrFound) { + selectAll.classList.remove("active"); + selectAll.innerText = self.userOptions.translations.selectAll; + selectAll.setAttribute('data-selected', 'false') + } else { + selectAll.classList.add("active"); + selectAll.innerText = self.userOptions.translations.clearAll; + selectAll.setAttribute('data-selected', 'true') + } + } + } + }); + } + + if (self.userOptions.stayOpen) { + self.drop.style.visibility = "visible"; + self.drop.style.boxShadow = "none"; + self.drop.style.minHeight = (this.userOptions.maxHeight + 10) + "px"; + self.drop.style.position = "relative"; + self.drop.style.left = "0px"; + self.drop.style.top = "0px"; + self.button.style.border = "none"; + } else { + this.main.addEventListener("click", function (e) { + if (self.isDisabled) return; + self.drop.style.visibility = "visible"; + document.addEventListener("click", docListener); + e.preventDefault(); + e.stopPropagation(); + if (!self.userOptions.stayOpen) { + VSBoxCounter.closeAllButMe(self.instanceOffset); + } + }); + } + + this.drop.addEventListener("click", function (e) { + if (self.isDisabled) return; + if (e.target.tagName === 'INPUT') return; + let isShowHideCommand = e.target.tagName === 'SPAN'; + let isCheckCommand = e.target.tagName === 'I'; + let liClicked = e.target.parentElement; + if (!liClicked.hasAttribute("data-value")) { + if (liClicked.classList.contains("grouped-option")) { + if (!isShowHideCommand && !isCheckCommand) return; + let oldClass, newClass; + if (isCheckCommand) { // check or uncheck children + self.checkUncheckFromParent(liClicked); + } else { //open or close + if (liClicked.classList.contains("open")) { + oldClass = "open" + newClass = "closed" + } else { + oldClass = "closed" + newClass = "open" + } + liClicked.classList.remove(oldClass); + liClicked.classList.add(newClass); + let theChildren = self.drop.querySelectorAll("[data-parent='" + liClicked.id + "']"); + theChildren.forEach(function (x) { + x.classList.remove(oldClass); + x.classList.add(newClass); + }) + } + return; + } + } + let choiceValue = e.target.getAttribute("data-value"); + let choiceText = e.target.getAttribute("data-text"); + let className = e.target.getAttribute("class"); + + if (className && className.indexOf("disabled") != -1) { + return; + } + + if (className && className.indexOf("overflow") != -1) { + return; + } + + if (choiceValue === 'all') { + if (e.target.hasAttribute('data-selected') + && e.target.getAttribute('data-selected') === 'true') { + self.setValue('none') + } else { + self.setValue('all'); + } + return; + } + + if (!self.isMultiple) { + self.root.value = choiceValue; + self.title.textContent = choiceText; + if (className) { + self.title.setAttribute("class", className + " title"); + } else { + self.title.setAttribute("class", "title"); + } + Array.prototype.slice.call(self.listElements).forEach(function (x) { + x.classList.remove("active"); + }); + if (choiceText != "") { + e.target.classList.add("active"); + } + self.privateSendChange(); + if (!self.userOptions.stayOpen) { + docListener(); + } + } else { + let wasActive = false; + if (className) { + wasActive = className.indexOf("active") != -1; + } + if (wasActive) { + e.target.classList.remove("active"); + } else { + e.target.classList.add("active"); + } + if (e.target.hasAttribute("data-parent")) { + self.checkUncheckFromChild(e.target); + } + + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + let nrAll = 0; + for (let i = 0; i < self.options.length; i++) { + nrAll++; + if (self.options[i].value == choiceValue) { + self.options[i].selected = !wasActive; + } + if (self.options[i].selected) { + nrActives++; + selectedTexts += sep + self.options[i].textContent; + sep = self.userOptions.buttonItemsSeparator; + } + } + if (nrAll == nrActives - Number(!self.userOptions.disableSelectAll)) { + let wordForAll = self.userOptions.translations.all; + selectedTexts = wordForAll; + } else if (self.multipleSize != -1) { + if (nrActives > self.multipleSize) { + let wordForItems = nrActives === 1 ? self.userOptions.translations.item : self.userOptions.translations.items; + selectedTexts = nrActives + " " + wordForItems; + } + } + self.title.textContent = selectedTexts; + self.checkSelectMax(nrActives); + self.checkUncheckAll(); + self.privateSendChange(); + } + e.preventDefault(); + e.stopPropagation(); + if (self.userOptions.placeHolder != "" && self.title.textContent == "") { + self.title.textContent = self.userOptions.placeHolder; + } + }); + function docListener() { + document.removeEventListener("click", docListener); + self.drop.style.visibility = "hidden"; + if (self.search) { + self.inputBox.value = ""; + Array.prototype.slice.call(self.listElements).forEach(function (x) { + x.classList.remove("hidden-search"); + }); + } + } + } + this.init(); + this.checkUncheckAll(); +} + +vanillaSelectBox.prototype.buildSelect = function (data) { + let self = this; + if(data == null || data.length < 1) return; + if(!self.isOptgroups){ + self.isOptgroups = data[0].parent != undefined && data[0].parent != ""; + } + + if(self.isOptgroups){ + let groups = {}; + data = data.filter(function(x){ + return x.parent != undefined && x.parent != ""; + }); + + data.forEach(function (x) { + if(!groups[x.parent]){ + groups[x.parent] = true; + } + }); + for (let group in groups) { + let anOptgroup = document.createElement("optgroup"); + anOptgroup.setAttribute("label", group); + + options = data.filter(function(x){ + return x.parent == group; + }); + options.forEach(function (x) { + let anOption = document.createElement("option"); + anOption.value = x.value; + anOption.text = x.text; + if(x.selected){ + anOption.setAttribute("selected",true) + } + anOptgroup.appendChild(anOption); + }); + self.root.appendChild(anOptgroup); + } + }else{ + data.forEach(function (x) { + let anOption = document.createElement("option"); + anOption.value = x.value; + anOption.text = x.text; + if(x.selected){ + anOption.setAttribute("selected",true) + } + self.root.appendChild(anOption); + }); + } +} + +vanillaSelectBox.prototype.remoteSearchIntegrate = function (data) { + let self = this; + + if (data == null || data.length == 0) { + let dataChecked = self.optionsCheckedToData(); + if(dataChecked) + data = dataChecked.slice(0); + self.remoteSearchIntegrateIt(data); + } else { + let dataChecked = self.optionsCheckedToData(); + if (dataChecked.length > 0){ + for (var i = data.length - 1; i >= 0; i--) { + if(dataChecked.indexOf(data[i].id) !=-1){ + data.slice(i,1); + } + } + } + data = data.concat(dataChecked); + + self.remoteSearchIntegrateIt(data); + } +} + +vanillaSelectBox.prototype.optionsCheckedToData = function () { + let self = this; + let dataChecked = []; + let treeOptions = self.ul.querySelectorAll("li.active:not(.grouped-option)"); + let keepParents = {}; + if (treeOptions) { + Array.prototype.slice.call(treeOptions).forEach(function (x) { + let oneData = {"value":x.getAttribute("data-value"),"text":x.getAttribute("data-text"),"selected":true}; + if(oneData.value !== "all"){ + if(self.isOptgroups){ + let parentId = x.getAttribute("data-parent"); + if(keepParents[parentId]!=undefined){ + oneData.parent = keepParents[parentId]; + }else{ + let parentPtr = self.ul.querySelector("#"+parentId); + let parentName = parentPtr.getAttribute("data-text"); + keepParents[parentId] = parentName; + oneData.parent = parentName; + } + } + dataChecked.push(oneData); + } + }); + } + return dataChecked; +} + +vanillaSelectBox.prototype.removeOptionsNotChecked = function (data) { + let self = this; + let minimumSize = self.onInitSize; + let newSearchSize = data == null ? 0 : data.length; + let presentSize = self.root.length; + if (presentSize + newSearchSize > minimumSize) { + let maxToRemove = presentSize + newSearchSize - minimumSize - 1; + let removed = 0; + for (var i = self.root.length - 1; i >= 0; i--) { + if (self.root.options[i].selected == false) { + if (removed <= maxToRemove) { + removed++; + self.root.remove(i); + } + } + } + } +} + +vanillaSelectBox.prototype.changeTree = function (data, options) { + let self = this; + self.empty(); + self.remoteSearchIntegrateIt(data); + if (options && options.onSearch) { + if (typeof options.onSearch === 'function') { + self.onSearch = options.onSearch; + self.isSearchRemote = true; + } + } + self.listElements = this.drop.querySelectorAll("li:not(.grouped-option)"); +} + +vanillaSelectBox.prototype.remoteSearchIntegrateIt = function (data) { + let self = this; + if (data == null || data.length == 0) return; + while(self.root.firstChild) + self.root.removeChild(self.root.firstChild); + + self.buildSelect(data); + self.reloadTree(); +} + +vanillaSelectBox.prototype.reloadTree = function () { + let self = this; + let lis = self.ul.querySelectorAll("li"); + if (lis != null) { + for (var i = lis.length - 1; i >= 0; i--) { + if (lis[i].getAttribute('data-value') !== 'all') { + self.ul.removeChild(lis[i]); + } + } + } + + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + let nrAll = 0; + + if (self.isOptgroups) { + if (document.querySelector(self.domSelector + ' optgroup') !== null) { + self.options = document.querySelectorAll(this.domSelector + " option"); + let groups = document.querySelectorAll(this.domSelector + ' optgroup'); + Array.prototype.slice.call(groups).forEach(function (group) { + let groupOptions = group.querySelectorAll('option'); + let li = document.createElement("li"); + let span = document.createElement("span"); + let iCheck = document.createElement("i"); + let labelElement = document.createElement("b"); + let dataWay = group.getAttribute("data-way"); + if (!dataWay) dataWay = "closed"; + if (!dataWay || (dataWay !== "closed" && dataWay !== "open")) dataWay = "closed"; + li.appendChild(span); + li.appendChild(iCheck); + self.ul.appendChild(li); + li.classList.add('grouped-option'); + li.classList.add(dataWay); + self.currentOptgroup++; + let optId = self.rootToken + "-opt-" + self.currentOptgroup; + li.id = optId; + li.appendChild(labelElement); + labelElement.appendChild(document.createTextNode(group.label)); + li.setAttribute("data-text", group.label); + self.ul.appendChild(li); + + Array.prototype.slice.call(groupOptions).forEach(function (x) { + let text = x.textContent; + let value = x.value; + let classes = x.getAttribute("class"); + if (classes) { + classes = classes.split(" "); + } + else { + classes = []; + } + classes.push(dataWay); + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + li.setAttribute("data-parent", optId); + if (classes.length != 0) { + classes.forEach(function (x) { + li.classList.add(x); + }); + } + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = self.userOptions.buttonItemsSeparator; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + li.appendChild(document.createTextNode(text)); + }) + }) + } + self.listElements = this.drop.querySelectorAll("li:not(.grouped-option)"); + } else { + self.options = self.root.querySelectorAll("option"); + Array.prototype.slice.call(self.options).forEach(function (x) { + let text = x.textContent; + let value = x.value; + if (value != "all") { + let originalAttrs; + if (x.hasAttributes()) { + originalAttrs = Array.prototype.slice.call(x.attributes) + .filter(function (a) { + return self.forbidenAttributes.indexOf(a.name) === -1 + }); + } + let classes = x.getAttribute("class"); + if (classes) { + classes = classes + .split(" ") + .filter(function (c) { + return self.forbidenClasses.indexOf(c) === -1 + }); + } else { + classes = []; + } + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + + let isDisabled = x.disabled; + + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + + if (originalAttrs !== undefined) { + originalAttrs.forEach(function (a) { + li.setAttribute(a.name, a.value); + }); + } + + classes.forEach(function (x) { + li.classList.add(x); + }); + + if (self.maxOptionWidth < Infinity) { + li.classList.add("short"); + li.style.maxWidth = self.maxOptionWidth + "px"; + } + + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = self.userOptions.buttonItemsSeparator; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + if (isDisabled) { + li.classList.add("disabled"); + } + li.appendChild(document.createTextNode(" " + text)); + } + }); + } + +} + +vanillaSelectBox.prototype.disableItems = function (values) { + let self = this; + let foundValues = []; + if (vanillaSelectBox_type(values) == "string") { + values = values.split(","); + } + + if (vanillaSelectBox_type(values) == "array") { + Array.prototype.slice.call(self.options).forEach(function (x) { + if (values.indexOf(x.value) != -1) { + foundValues.push(x.value); + x.setAttribute("disabled", ""); + } + }); + } + Array.prototype.slice.call(self.listElements).forEach(function (x) { + let val = x.getAttribute("data-value"); + if (foundValues.indexOf(val) != -1) { + x.classList.add("disabled"); + } + }); +} + +vanillaSelectBox.prototype.enableItems = function (values) { + let self = this; + let foundValues = []; + if (vanillaSelectBox_type(values) == "string") { + values = values.split(","); + } + + if (vanillaSelectBox_type(values) == "array") { + Array.prototype.slice.call(self.options).forEach(function (x) { + if (values.indexOf(x.value) != -1) { + foundValues.push(x.value); + x.removeAttribute("disabled"); + } + }); + } + + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (foundValues.indexOf(x.getAttribute("data-value")) != -1) { + x.classList.remove("disabled"); + } + }); +} + +vanillaSelectBox.prototype.checkSelectMax = function (nrActives) { + let self = this; + if (self.maxSelect == Infinity || !self.isMultiple) return; + if (self.maxSelect <= nrActives) { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + if (!x.classList.contains('disabled') && !x.classList.contains('active')) { + x.classList.add("overflow"); + } + } + }); + } else { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.classList.contains('overflow')) { + x.classList.remove("overflow"); + } + }); + } +} + +vanillaSelectBox.prototype.checkUncheckFromChild = function (liClicked) { + let self = this; + let parentId = liClicked.getAttribute('data-parent'); + let parentLi = document.getElementById(parentId); + if (!self.isMultiple) return; + let listElements = self.drop.querySelectorAll("li"); + let childrenElements = Array.prototype.slice.call(listElements).filter(function (el) { + return el.hasAttribute("data-parent") && el.getAttribute('data-parent') == parentId && !el.classList.contains('hidden-search') ; + }); + let nrChecked = 0; + let nrCheckable = childrenElements.length; + if (nrCheckable == 0) return; + childrenElements.forEach(function (el) { + if (el.classList.contains('active')) nrChecked++; + }); + if (nrChecked === nrCheckable || nrChecked === 0) { + if (nrChecked === 0) { + parentLi.classList.remove("checked"); + } else { + parentLi.classList.add("checked"); + } + } else { + parentLi.classList.remove("checked"); + } +} + +vanillaSelectBox.prototype.checkUncheckFromParent = function (liClicked) { + let self = this; + let parentId = liClicked.id; + if (!self.isMultiple) return; + let listElements = self.drop.querySelectorAll("li"); + let childrenElements = Array.prototype.slice.call(listElements).filter(function (el) { + return el.hasAttribute("data-parent") && el.getAttribute('data-parent') == parentId && !el.classList.contains('hidden-search'); + }); + let nrChecked = 0; + let nrCheckable = childrenElements.length; + if (nrCheckable == 0) return; + childrenElements.forEach(function (el) { + if (el.classList.contains('active')) nrChecked++; + }); + if (nrChecked === nrCheckable || nrChecked === 0) { + //check all or uncheckAll : just do the opposite + childrenElements.forEach(function (el) { + var event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + el.dispatchEvent(event); + }); + if (nrChecked === 0) { + liClicked.classList.add("checked"); + } else { + liClicked.classList.remove("checked"); + } + } else { + //check all + liClicked.classList.remove("checked"); + childrenElements.forEach(function (el) { + if (!el.classList.contains('active')) { + var event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + el.dispatchEvent(event); + } + }); + } +} + +vanillaSelectBox.prototype.checkUncheckAll = function () { + let self = this; + if (!self.isMultiple) return; + let nrChecked = 0; + let nrCheckable = 0; + let totalAvailableElements = 0; + let checkAllElement = null; + if (self.listElements == null) return; + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + if (x.getAttribute('data-value') === 'all') { + checkAllElement = x; + } + if (x.getAttribute('data-value') !== 'all' + && !x.classList.contains('hidden-search') + && !x.classList.contains('disabled')) { + nrCheckable++; + nrChecked += x.classList.contains('active'); + } + if (x.getAttribute('data-value') !== 'all' + && !x.classList.contains('disabled')) { + totalAvailableElements++; + } + } + }); + + if (checkAllElement) { + if (nrChecked === nrCheckable) { + // check the checkAll checkbox + if (nrChecked === totalAvailableElements) { + self.title.textContent = self.userOptions.translations.all; + } + checkAllElement.classList.add("active"); + checkAllElement.innerText = self.userOptions.translations.clearAll; + checkAllElement.setAttribute('data-selected', 'true') + } else if (nrChecked === 0) { + // uncheck the checkAll checkbox + self.title.textContent = self.userOptions.placeHolder; + checkAllElement.classList.remove("active"); + checkAllElement.innerText = self.userOptions.translations.selectAll; + checkAllElement.setAttribute('data-selected', 'false') + } + } +} + +vanillaSelectBox.prototype.setValue = function (values) { + let self = this; + let listElements = self.drop.querySelectorAll("li"); + + if (values == null || values == undefined || values == "") { + self.empty(); + } else { + if (self.isMultiple) { + if (vanillaSelectBox_type(values) == "string") { + if (values === "all") { + values = []; + Array.prototype.slice.call(listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + let value = x.getAttribute('data-value'); + if (value !== 'all') { + if (!x.classList.contains('hidden-search') && !x.classList.contains('disabled')) { + values.push(x.getAttribute('data-value')); + } + // already checked (but hidden by search) + if (x.classList.contains('active')) { + if (x.classList.contains('hidden-search') || x.classList.contains('disabled')) { + values.push(value); + } + } + }else{ + x.classList.add("active"); + } + } else if (x.classList.contains('grouped-option')) { + x.classList.add("checked"); + } + }); + } else if (values === "none") { + values = []; + Array.prototype.slice.call(listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + let value = x.getAttribute('data-value'); + if (value !== 'all') { + if (x.classList.contains('active')) { + if (x.classList.contains('hidden-search') || x.classList.contains('disabled')) { + values.push(value); + } + } + } + } else if (x.classList.contains('grouped-option')) { + x.classList.remove("checked"); + } + }); + } else { + values = values.split(","); + } + } + let foundValues = []; + if (vanillaSelectBox_type(values) == "array") { + Array.prototype.slice.call(self.options).forEach(function (x) { + if (values.indexOf(x.value) !== -1) { + x.selected = true; + foundValues.push(x.value); + } else { + x.selected = false; + } + }); + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + let nrAll = 0; + Array.prototype.slice.call(listElements).forEach(function (x) { + if (x.value !== 'all') { + nrAll++; + } + if (foundValues.indexOf(x.getAttribute("data-value")) != -1) { + x.classList.add("active"); + nrActives++; + selectedTexts += sep + x.getAttribute("data-text"); + sep = self.userOptions.buttonItemsSeparator; + } else { + x.classList.remove("active"); + } + }); + if (nrAll == nrActives - Number(!self.userOptions.disableSelectAll)) { + let wordForAll = self.userOptions.translations.all; + selectedTexts = wordForAll; + } else if (self.multipleSize != -1) { + if (nrActives > self.multipleSize) { + let wordForItems = nrActives === 1 ? self.userOptions.translations.item : self.userOptions.translations.items; + selectedTexts = nrActives + " " + wordForItems; + } + } + self.title.textContent = selectedTexts; + self.privateSendChange(); + } + self.checkUncheckAll(); + } else { + let found = false; + let text = ""; + let classNames = "" + Array.prototype.slice.call(listElements).forEach(function (x) { + let liVal = x.getAttribute("data-value"); + if (liVal == values) { + x.classList.add("active"); + found = true; + text = x.getAttribute("data-text") + } else { + x.classList.remove("active"); + } + }); + Array.prototype.slice.call(self.options).forEach(function (x) { + if (x.value == values) { + x.selected = true; + className = x.getAttribute("class"); + if (!className) className = ""; + } else { + x.selected = false; + } + }); + if (found) { + self.title.textContent = text; + if (self.userOptions.placeHolder != "" && self.title.textContent == "") { + self.title.textContent = self.userOptions.placeHolder; + } + if (className != "") { + self.title.setAttribute("class", className + " title"); + } else { + self.title.setAttribute("class", "title"); + } + } + } + } +} + +vanillaSelectBox.prototype.privateSendChange = function () { + let event = document.createEvent('HTMLEvents'); + event.initEvent('change', true, false); + this.root.dispatchEvent(event); +} + +vanillaSelectBox.prototype.empty = function () { + Array.prototype.slice.call(this.listElements).forEach(function (x) { + x.classList.remove("active"); + }); + let parentElements = this.drop.querySelectorAll("li.grouped-option"); + if(parentElements){ + Array.prototype.slice.call(parentElements).forEach(function (x) { + x.classList.remove("checked"); + }); + } + Array.prototype.slice.call(this.options).forEach(function (x) { + x.selected = false; + }); + this.title.textContent = ""; + if (this.userOptions.placeHolder != "" && this.title.textContent == "") { + this.title.textContent = this.userOptions.placeHolder; + } + this.checkUncheckAll(); + this.privateSendChange(); +} + +vanillaSelectBox.prototype.destroy = function () { + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + VSBoxCounter.remove(this.instanceOffset); + already.remove(); + this.root.style.display = "inline-block"; + } +} +vanillaSelectBox.prototype.disable = function () { + this.main.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + button = already.querySelector("button") + if (button) button.classList.add("disabled"); + this.isDisabled = true; + } +} +vanillaSelectBox.prototype.enable = function () { + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + button = already.querySelector("button") + if (button) button.classList.remove("disabled"); + this.isDisabled = false; + } +} + +vanillaSelectBox.prototype.showOptions = function () { + console.log(this.userOptions); +} +// Polyfills for IE +if (!('remove' in Element.prototype)) { + Element.prototype.remove = function () { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; +} + +function vanillaSelectBox_type(target) { + const computedType = Object.prototype.toString.call(target); + const stripped = computedType.replace("[object ", "").replace("]", ""); + const lowercased = stripped.toLowerCase(); + return lowercased; +} diff --git a/src/webui/www/private/views/log.html b/src/webui/www/private/views/log.html new file mode 100644 index 000000000..17679e434 --- /dev/null +++ b/src/webui/www/private/views/log.html @@ -0,0 +1,427 @@ + + +
    +
    +
    + + + + + +
    + +
    + QBT_TR(Results)QBT_TR[CONTEXT=ExecutionLogWidget] (QBT_TR(showing)QBT_TR[CONTEXT=ExecutionLogWidget] 0 QBT_TR(out of)QBT_TR[CONTEXT=ExecutionLogWidget] 0): +
    +
    + +
    +
    +
    + + + + +
    +
    +
    + + + + + +
    +
    +
    + +
    +
    + + + + diff --git a/src/webui/www/private/views/logTabs.html b/src/webui/www/private/views/logTabs.html new file mode 100644 index 000000000..7f10519e6 --- /dev/null +++ b/src/webui/www/private/views/logTabs.html @@ -0,0 +1,7 @@ + diff --git a/src/webui/www/webui.qrc b/src/webui/www/webui.qrc index 597453128..bdf1b6c8b 100644 --- a/src/webui/www/webui.qrc +++ b/src/webui/www/webui.qrc @@ -12,6 +12,7 @@ private/css/noscript.css private/css/style.css private/css/Tabs.css + private/css/lib/vanillaSelectBox.css private/css/Window.css private/download.html private/downloadlimit.html @@ -392,6 +393,7 @@ private/scripts/prop-trackers.js private/scripts/prop-webseeds.js private/scripts/speedslider.js + private/scripts/lib/vanillaSelectBox.js private/setlocation.html private/shareratio.html private/upload.html @@ -400,6 +402,8 @@ private/views/aboutToolbar.html private/views/filters.html private/views/installsearchplugin.html + private/views/log.html + private/views/logTabs.html private/views/preferences.html private/views/preferencesToolbar.html private/views/properties.html