From 66bec102dbd6cd8317b78563aab09f3f3515f545 Mon Sep 17 00:00:00 2001 From: Alessio Gogna <5177307+alecsg77@users.noreply.github.com> Date: Sat, 8 May 2021 22:24:18 +0200 Subject: [PATCH] [Feature] Filter Meta Indexer by tag and by language (#11662). resolves #8884 resolves #7170 resolves #4787 resolves #2185 * bump to 0.18.* Also partially addresses https://github.com/Jackett/Jackett/issues/661 (if user adds `enabled` and `disabled` tags). Co-authored-by: garfield69 Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com> --- azure-pipelines.yml | 2 +- src/Jackett.Common/Content/css/tagify.css | 1 + src/Jackett.Common/Content/custom.css | 22 ++ src/Jackett.Common/Content/custom.js | 155 +++++++++-- src/Jackett.Common/Content/custom_mobile.css | 18 ++ src/Jackett.Common/Content/index.html | 38 ++- .../Content/libs/jQuery.tagify.min.js | 18 ++ src/Jackett.Common/Content/libs/tagify.min.js | 1 + src/Jackett.Common/Indexers/BaseIndexer.cs | 4 + src/Jackett.Common/Indexers/IIndexer.cs | 2 + .../Indexers/Meta/BaseMetaIndexer.cs | 8 +- .../Indexers/Meta/MetaIndexers.cs | 40 +++ src/Jackett.Common/Jackett.Common.csproj | 9 + src/Jackett.Common/Models/DTO/Indexer.cs | 4 + .../Models/IndexerConfig/ConfigurationData.cs | 241 ++++++++++-------- .../Services/IndexerManagerService.cs | 91 +++++-- .../Interfaces/IIndexerManagerService.cs | 2 +- src/Jackett.Common/Utils/FilterFunc.cs | 58 +++++ .../Utils/FilterFuncs/FilterFuncComponent.cs | 46 ++++ .../Utils/FilterFuncs/FilterFuncExpression.cs | 51 ++++ .../Controllers/ResultsController.cs | 2 +- .../ServerConfigurationController.cs | 2 +- .../FilterFuncs/FilterFuncComponentTests.cs | 98 +++++++ .../FilterFuncs/FilterFuncExpressionTests.cs | 128 ++++++++++ .../Common/Utils/FilterFuncs/IndexerStub.cs | 55 ++++ .../Utils/FilterFuncs/LanguageFuncTests.cs | 55 ++++ .../Common/Utils/FilterFuncs/TagFuncTests.cs | 47 ++++ .../Common/Utils/FilterFuncs/TypeFuncTests.cs | 52 ++++ .../TestHelpers/TestExceptions.cs | 9 + .../TestIIndexerManagerServiceHelper.cs | 2 +- 30 files changed, 1091 insertions(+), 170 deletions(-) create mode 100644 src/Jackett.Common/Content/css/tagify.css create mode 100644 src/Jackett.Common/Content/libs/jQuery.tagify.min.js create mode 100644 src/Jackett.Common/Content/libs/tagify.min.js create mode 100644 src/Jackett.Common/Utils/FilterFunc.cs create mode 100644 src/Jackett.Common/Utils/FilterFuncs/FilterFuncComponent.cs create mode 100644 src/Jackett.Common/Utils/FilterFuncs/FilterFuncExpression.cs create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncComponentTests.cs create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncExpressionTests.cs create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs create mode 100644 src/Jackett.Test/TestHelpers/TestExceptions.cs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bb8d7f35b..1c07673cc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,7 +2,7 @@ name: $(majorVersion).$(minorVersion).$(patchVersion) variables: majorVersion: 0 - minorVersion: 17 + minorVersion: 18 patchVersion: $[counter(variables['minorVersion'], 1)] # this will reset when we bump minor jackettVersion: $(majorVersion).$(minorVersion).$(patchVersion) buildConfiguration: Release diff --git a/src/Jackett.Common/Content/css/tagify.css b/src/Jackett.Common/Content/css/tagify.css new file mode 100644 index 000000000..31afa052e --- /dev/null +++ b/src/Jackett.Common/Content/css/tagify.css @@ -0,0 +1 @@ +:root{--tagify-dd-color-primary:rgb(53,149,246);--tagify-dd-bg-color:white}.tagify{--tags-border-color:#DDD;--tags-hover-border-color:#CCC;--tags-focus-border-color:#3595f6;--tag-bg:#E5E5E5;--tag-hover:#D3E2E2;--tag-text-color:black;--tag-text-color--edit:black;--tag-pad:0.3em 0.5em;--tag-inset-shadow-size:1.1em;--tag-invalid-color:#D39494;--tag-invalid-bg:rgba(211, 148, 148, 0.5);--tag-remove-bg:rgba(211, 148, 148, 0.3);--tag-remove-btn-color:black;--tag-remove-btn-bg:none;--tag-remove-btn-bg--hover:#c77777;--input-color:inherit;--tag--min-width:1ch;--tag--max-width:auto;--tag-hide-transition:0.3s;--placeholder-color:rgba(0, 0, 0, 0.4);--placeholder-color-focus:rgba(0, 0, 0, 0.25);--loader-size:.8em;display:flex;align-items:flex-start;flex-wrap:wrap;border:1px solid #ddd;border:1px solid var(--tags-border-color);padding:0;line-height:normal;cursor:text;outline:0;position:relative;box-sizing:border-box;transition:.1s}@keyframes tags--bump{30%{transform:scale(1.2)}}@keyframes rotateLoader{to{transform:rotate(1turn)}}.tagify:hover{border-color:#ccc;border-color:var(--tags-hover-border-color)}.tagify.tagify--focus{transition:0s;border-color:#3595f6;border-color:var(--tags-focus-border-color)}.tagify[readonly]:not(.tagify--mix){cursor:default}.tagify[readonly]:not(.tagify--mix)>.tagify__input{visibility:hidden;width:0;margin:5px 0}.tagify[readonly]:not(.tagify--mix) .tagify__tag>div{padding:.3em .5em;padding:var(--tag-pad)}.tagify[readonly]:not(.tagify--mix) .tagify__tag>div::before{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}.tagify[readonly] .tagify__tag__removeBtn{display:none}.tagify--loading .tagify__input>br:last-child{display:none}.tagify--loading .tagify__input::before{content:none}.tagify--loading .tagify__input::after{content:'';vertical-align:middle;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear;content:''!important;margin:-2px 0 -2px .5em}.tagify--loading .tagify__input:empty::after{margin-left:0}.tagify+input,.tagify+textarea{position:absolute!important;left:-9999em!important;transform:scale(0)!important}.tagify__tag{display:inline-flex;align-items:center;margin:5px 0 5px 5px;position:relative;z-index:1;outline:0;cursor:default;transition:.13s ease-out}.tagify__tag>div{vertical-align:top;box-sizing:border-box;max-width:100%;padding:.3em .5em;padding:var(--tag-pad,.3em .5em);color:#000;color:var(--tag-text-color,#000);line-height:inherit;border-radius:3px;white-space:nowrap;transition:.13s ease-out}.tagify__tag>div>*{white-space:pre-wrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:top;min-width:1ch;max-width:auto;min-width:var(--tag--min-width,1ch);max-width:var(--tag--max-width,auto);transition:.8s ease,.1s color}.tagify__tag>div>[contenteditable]{outline:0;-webkit-user-select:text;user-select:text;cursor:text;margin:-2px;padding:2px;max-width:350px}.tagify__tag>div::before{content:'';position:absolute;border-radius:inherit;left:0;top:0;right:0;bottom:0;z-index:-1;pointer-events:none;transition:120ms ease;animation:tags--bump .3s ease-out 1;box-shadow:0 0 0 1.1em #e5e5e5 inset;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-bg,#e5e5e5) inset}.tagify__tag:focus div::before,.tagify__tag:hover:not([readonly]) div::before{top:-2px;right:-2px;bottom:-2px;left:-2px;box-shadow:0 0 0 1.1em #d3e2e2 inset;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-hover,#d3e2e2) inset}.tagify__tag--loading{pointer-events:none}.tagify__tag--loading .tagify__tag__removeBtn{display:none}.tagify__tag--loading::after{--loader-size:.4em;content:'';vertical-align:middle;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear;margin:0 .5em 0 -.1em}.tagify__tag--flash div::before{animation:none}.tagify__tag--hide{width:0!important;padding-left:0;padding-right:0;margin-left:0;margin-right:0;opacity:0;transform:scale(0);transition:.3s;transition:var(--tag-hide-transition,.3s);pointer-events:none}.tagify__tag--hide>div>*{white-space:nowrap}.tagify__tag.tagify--noAnim>div::before{animation:none}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div>span{opacity:.5}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before{box-shadow:0 0 0 1.1em rgba(211,148,148,.5) inset!important;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-invalid-bg,rgba(211,148,148,.5)) inset!important;transition:.2s}.tagify__tag[readonly] .tagify__tag__removeBtn{display:none}.tagify__tag[readonly]>div::before{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}.tagify__tag--editable>div{color:#000;color:var(--tag-text-color--edit,#000)}.tagify__tag--editable>div::before{box-shadow:0 0 0 2px #d3e2e2 inset!important;box-shadow:0 0 0 2px var(--tag-hover,#d3e2e2) inset!important}.tagify__tag--editable>.tagify__tag__removeBtn{pointer-events:none}.tagify__tag--editable>.tagify__tag__removeBtn::after{opacity:0;transform:translateX(100%) translateX(5px)}.tagify__tag--editable.tagify--invalid>div::before{box-shadow:0 0 0 2px #d39494 inset!important;box-shadow:0 0 0 2px var(--tag-invalid-color,#d39494) inset!important}.tagify__tag__removeBtn{order:5;display:inline-flex;align-items:center;justify-content:center;border-radius:50px;cursor:pointer;font:14px/1 Arial;background:0 0;background:var(--tag-remove-btn-bg,none);color:#000;color:var(--tag-remove-btn-color,#000);width:14px;height:14px;margin-right:4.66667px;margin-left:auto;overflow:hidden;transition:.2s ease-out}.tagify__tag__removeBtn::after{content:"\00D7";transition:.3s,color 0s}.tagify__tag__removeBtn:hover{color:#fff;background:#c77777;background:var(--tag-remove-btn-bg--hover,#c77777)}.tagify__tag__removeBtn:hover+div>span{opacity:.5}.tagify__tag__removeBtn:hover+div::before{box-shadow:0 0 0 1.1em rgba(211,148,148,.3) inset!important;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-remove-bg,rgba(211,148,148,.3)) inset!important;transition:box-shadow .2s}.tagify:not(.tagify--mix) .tagify__input br{display:none}.tagify:not(.tagify--mix) .tagify__input *{display:inline;white-space:nowrap}.tagify__input{flex-grow:1;display:inline-block;min-width:110px;margin:5px;padding:.3em .5em;padding:var(--tag-pad,.3em .5em);line-height:inherit;position:relative;white-space:pre-wrap;color:inherit;color:var(--input-color,inherit);box-sizing:inherit}.tagify__input:empty::before{transition:.2s ease-out;opacity:1;transform:none;display:inline-block;width:auto}.tagify--mix .tagify__input:empty::before{display:inline-block}.tagify__input:focus{outline:0}.tagify__input:focus::before{transition:.2s ease-out;opacity:0;transform:translatex(6px)}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.tagify__input:focus::before{display:none}}@supports (-ms-ime-align:auto){.tagify__input:focus::before{display:none}}.tagify__input:focus:empty::before{transition:.2s ease-out;opacity:1;transform:none;color:rgba(0,0,0,.25);color:var(--placeholder-color-focus)}@-moz-document url-prefix(){.tagify__input:focus:empty::after{display:none}}.tagify__input::before{content:attr(data-placeholder);height:1em;line-height:1em;margin:auto 0;z-index:1;color:rgba(0,0,0,.4);color:var(--placeholder-color);white-space:nowrap;pointer-events:none;opacity:0;position:absolute}.tagify--mix .tagify__input::before{display:none;position:static;line-height:inherit}.tagify__input::after{content:attr(data-suggest);display:inline-block;white-space:pre;color:#000;opacity:.3;pointer-events:none;max-width:100px}.tagify__input .tagify__tag{margin:0 1px}.tagify__input .tagify__tag>div{padding-top:0;padding-bottom:0}.tagify--mix{display:block}.tagify--mix .tagify__input{padding:5px;margin:0;width:100%;height:100%;line-height:1.5;display:block}.tagify--mix .tagify__input::before{height:auto}.tagify--mix .tagify__input::after{content:none}.tagify--select::after{content:'>';opacity:.5;position:absolute;top:50%;right:0;bottom:0;font:16px monospace;line-height:8px;height:8px;pointer-events:none;transform:translate(-150%,-50%) scaleX(1.2) rotate(90deg);transition:.2s ease-in-out}.tagify--select[aria-expanded=true]::after{transform:translate(-150%,-50%) rotate(270deg) scaleY(1.2)}.tagify--select .tagify__tag{position:absolute;top:0;right:1.8em;bottom:0}.tagify--select .tagify__tag div{display:none}.tagify--select .tagify__input{width:100%}.tagify--invalid{--tags-border-color:#D39494}.tagify__dropdown{position:absolute;z-index:9999;transform:translateY(1px);overflow:hidden}.tagify__dropdown[placement=top]{margin-top:0;transform:translateY(-100%)}.tagify__dropdown[placement=top] .tagify__dropdown__wrapper{border-top-width:1px;border-bottom-width:0}.tagify__dropdown[position=text]{box-shadow:0 0 0 3px rgba(var(--tagify-dd-color-primary),.1);font-size:.9em}.tagify__dropdown[position=text] .tagify__dropdown__wrapper{border-width:1px}.tagify__dropdown__wrapper{max-height:300px;overflow:hidden;background:#fff;background:var(--tagify-dd-bg-color);border:1px solid #3595f6;border-color:var(--tagify-dd-color-primary);border-width:1.1px;border-top-width:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.2);transition:.25s cubic-bezier(0,1,.5,1)}.tagify__dropdown__wrapper:hover{overflow:auto}.tagify__dropdown--initial .tagify__dropdown__wrapper{max-height:20px;transform:translateY(-1em)}.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper{transform:translateY(2em)}.tagify__dropdown__item{box-sizing:inherit;padding:.3em .5em;margin:1px;cursor:pointer;border-radius:2px;position:relative;outline:0}.tagify__dropdown__item--active{background:#3595f6;background:var(--tagify-dd-color-primary);color:#fff}.tagify__dropdown__item:active{filter:brightness(105%)} \ No newline at end of file diff --git a/src/Jackett.Common/Content/custom.css b/src/Jackett.Common/Content/custom.css index 246be5a50..750d9eee5 100644 --- a/src/Jackett.Common/Content/custom.css +++ b/src/Jackett.Common/Content/custom.css @@ -76,6 +76,10 @@ body { max-width: 255px; } +.setup-item-inputtags { + max-width: 255px; +} + [data-type=hiddendata]{ display: none; } @@ -328,3 +332,21 @@ input#searchquery { #proxy-warning { color: red; } + +.label-tag { + text-transform: lowercase; + background-color: #777; +} + +.tagify { + height: auto; +} + +.tagify .tagify__input { + min-width: 0; + text-transform: lowercase; +} + +.tagify .tagify__tag-text { + text-transform: lowercase; +} diff --git a/src/Jackett.Common/Content/custom.js b/src/Jackett.Common/Content/custom.js index c48f2be67..9ecceacc0 100644 --- a/src/Jackett.Common/Content/custom.js +++ b/src/Jackett.Common/Content/custom.js @@ -3,6 +3,8 @@ var basePath = ''; var indexers = []; var configuredIndexers = []; var unconfiguredIndexers = []; +var configuredTags = []; +var availableFilters = []; $.fn.inView = function () { if (!this.length) return false; @@ -58,7 +60,7 @@ function openSearchIfNecessary() { decodeURIComponent(item.split('=')[1].replace(/\+/g, '%20'))) }, prev), {}); if ("search" in hashArgs) { - showSearch(hashArgs.tracker, hashArgs.search, hashArgs.category); + showSearch(hashArgs.filter, hashArgs.tracker, hashArgs.search, hashArgs.category); } } @@ -67,6 +69,14 @@ function insertWordWrap(str) { return str.replace(/([\.\-_\/\\])/g, "$1\u200B"); } +function type_filter(indexer) { + return indexer.type == this.value; +} + +function tag_filter(indexer) { + return indexer.tags.map(t => t.toLowerCase()).indexOf(this.value.toLowerCase()) > -1; +} + function getJackettConfig(callback) { api.getServerConfig(callback).fail(function () { doNotify("Error loading Jackett settings, request to Jackett server failed, is server running ?", "danger", "glyphicon glyphicon-alert"); @@ -131,11 +141,14 @@ function loadJackettSettings() { } function reloadIndexers() { + $('#filters').hide(); $('#indexers').hide(); api.getAllIndexers(function (data) { indexers = data; configuredIndexers = []; unconfiguredIndexers = []; + configuredTags = []; + availableFilters = []; for (var i = 0; i < data.length; i++) { var item = data[i]; item.rss_host = resolveUrl(basePath + "/api/v2.0/indexers/" + item.id + "/results/torznab/api?apikey=" + api.key + "&t=search&cat=&q="); @@ -169,7 +182,13 @@ function reloadIndexers() { else unconfiguredIndexers.push(item); } + + configuredTags = configuredIndexers.map(i => i.tags).reduce((a, g) => a.concat(g), []).filter((v, i, a) => a.indexOf(v) === i); + + configureFilters(configuredIndexers); + displayConfiguredIndexersList(configuredIndexers); + $('#indexers div.dataTables_filter input').focusWithoutScrolling(); openSearchIfNecessary(); }).fail(function () { @@ -177,6 +196,23 @@ function reloadIndexers() { }); } +function configureFilters(indexers) { + function add(f) { + if (availableFilters.find(x => x.id == f.id)) + return; + if (!indexers.every(f.apply, f) && indexers.some(f.apply, f)) + availableFilters.push(f); + } + + ["public", "private", "semi-private"] + .map(t => { return { id: "type:" + t, apply: type_filter, value: t } }) + .forEach(add); + + configuredTags.sort() + .map(t => { return { id: "tag:" + t.toLowerCase(), apply: tag_filter, value: t }}) + .forEach(add); +} + function displayConfiguredIndexersList(indexers) { var indexersTemplate = Handlebars.compile($("#configured-indexer-table").html()); var indexersTable = $(indexersTemplate({ @@ -484,17 +520,20 @@ function prepareSearchButtons(element) { var id = $btn.data("id"); $btn.click(function () { window.location.hash = "search&tracker=" + id; - showSearch(id); + showSearch(null, id); }); }); } function prepareSetupButtons(element) { element.find('.indexer-setup').each(function (i, btn) { - var indexer = configuredIndexers[i]; - $(btn).click(function () { - displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks, indexer.description); - }); + var $btn = $(btn); + var id = $btn.data("id"); + var indexer = configuredIndexers.find(i => i.id === id); + if (indexer) + $btn.click(function () { + displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks, indexer.description); + }); }); } @@ -610,11 +649,32 @@ function populateConfigItems(configForm, config) { var item = config[i]; var setupValueTemplate = Handlebars.compile($("#setup-item-" + item.type).html()); item.value_element = setupValueTemplate(item); - var template = setupItemTemplate(item); + var template = $(setupItemTemplate(item)); $formItemContainer.append(template); + setupConfigItem(template, item); } } +function setupConfigItem(configItem, item) { + switch (item.type) { + case "inputtags": { + configItem.find("input").tagify({ + dropdown: { + enabled: 0, + position: "text" + }, + separator: item.separator || ",", + whitelist: item.whitelist || [], + blacklist: item.blacklist || [], + pattern: item.pattern || null, + delimiters: item.delimiters || item.separator || ",", + originalInputValueFormat: function (values) { return values.map(item => item.value.toLowerCase()).join(this.separator); } + }); + } + break; + } +} + function newConfigModal(title, config, caps, link, alternativesitelinks, description) { var configTemplate = Handlebars.compile($("#jackett-config-setup-modal").html()); var configForm = $(configTemplate({ @@ -638,6 +698,8 @@ function newConfigModal(title, config, caps, link, alternativesitelinks, descrip }); } + $("div[data-id='tags'] input", configForm).data("tagify").settings.whitelist = configuredTags; + return configForm; } @@ -668,9 +730,13 @@ function getConfigModalJson(configForm) { $el.find(".setup-item-inputcheckbox input:checked").each(function () { itemEntry.values.push($(this).val()); }); + break; case "inputselect": itemEntry.value = $el.find(".setup-item-inputselect select").val(); break; + case "inputtags": + itemEntry.value = $el.find(".setup-item-inputtags input").val(); + break; } configJson.push(itemEntry) }); @@ -802,14 +868,15 @@ function updateReleasesRow(row) { } } -function showSearch(selectedIndexer, query, category) { +function showSearch(selectedFilter, selectedIndexer, query, category) { var selectedIndexers = []; if (selectedIndexer) - selectedIndexers = selectedIndexer.split(","); + selectedIndexers = selectedIndexer.split(","); $('#select-indexer-modal').remove(); var releaseTemplate = Handlebars.compile($("#jackett-search").html()); var releaseDialog = $(releaseTemplate({ - indexers: configuredIndexers + filters: availableFilters, + active: selectedFilter })); $("#modals").append(releaseDialog); @@ -823,6 +890,29 @@ function showSearch(selectedIndexer, query, category) { window.location.hash = ''; }); + var setTrackers = function (filterId, trackers) { + var select = $('#searchTracker'); + var selected = select.val(); + var filter = availableFilters.find(f => f.id == filterId); + if (filter) + trackers = trackers.filter(filter.apply,filter); + var options = trackers.map(t => { + return { + label: t.name, + value: t.id + } + }); + select.multiselect('dataprovider', options); + select.val(selected).multiselect("refresh"); + }; + + $('#searchFilter').change(jQuery.proxy(function () { + var filterId = $('#searchFilter').val(); + setTrackers(filterId, this.items); + }, { + items: configuredIndexers + })); + var setCategories = function (trackers, items) { var cats = {}; for (var i = 0; i < items.length; i++) { @@ -869,6 +959,7 @@ function showSearch(selectedIndexer, query, category) { return; } var searchString = releaseDialog.find('#searchquery').val(); + var filterId = releaseDialog.find('#searchFilter').val(); var queryObj = { Query: searchString, Category: releaseDialog.find('#searchCategory').val(), @@ -878,14 +969,15 @@ function showSearch(selectedIndexer, query, category) { window.location.hash = Object.entries({ search: encodeURIComponent(queryObj.Query).replace(/%20/g, '+'), tracker: queryObj.Tracker.join(","), - category: queryObj.Category.join(",") - }).map(([k, v], i) => k + '=' + v).join('&'); + category: queryObj.Category.join(","), + filter: filterId ? encodeURIComponent(filterId) : "" + }).filter(([k, v]) => v).map(([k, v], i) => k + '=' + v).join('&'); $('#jackett-search-perform').html($('#spinner').html()); $('#searchResults div.dataTables_filter input').val(""); clearSearchResultTable($('#searchResults')); - var trackerId = "all"; + var trackerId = filterId || "all"; api.resultsForIndexer(trackerId, queryObj, function (data) { for (var i = 0; i < data.Results.length; i++) { var item = data.Results[i]; @@ -906,16 +998,14 @@ function showSearch(selectedIndexer, query, category) { var searchTracker = releaseDialog.find("#searchTracker"); var searchCategory = releaseDialog.find('#searchCategory'); - searchCategory.multiselect({ + var searchFilter = releaseDialog.find('#searchFilter'); + + searchFilter.multiselect({ maxHeight: 400, enableFiltering: true, - includeSelectAllOption: true, enableCaseInsensitiveFiltering: true, - nonSelectedText: 'Any' + nonSelectedText: 'All' }); - if (selectedIndexers) - searchTracker.val(selectedIndexers); - searchTracker.trigger("change"); updateSearchResultTable($('#searchResults'), []); clearSearchResultTable($('#searchResults')); @@ -928,6 +1018,29 @@ function showSearch(selectedIndexer, query, category) { nonSelectedText: 'All' }); + searchCategory.multiselect({ + maxHeight: 400, + enableFiltering: true, + includeSelectAllOption: true, + enableCaseInsensitiveFiltering: true, + nonSelectedText: 'Any' + }); + + if (availableFilters.length > 0) { + if (selectedFilter) { + searchFilter.val(selectedFilter); + searchFilter.multiselect("refresh"); + } + searchFilter.trigger("change"); + } + else + setTrackers(selectedFilter, configuredIndexers); + + if (selectedIndexers) { + searchTracker.val(selectedIndexers); + searchTracker.multiselect("refresh"); + } + searchTracker.trigger("change"); if (category !== undefined) { searchCategory.val(category.split(",")); @@ -1231,7 +1344,7 @@ function bindUIButtons() { }); $("#jackett-show-search").click(function () { - showSearch(null); + showSearch(); window.location.hash = "search"; }); @@ -1348,4 +1461,4 @@ function proxyWarning(input) { } else { $('#proxy-warning').hide(); } -} \ No newline at end of file +} diff --git a/src/Jackett.Common/Content/custom_mobile.css b/src/Jackett.Common/Content/custom_mobile.css index 578880907..64a6f04e1 100644 --- a/src/Jackett.Common/Content/custom_mobile.css +++ b/src/Jackett.Common/Content/custom_mobile.css @@ -330,3 +330,21 @@ input#searchquery { #proxy-warning { color: red; } + +.label-tag { + text-transform: lowercase; + background-color: #777; +} + +.tagify { + height: auto; +} + +.tagify .tagify__input { + min-width: 0; + text-transform: lowercase; +} + +.tagify .tagify__tag-text { + text-transform: lowercase; +} diff --git a/src/Jackett.Common/Content/index.html b/src/Jackett.Common/Content/index.html index e830ce0ba..3d8b69780 100644 --- a/src/Jackett.Common/Content/index.html +++ b/src/Jackett.Common/Content/index.html @@ -22,11 +22,14 @@ + + - - + + + @@ -65,7 +68,7 @@

Configured Indexers

-
+

@@ -213,7 +216,7 @@
- + - + diff --git a/src/Jackett.Common/Content/libs/jQuery.tagify.min.js b/src/Jackett.Common/Content/libs/jQuery.tagify.min.js new file mode 100644 index 000000000..79c021040 --- /dev/null +++ b/src/Jackett.Common/Content/libs/jQuery.tagify.min.js @@ -0,0 +1,18 @@ +;(function($){ + // just a jQuery wrapper for the vanilla version of this component + $.fn.tagify = function(settings = {}){ + return this.each(function() { + var $input = $(this), + tagify; + + if( $input.data("tagify") ) // don't continue if already "tagified" + return this; + + settings.isJQueryPlugin = true; + tagify = new Tagify($input[0], settings); + $input.data("tagify", tagify); + }); + } + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";const t=(t,e,i,s)=>(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase());function e(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function i(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function s(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function a(t){return t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'")}function n(t){return t instanceof Array}function o(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function r(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(o(e[i])){o(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(n(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function l(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var d=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);var h,g={init(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownWrapperSelector)},show(e){var i,s,a,n=this.settings,r="mix"==n.mode&&!n.enforceWhitelist,l=!n.whitelist||!n.whitelist.length,d="manual"==n.dropdown.position;if(e=void 0===e?this.state.inputText:e,(!l||r||n.templates.dropdownItemNoMatch)&&!1!==n.dropdown.enable&&!this.state.isLoading){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems.call(this,e),e&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",e),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:e}))),!a){if(this.suggestedListItems.length)e&&r&&!this.state.editing.scope&&!t(this.suggestedListItems[0].value,e)&&this.suggestedListItems.unshift({value:e});else{if(!e||!r||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide.call(this);this.suggestedListItems=[{value:e}]}s=""+(o(i=this.suggestedListItems[0])?i.value:i),n.autoComplete&&s&&0==s.indexOf(e)&&this.input.autocomplete.suggest.call(this,i)}this.dropdown.fill.call(this,a),n.dropdown.highlightFirst&&this.dropdown.highlightOption.call(this,this.DOM.dropdown.content.children[0]),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=e||!0,this.state.dropdown.query=e,this.setStateSelection(),d||setTimeout((()=>{this.dropdown.position.call(this),this.dropdown.render.call(this)})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position.call(this,s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this},fill(t){var e;t="string"==typeof t?t:this.dropdown.createListHTML.call(this,t||this.suggestedListItems),this.DOM.dropdown.content.innerHTML=(e=t)?e.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g,((t,e)=>e||" ")):""},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems.call(this,t),this.dropdown.fill.call(this),this.suggestedListItems.length||this.dropdown.hide.call(this),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=document.documentElement.clientHeight,g=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>480?e.position:"all",c=this.DOM["input"==g?"input":"scope"];t=t||l.clientHeight,this.state.dropdown.visible&&("text"==g?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(this.settings.dropdown.appendTarget),s=(i=c.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),s=Math.floor(s),a=Math.ceil(a),d=void 0===d?h-i.bottom{e?this.dropdown.selectOption.call(this,e):this.dropdown.hide.call(this)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;let t=this.state.inputText.trim();""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption.call(this,e)},onMouseLeave(t){this.dropdown.highlightOption.call(this)},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode.call(this,e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption.call(this,e):this.dropdown.hide.call(this)})).catch((t=>t))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t?+t.getAttribute("tagifySuggestionIdx"):-1;return this.suggestedListItems[e]||null},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position.call(this))},selectOption(t){var e=this.settings.dropdown,i=e.clearOnSelect,s=e.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(s&&this.dropdown.hide.call(this));var a=t.getAttribute("tagifySuggestionIdx"),n=this.suggestedListItems[+a];this.trigger("dropdown:select",{data:n,elm:t}),a&&n?(this.state.editing?this.onEditTagDone(null,r({__isValid:!0},n)):this["mix"==this.settings.mode?"addMixTags":"addTags"]([n],i),setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),s?setTimeout(this.dropdown.hide.bind(this)):this.dropdown.refilter.call(this)):this.dropdown.hide.call(this)},selectAll(){return this.suggestedListItems.length=0,this.dropdown.hide.call(this),this.addTags(this.dropdown.filterListItems.call(this,""),!0),this},filterListItems(t,e){var i,s,a,n,r,d=this.settings,h=d.dropdown,g=(e=e||{},t="select"==d.mode&&this.value.length&&this.value[0][d.tagTextProp]==t?"":t,[]),c=d.whitelist,p=h.maxItems||1/0,u=h.searchKeys,m=0;if(!t||!u.length)return(d.duplicates?c:c.filter((t=>!this.isTagDuplicate(o(t)?t.value:t)))).slice(0,p);function v(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(r=h.caseSensitive?""+t:(""+t).toLowerCase();mu.includes(t)))?["value"]:u;if(h.fuzzySearch&&!e.exact?(a=t.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase(),h.accentedSearch&&(a=l(a),r=l(r)),s=v(a,r)):s=t.some((t=>{var s=""+(i[t]||"");return h.accentedSearch&&(s=l(s),r=l(r)),h.caseSensitive||(s=s.toLowerCase()),e.exact?s==r:0==s.indexOf(r)})),n=!d.duplicates&&this.isTagDuplicate(o(i)?i.value:i),s&&!n&&p--&&g.push(i),0==p)break}return g},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return r([],t).map(((t,e)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var i=this.dropdown.getMappedValue.call(this,t);t.value=i&&"string"==typeof i?a(i):i;var s=this.settings.templates.dropdownItem.call(this,t);return s=s.replace(/\s*tagifySuggestionIdx=(["'])(.*?)\1/gim,"").replace(">",` tagifySuggestionIdx="${e}">`)})).join("")}},c={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tag:"tagify__tag",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",inputInvalid:"tagify__input--invalid",tagX:"tagify__tag__removeBtn",tagText:"tagify__tag-text",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownInital:"tagify__dropdown--initial",scopeLoading:"tagify--loading",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}},p={wrapper:(t,e)=>`\n \n `,tag(t){return`\n \n
\n ${t[this.settings.tagTextProp]||t.value}\n
\n
`},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return`
\n
\n
`},dropdownItem(t){return`
${t.value}
`},dropdownItemNoMatch:null};var u={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(t=!0){var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t)for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",i[this.isIE?"onInputIE":"onInput"].bind(this)),window.addEventListener("keydown",i.onWindowKeyDown.bind(this)),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],blur:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)]})("blur"!=a||t)&&this.DOM[e[a][0]][s](a,e[a][1])},callbacks:{onFocusBlur(t){var e=t.target?this.trim(t.target.textContent):"",i=this.settings,s=t.type,a=i.dropdown.enabled>=0,n={relatedTarget:t.relatedTarget},o=this.state.actions.selectOption&&(a||!i.dropdown.closeOnSelect),r=this.state.actions.addNew&&a,l=t.relatedTarget&&t.relatedTarget.classList.contains(i.classNames.tag)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==s){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide.call(this),void this.DOM.input.focus();this.postUpdate(),this.triggerChangeEvent()}if(!o&&!r)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",n),void(0===i.dropdown.enabled&&this.dropdown.show.call(this));"blur"==s&&(this.trigger("blur",n),this.loading(!1),"select"==this.settings.mode&&l&&(e=""),("select"==this.settings.mode&&e?!this.value.length||this.value[0].value!=e:e&&!this.state.actions.selectOption&&i.addTagOnBlur)&&this.addTags(e,!0),"select"!=this.settings.mode||e||this.removeTags()),this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this)}else"focus"==s?this.trigger("focus",n):"blur"==t.type&&(this.trigger("blur",n),this.loading(!1),this.dropdown.hide.call(this),this.state.dropdown.visible=void 0,this.setStateSelection())},onWindowKeyDown(t){var e,i=document.activeElement;if(i.classList.contains(this.settings.classNames.tag)&&this.DOM.scope.contains(document.activeElement))switch(e=i.nextElementSibling,t.key){case"Backspace":this.removeTags(i),(e||this.DOM.input).focus();break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.trim(t.target.textContent);if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==this.settings.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var a,n,o,r=document.getSelection(),l="Delete"==t.key&&r.anchorOffset==(r.anchorNode.length||0),g=1==r.anchorNode.nodeType||!r.anchorOffset&&r.anchorNode.previousElementSibling,c=i(this.DOM.input.innerHTML),p=this.getTagElms();if("edit"==this.settings.backspace&&g)return a=1==r.anchorNode.nodeType?null:r.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,a),void t.preventDefault();if(d()&&g)return o=s(g),g.hasAttribute("readonly")||g.remove(),this.DOM.input.focus(),void setTimeout((()=>{this.placeCaretAfterNode(o),this.DOM.input.click()}));if("BR"==r.anchorNode.nodeName)return;if((l||g)&&1==r.anchorNode.nodeType?n=0==r.anchorOffset?l?p[0]:null:p[r.anchorOffset-1]:l?n=r.anchorNode.nextElementSibling:g&&(n=g),3==r.anchorNode.nodeType&&!r.anchorNode.nodeValue&&r.anchorNode.previousElementSibling&&t.preventDefault(),(g||l)&&!this.settings.backspace)return void t.preventDefault();if("Range"!=r.type&&!r.anchorOffset&&r.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=r.type&&n&&n.hasAttribute("readonly"))return void this.placeCaretAfterNode(s(n));clearTimeout(h),h=setTimeout((()=>{var t=document.getSelection(),e=i(this.DOM.input.innerHTML),s=t.anchorNode.previousElementSibling;if(!d()&&e.length>=c.length&&s&&!s.hasAttribute("readonly")&&(this.removeTags(s),this.fixFirefoxLastTagNoCaret(),2==this.DOM.input.children.length&&"BR"==this.DOM.input.children[1].tagName))return this.DOM.input.innerHTML="",this.value.length=0,!0;this.value=[].map.call(p,((t,e)=>{var i=this.tagData(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":this.state.dropdown.visible&&"manual"!=this.settings.dropdown.position||""!=e&&8203!=e.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show.call(this);break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&this.settings.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let i="select"==this.settings.mode;if(!e||i)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible||229==t.keyCode)return;t.preventDefault(),setTimeout((()=>{this.state.actions.selectOption||this.addTags(e,!0)}))}},onInput(t){if("mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};s.isValid=this.validateTag({value:e}),this.trigger("input",s),this.state.inputText!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):this.settings.dropdown.enabled>=0&&this.dropdown[i?"show":"hide"].call(this,e))},onMixTagsInput(t){var e,i,s,a,n,o,l,h,g=this.settings,c=this.value.length,p=this.getTagElms(),u=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>this.tagData(t).value));if("deleteContentBackward"==t.inputType&&d()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&u.appendChild(this.createTagElem(t))})),u.childNodes.length&&(m.insertNode(u),this.setRangeAtStartEnd(!1,u.lastChild)),p.length!=c)return this.value=[].map.call(this.getTagElms(),(t=>this.tagData(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(g.pattern).length-1,(i=e.match(g.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(g.pattern)[0],value:a.replace(g.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,h=this.state.tag.value.match(g.delimiters))return this.state.tag.value=this.state.tag.value.replace(g.delimiters,""),this.state.tag.delimiters=h[0],this.addTags(this.state.tag.value,g.dropdown.clearOnSelect),void this.dropdown.hide.call(this);n=this.state.tag.value.length>=g.dropdown.enabled;try{l=(l=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&l.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(l||s{this.update({withoutChangeEvent:!0}),this.trigger("input",r({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"].call(this,this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:this.tagData(i),originalEvent:this.cloneEvent(t)}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide.call(this):0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show.call(this):"select"==e.mode&&!this.state.dropdown.visible&&this.dropdown.show.call(this));this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste(t){var e,i;t.preventDefault(),this.settings.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),this.settings.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.addTags(this.DOM.input.textContent,!0))})).catch((t=>t)))},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o=i.innerHTML!=i.__tagifyTagData.__originalHTML,l=this.validateTag({[this.settings.tagTextProp]:n});o||!0!==t.originalIsValid||(l=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==l),a.__isValid=l,i.title=!0===l?a.title||a.value:l,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show.call(this,n)),this.trigger("edit:input",{tag:i,index:s,data:r({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i=this.settings,s=t.closest("."+i.classNames.tag),a=this.input.normalize.call(this,t),n=this.tagData(s).__originalData,o=s.innerHTML!=s.__tagifyTagData.__originalHTML,l=this.validateTag({[i.tagTextProp]:a});if(a)if(o){if(e=this.getWhitelistItem(a)||r({},n,{[i.tagTextProp]:a,value:a}),i.transformTag.call(this,e,n),!0!==(l=this.validateTag({[i.tagTextProp]:e[i.tagTextProp]}))){if(this.trigger("invalid",{data:e,tag:s,message:l}),i.editTags.keepInvalid)return;i.keepInvalidTags?e.__isValid=l:e=n}this.onEditTagDone(s,e)}else this.onEditTagDone(s,n);else this.onEditTagDone(s)}},onEditTagkeydown(t,e){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":e.innerHTML=e.__tagifyTagData.__originalHTML;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.settings;s&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==a.mode||a.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))}}};function m(t,e){return t?t.previousElementSibling&&t.previousElementSibling.classList.contains("tagify")?(console.warn("Tagify: ","input element is already Tagified",t),this):(r(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?r({},o):o).tagify=this,s instanceof Object)for(var l in s)s[l]instanceof HTMLElement&&(o[l]=s[l]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,this.applySettings(t,e||{}),this.state={inputText:"",editing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),void(t.autofocus&&this.DOM.input.focus())):(console.warn("Tagify: ","input element not found",t),this)}return m.prototype={dropdown:g,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},templates:p,parseTemplate(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},applySettings(t,e){c.templates=this.templates;var i=this.settings=r({},c,e);i.readonly=t.hasAttribute("readonly"),i.placeholder=t.getAttribute("placeholder")||i.placeholder||"",i.required=t.hasAttribute("required");for(let t in i.classNames)Object.defineProperty(i.classNames,t+"Selector",{get(){return"."+this[t].split(" ").join(".")}});if(this.isIE&&(i.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var s=t.getAttribute("data-"+e);s&&(s=s.split(i.delimiters))instanceof Array&&(i[e]=s)})),"autoComplete"in e&&!o(e.autoComplete)&&(i.autoComplete=c.autoComplete,i.autoComplete.enabled=e.autoComplete),"mix"==i.mode&&(i.autoComplete.rightKey=!0,i.delimiters=e.delimiters||null,i.tagTextProp&&!i.dropdown.searchKeys.includes(i.tagTextProp)&&i.dropdown.searchKeys.push(i.tagTextProp)),t.pattern)try{i.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings.delimiters)try{i.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}"select"==i.mode&&(i.dropdown.enabled=0),i.dropdown.appendTarget=e.dropdown&&e.dropdown.appendTarget?e.dropdown.appendTarget:document.body},getAttributes(t){if("[object Object]"!=Object.prototype.toString.call(t))return"";var e,i,s=Object.keys(t),a="";for(i=s.length;i--;)"class"!=(e=s[i])&&t.hasOwnProperty(e)&&void 0!==t[e]&&(a+=" "+e+(void 0!==t[e]?`="${t[e]}"`:""));return a},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCaretGlobalPosition(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(({value:t,unit:e})=>"s"==e?1e3*t:t)(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t)),this.settings.dropdown.enabled>=0&&this.dropdown.init.call(this)},destroy(){this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide.call(this,!0),clearTimeout(this.dropdownHide__bindEventsTimeout)},loadOriginalValues(t){var e,i=this.settings;if(void 0===t&&(t=i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value),this.removeAllTags({withoutChangeEvent:!0}),t)if("mix"==i.mode)this.parseMixTags(t.trim()),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","
");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value,this.state.loadedOriginalValues=!0},cloneEvent(t){var e={};for(var i in t)e[i]=t[i];return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:u,fixFirefoxLastTagNoCaret(){},placeCaretAfterNode(t){if(t&&t.parentNode){var e=t.nextSibling,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartBefore(e||t),s.setEndBefore(e||t),i.removeAllRanges(),i.addRange(s))}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide.call(this);var i=this.settings;function s(){return t.querySelector(i.classNames.tagTextSelector)}var a=s(),n=this.getNodeIndex(t),o=this.tagData(t),l=this.events.callbacks,d=this,h=!0;if(a){if(!(o instanceof Object&&"editable"in o)||o.editable)return a.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),this.tagData(t,{__originalData:r({},o),__originalHTML:t.innerHTML}),a.addEventListener("focus",l.onEditTagFocus.bind(this,t)),a.addEventListener("blur",(function(){setTimeout((()=>l.onEditTagBlur.call(d,s())))})),a.addEventListener("input",l.onEditTagInput.bind(this,a)),a.addEventListener("keydown",(e=>l.onEditTagkeydown.call(this,e,t))),a.focus(),this.setRangeAtStartEnd(!1,a),e.skipValidation||(h=this.editTagToggleValidity(t,o.value)),a.originalIsValid=h,this.trigger("edit:start",{tag:t,index:n,data:o,isValid:h}),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i,s=this.tagData(t);if(s)return i=!(!s.__isValid||1==s.__isValid),t.classList.toggle(this.settings.classNames.tagInvalid,i),s.__isValid;console.warn("tag has no data: ",t,s)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:this.tagData(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(this.editTagToggleValidity(t),t=this.replaceTag(t,e),this.settings.a11y.focusableTags&&t.focus()):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide.call(this),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&r(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(this.tagData(t))})),this.update()},setRangeAtStartEnd(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){console.warn("Tagify: ",t)}},injectAtCaret(t,e){if(e=e||this.state.selection.range)return"string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this},input:{set(t="",e=!0){var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=a(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.settings.trim&&(i=i.replace(/^\s+/,"")),i},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide.call(this),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(...t){var e="."+[...this.settings.classNames.tag.split(" "),...t].join(".");return[].slice.call(this.DOM.scope.querySelectorAll(e))},getLastTag(){var t=this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);return t[t.length-1]},tagData:(t,e,i)=>t?(e&&(t.__tagifyTagData=i?e:r({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag elment doesn't exist",t,e),e),isTagDuplicate(e,i){var s=this.settings;return"select"!=s.mode&&this.value.reduce(((a,n)=>t(this.trim(""+e),n.value,i||s.dropdown.caseSensitive)?a+1:a),0)},getTagIndexByValue(e){var i=[];return this.getTagElms().forEach(((s,a)=>{t(this.trim(s.textContent),e,this.settings.dropdown.caseSensitive)&&i.push(a)})),i},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(e,i,s){i=i||"value";var a,n=this.settings;return(s=s||n.whitelist).some((s=>{var o="string"==typeof s?s:s[i]||s.value;if(t(o,e,n.dropdown.caseSensitive,n.trim))return a="string"==typeof s?{value:s}:s,!0})),a||"value"!=i||"value"==n.tagTextProp||(a=this.getWhitelistItem(e,n.tagTextProp,s)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,this.state.editing)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t){var e=this.settings;document.activeElement.blur(),e.readonly=t,this.DOM.scope[(t?"set":"remove")+"Attribute"]("readonly",!0),"mix"==e.mode&&(this.DOM.input.contentEditable=!t)},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp;e.enforceWhitelist;var o=[],r=!!i&&i[0]instanceof Object,l=t instanceof Array,d=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=d(t)}else l&&(t=[].concat(...t.map((t=>t.value?t:d(t)))));return r&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0}).filter((t=>!e.includes(t.value))),s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),t=o),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];return t=t.split(i[0]).map(((t,e)=>{var d,h,g,c=t.split(i[1]),p=c[0],u=l.length==o;try{if(p==+p)throw Error;h=JSON.parse(p)}catch(t){h=this.normalizeTags(p)[0]||{value:p}}if(u||!(c.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else a.call(this,h),h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),c[0]=g.outerHTML,this.value.push(h);return c.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach(((t,e)=>this.tagData(t,l[e]))),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),i=n.nodeValue.lastIndexOf(e),s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0}},selectTag(t,e){if(!this.settings.enforceWhitelist||this.isTagWhitelisted(e.value))return this.input.set.call(this,e[this.settings.tagTextProp||"value"],!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this)),this.getLastTag()?this.replaceTag(this.getLastTag(),e):this.appendTag(t),this.value[0]=e,this.trigger("add",{tag:t,data:e}),this.update(),[t]},addEmptyTag(t){var e=r({value:""},t||{}),i=this.createTagElem(e);this.tagData(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i=this.settings.skipInvalid){var s=[],a=this.settings,n=document.createDocumentFragment();return t&&0!=t.length?(t=this.normalizeTags(t),"mix"==a.mode?this.addMixTags(t):("select"==a.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,o={},l=Object.assign({},t,{value:t.value+""});if((t=Object.assign({},l)).__isValid=this.hasMaxTags()||this.validateTag(t),a.transformTag.call(this,t),!0!==t.__isValid){if(i)return;r(o,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:l}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value))}if(t.readonly&&(o["aria-readonly"]=!0),e=this.createTagElem(t,o),s.push(e),"select"==a.mode)return this.selectTag(e,t);n.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.update(),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position.call(this)})),this.appendTag(n),t.length&&e&&this.input.set.call(this),this.dropdown.refilter.call(this),s)):("select"==a.mode&&this.removeAllTags(),s)},addMixTags(t){if(t[0].prefix||this.state.tag)this.prefixedTextToTag(t[0]);else{"string"==typeof t&&(t=[{value:t}]);var e=!!this.state.selection,i=document.createDocumentFragment();t.forEach((t=>{var e=this.createTagElem(t);i.appendChild(e),this.insertAfterTag(e)})),e?this.injectAtCaret(i):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(i),this.updateValueByDOMTags(),this.update())}},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;this.placeCaretAfterNode(a)}return this.state.tag=null,this.trigger("add",r({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.scope.lastElementChild;i===e.input?e.scope.insertBefore(t,i):e.scope.appendChild(t)},createTagElem(t,e){t.__tagId=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)));var i,s=r({},t,{value:a(t.value+"")});return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(i=this.parseTemplate("tag",[s])),this.tagData(i,t),i},reCheckInvalidTags(){var t=this.settings,e=`${t.classNames.tagSelector}${t.classNames.tagNotAllowedSelector}`,i=this.DOM.scope.querySelectorAll(e);[].forEach.call(i,(t=>{var e=this.tagData(t),i=t.getAttribute("title")==this.TEXTS.duplicate,s=!0===this.validateTag(e);i&&s&&(e=e.__preInvalidData?e.__preInvalidData:{value:e.value},this.replaceTag(t,e))}))},removeTags(t,e,i){var s;t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>(e&&"string"==typeof e&&(e=this.getTagElmByValue(e)),e&&t.push({node:e,idx:this.getTagIdx(this.tagData(e)),data:this.tagData(e,{__removed:!0})}),t)),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==this.settings.mode&&(i=0,this.input.set.call(this)),1==s.length&&s[0].node.classList.contains(this.settings.classNames.tagNotAllowed)&&(e=!0),s.length&&this.settings.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?this.settings.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter.call(this),this.dropdown.position.call(this),this.DOM.input.normalize(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(this.settings.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(s.forEach((t=>{var e=Object.assign({},t.data);delete e.__removed;var i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)})),this.update())})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.position.call(this),"select"==this.settings.mode&&this.input.set.call(this),this.update(t)},postUpdate(){var t=this.settings.classNames,e="mix"==this.settings.mode?this.settings.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value:this.value.length;this.toggleClass(t.hasMaxTags,this.value.length>=this.settings.maxTags),this.toggleClass(t.hasNoTags,!this.value.length),this.toggleClass(t.empty,!e)},update(t){var e=this.DOM.originalInput,i=(t||{}).withoutChangeEvent;this.settings.mixMode.integrated||(e.value=this.getInputValue()),this.postUpdate(),!i&&this.state.loadedOriginalValues&&this.triggerChangeEvent()},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return i=t||this.value,s=this.dataProps,i&&n(i)&&i.map((t=>e(t,s)));var i,s},getMixedTagsAsString(){var t="",i=this,s=this.settings.mixTagsInterpolator;return function a(n){n.childNodes.forEach((n=>{if(1==n.nodeType){if(n.classList.contains(i.settings.classNames.tag)&&i.tagData(n)){if(i.tagData(n).__removed)return;return void(t+=s[0]+JSON.stringify(e(i.tagData(n),i.dataProps))+s[1])}"BR"!=n.tagName||n.parentNode!=i.DOM.input&&1!=n.parentNode.childNodes.length?"DIV"!=n.tagName&&"P"!=n.tagName||(t+="\r\n",a(n)):t+="\r\n"}else t+=n.textContent}))}(this.DOM.input),t}},m.prototype.removeTag=m.prototype.removeTags,m})); + })(jQuery); \ No newline at end of file diff --git a/src/Jackett.Common/Content/libs/tagify.min.js b/src/Jackett.Common/Content/libs/tagify.min.js new file mode 100644 index 000000000..58127dd0e --- /dev/null +++ b/src/Jackett.Common/Content/libs/tagify.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";const t=(t,e,i,s)=>(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase());function e(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function i(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function s(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function a(t){return t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'")}function n(t){return t instanceof Array}function o(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function r(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(o(e[i])){o(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(n(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function l(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var d=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);var h,g={init(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownWrapperSelector)},show(e){var i,s,a,n=this.settings,r="mix"==n.mode&&!n.enforceWhitelist,l=!n.whitelist||!n.whitelist.length,d="manual"==n.dropdown.position;if(e=void 0===e?this.state.inputText:e,(!l||r||n.templates.dropdownItemNoMatch)&&!1!==n.dropdown.enable&&!this.state.isLoading){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems.call(this,e),e&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",e),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:e}))),!a){if(this.suggestedListItems.length)e&&r&&!this.state.editing.scope&&!t(this.suggestedListItems[0].value,e)&&this.suggestedListItems.unshift({value:e});else{if(!e||!r||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide.call(this);this.suggestedListItems=[{value:e}]}s=""+(o(i=this.suggestedListItems[0])?i.value:i),n.autoComplete&&s&&0==s.indexOf(e)&&this.input.autocomplete.suggest.call(this,i)}this.dropdown.fill.call(this,a),n.dropdown.highlightFirst&&this.dropdown.highlightOption.call(this,this.DOM.dropdown.content.children[0]),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=e||!0,this.state.dropdown.query=e,this.setStateSelection(),d||setTimeout((()=>{this.dropdown.position.call(this),this.dropdown.render.call(this)})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position.call(this,s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this},fill(t){var e;t="string"==typeof t?t:this.dropdown.createListHTML.call(this,t||this.suggestedListItems),this.DOM.dropdown.content.innerHTML=(e=t)?e.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g,((t,e)=>e||" ")):""},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems.call(this,t),this.dropdown.fill.call(this),this.suggestedListItems.length||this.dropdown.hide.call(this),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=document.documentElement.clientHeight,g=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>480?e.position:"all",c=this.DOM["input"==g?"input":"scope"];t=t||l.clientHeight,this.state.dropdown.visible&&("text"==g?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(this.settings.dropdown.appendTarget),s=(i=c.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),s=Math.floor(s),a=Math.ceil(a),d=void 0===d?h-i.bottom{e?this.dropdown.selectOption.call(this,e):this.dropdown.hide.call(this)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;let t=this.state.inputText.trim();""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption.call(this,e)},onMouseLeave(t){this.dropdown.highlightOption.call(this)},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode.call(this,e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption.call(this,e):this.dropdown.hide.call(this)})).catch((t=>t))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t?+t.getAttribute("tagifySuggestionIdx"):-1;return this.suggestedListItems[e]||null},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position.call(this))},selectOption(t){var e=this.settings.dropdown,i=e.clearOnSelect,s=e.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(s&&this.dropdown.hide.call(this));var a=t.getAttribute("tagifySuggestionIdx"),n=this.suggestedListItems[+a];this.trigger("dropdown:select",{data:n,elm:t}),a&&n?(this.state.editing?this.onEditTagDone(null,r({__isValid:!0},n)):this["mix"==this.settings.mode?"addMixTags":"addTags"]([n],i),setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),s?setTimeout(this.dropdown.hide.bind(this)):this.dropdown.refilter.call(this)):this.dropdown.hide.call(this)},selectAll(){return this.suggestedListItems.length=0,this.dropdown.hide.call(this),this.addTags(this.dropdown.filterListItems.call(this,""),!0),this},filterListItems(t,e){var i,s,a,n,r,d=this.settings,h=d.dropdown,g=(e=e||{},t="select"==d.mode&&this.value.length&&this.value[0][d.tagTextProp]==t?"":t,[]),c=d.whitelist,p=h.maxItems||1/0,u=h.searchKeys,m=0;if(!t||!u.length)return(d.duplicates?c:c.filter((t=>!this.isTagDuplicate(o(t)?t.value:t)))).slice(0,p);function v(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(r=h.caseSensitive?""+t:(""+t).toLowerCase();mu.includes(t)))?["value"]:u;if(h.fuzzySearch&&!e.exact?(a=t.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase(),h.accentedSearch&&(a=l(a),r=l(r)),s=v(a,r)):s=t.some((t=>{var s=""+(i[t]||"");return h.accentedSearch&&(s=l(s),r=l(r)),h.caseSensitive||(s=s.toLowerCase()),e.exact?s==r:0==s.indexOf(r)})),n=!d.duplicates&&this.isTagDuplicate(o(i)?i.value:i),s&&!n&&p--&&g.push(i),0==p)break}return g},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return r([],t).map(((t,e)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var i=this.dropdown.getMappedValue.call(this,t);t.value=i&&"string"==typeof i?a(i):i;var s=this.settings.templates.dropdownItem.call(this,t);return s=s.replace(/\s*tagifySuggestionIdx=(["'])(.*?)\1/gim,"").replace(">",` tagifySuggestionIdx="${e}">`)})).join("")}},c={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tag:"tagify__tag",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",inputInvalid:"tagify__input--invalid",tagX:"tagify__tag__removeBtn",tagText:"tagify__tag-text",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownInital:"tagify__dropdown--initial",scopeLoading:"tagify--loading",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}},p={wrapper:(t,e)=>`\n \n `,tag(t){return`\n \n
\n ${t[this.settings.tagTextProp]||t.value}\n
\n
`},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return`
\n
\n
`},dropdownItem(t){return`
${t.value}
`},dropdownItemNoMatch:null};var u={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(t=!0){var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t)for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",i[this.isIE?"onInputIE":"onInput"].bind(this)),window.addEventListener("keydown",i.onWindowKeyDown.bind(this)),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],blur:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)]})("blur"!=a||t)&&this.DOM[e[a][0]][s](a,e[a][1])},callbacks:{onFocusBlur(t){var e=t.target?this.trim(t.target.textContent):"",i=this.settings,s=t.type,a=i.dropdown.enabled>=0,n={relatedTarget:t.relatedTarget},o=this.state.actions.selectOption&&(a||!i.dropdown.closeOnSelect),r=this.state.actions.addNew&&a,l=t.relatedTarget&&t.relatedTarget.classList.contains(i.classNames.tag)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==s){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide.call(this),void this.DOM.input.focus();this.postUpdate(),this.triggerChangeEvent()}if(!o&&!r)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",n),void(0===i.dropdown.enabled&&this.dropdown.show.call(this));"blur"==s&&(this.trigger("blur",n),this.loading(!1),"select"==this.settings.mode&&l&&(e=""),("select"==this.settings.mode&&e?!this.value.length||this.value[0].value!=e:e&&!this.state.actions.selectOption&&i.addTagOnBlur)&&this.addTags(e,!0),"select"!=this.settings.mode||e||this.removeTags()),this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this)}else"focus"==s?this.trigger("focus",n):"blur"==t.type&&(this.trigger("blur",n),this.loading(!1),this.dropdown.hide.call(this),this.state.dropdown.visible=void 0,this.setStateSelection())},onWindowKeyDown(t){var e,i=document.activeElement;if(i.classList.contains(this.settings.classNames.tag)&&this.DOM.scope.contains(document.activeElement))switch(e=i.nextElementSibling,t.key){case"Backspace":this.removeTags(i),(e||this.DOM.input).focus();break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.trim(t.target.textContent);if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==this.settings.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var a,n,o,r=document.getSelection(),l="Delete"==t.key&&r.anchorOffset==(r.anchorNode.length||0),g=1==r.anchorNode.nodeType||!r.anchorOffset&&r.anchorNode.previousElementSibling,c=i(this.DOM.input.innerHTML),p=this.getTagElms();if("edit"==this.settings.backspace&&g)return a=1==r.anchorNode.nodeType?null:r.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,a),void t.preventDefault();if(d()&&g)return o=s(g),g.hasAttribute("readonly")||g.remove(),this.DOM.input.focus(),void setTimeout((()=>{this.placeCaretAfterNode(o),this.DOM.input.click()}));if("BR"==r.anchorNode.nodeName)return;if((l||g)&&1==r.anchorNode.nodeType?n=0==r.anchorOffset?l?p[0]:null:p[r.anchorOffset-1]:l?n=r.anchorNode.nextElementSibling:g&&(n=g),3==r.anchorNode.nodeType&&!r.anchorNode.nodeValue&&r.anchorNode.previousElementSibling&&t.preventDefault(),(g||l)&&!this.settings.backspace)return void t.preventDefault();if("Range"!=r.type&&!r.anchorOffset&&r.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=r.type&&n&&n.hasAttribute("readonly"))return void this.placeCaretAfterNode(s(n));clearTimeout(h),h=setTimeout((()=>{var t=document.getSelection(),e=i(this.DOM.input.innerHTML),s=t.anchorNode.previousElementSibling;if(!d()&&e.length>=c.length&&s&&!s.hasAttribute("readonly")&&(this.removeTags(s),this.fixFirefoxLastTagNoCaret(),2==this.DOM.input.children.length&&"BR"==this.DOM.input.children[1].tagName))return this.DOM.input.innerHTML="",this.value.length=0,!0;this.value=[].map.call(p,((t,e)=>{var i=this.tagData(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":this.state.dropdown.visible&&"manual"!=this.settings.dropdown.position||""!=e&&8203!=e.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show.call(this);break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&this.settings.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let i="select"==this.settings.mode;if(!e||i)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible||229==t.keyCode)return;t.preventDefault(),setTimeout((()=>{this.state.actions.selectOption||this.addTags(e,!0)}))}},onInput(t){if("mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};s.isValid=this.validateTag({value:e}),this.trigger("input",s),this.state.inputText!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):this.settings.dropdown.enabled>=0&&this.dropdown[i?"show":"hide"].call(this,e))},onMixTagsInput(t){var e,i,s,a,n,o,l,h,g=this.settings,c=this.value.length,p=this.getTagElms(),u=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>this.tagData(t).value));if("deleteContentBackward"==t.inputType&&d()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&u.appendChild(this.createTagElem(t))})),u.childNodes.length&&(m.insertNode(u),this.setRangeAtStartEnd(!1,u.lastChild)),p.length!=c)return this.value=[].map.call(this.getTagElms(),(t=>this.tagData(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(g.pattern).length-1,(i=e.match(g.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(g.pattern)[0],value:a.replace(g.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,h=this.state.tag.value.match(g.delimiters))return this.state.tag.value=this.state.tag.value.replace(g.delimiters,""),this.state.tag.delimiters=h[0],this.addTags(this.state.tag.value,g.dropdown.clearOnSelect),void this.dropdown.hide.call(this);n=this.state.tag.value.length>=g.dropdown.enabled;try{l=(l=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&l.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(l||s{this.update({withoutChangeEvent:!0}),this.trigger("input",r({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"].call(this,this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:this.tagData(i),originalEvent:this.cloneEvent(t)}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide.call(this):0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show.call(this):"select"==e.mode&&!this.state.dropdown.visible&&this.dropdown.show.call(this));this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste(t){var e,i;t.preventDefault(),this.settings.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),this.settings.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.addTags(this.DOM.input.textContent,!0))})).catch((t=>t)))},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o=i.innerHTML!=i.__tagifyTagData.__originalHTML,l=this.validateTag({[this.settings.tagTextProp]:n});o||!0!==t.originalIsValid||(l=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==l),a.__isValid=l,i.title=!0===l?a.title||a.value:l,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show.call(this,n)),this.trigger("edit:input",{tag:i,index:s,data:r({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i=this.settings,s=t.closest("."+i.classNames.tag),a=this.input.normalize.call(this,t),n=this.tagData(s).__originalData,o=s.innerHTML!=s.__tagifyTagData.__originalHTML,l=this.validateTag({[i.tagTextProp]:a});if(a)if(o){if(e=this.getWhitelistItem(a)||r({},n,{[i.tagTextProp]:a,value:a}),i.transformTag.call(this,e,n),!0!==(l=this.validateTag({[i.tagTextProp]:e[i.tagTextProp]}))){if(this.trigger("invalid",{data:e,tag:s,message:l}),i.editTags.keepInvalid)return;i.keepInvalidTags?e.__isValid=l:e=n}this.onEditTagDone(s,e)}else this.onEditTagDone(s,n);else this.onEditTagDone(s)}},onEditTagkeydown(t,e){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":e.innerHTML=e.__tagifyTagData.__originalHTML;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.settings;s&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==a.mode||a.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))}}};function m(t,e){return t?t.previousElementSibling&&t.previousElementSibling.classList.contains("tagify")?(console.warn("Tagify: ","input element is already Tagified",t),this):(r(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?r({},o):o).tagify=this,s instanceof Object)for(var l in s)s[l]instanceof HTMLElement&&(o[l]=s[l]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,this.applySettings(t,e||{}),this.state={inputText:"",editing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),void(t.autofocus&&this.DOM.input.focus())):(console.warn("Tagify: ","input element not found",t),this)}return m.prototype={dropdown:g,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},templates:p,parseTemplate(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},applySettings(t,e){c.templates=this.templates;var i=this.settings=r({},c,e);i.readonly=t.hasAttribute("readonly"),i.placeholder=t.getAttribute("placeholder")||i.placeholder||"",i.required=t.hasAttribute("required");for(let t in i.classNames)Object.defineProperty(i.classNames,t+"Selector",{get(){return"."+this[t].split(" ").join(".")}});if(this.isIE&&(i.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var s=t.getAttribute("data-"+e);s&&(s=s.split(i.delimiters))instanceof Array&&(i[e]=s)})),"autoComplete"in e&&!o(e.autoComplete)&&(i.autoComplete=c.autoComplete,i.autoComplete.enabled=e.autoComplete),"mix"==i.mode&&(i.autoComplete.rightKey=!0,i.delimiters=e.delimiters||null,i.tagTextProp&&!i.dropdown.searchKeys.includes(i.tagTextProp)&&i.dropdown.searchKeys.push(i.tagTextProp)),t.pattern)try{i.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings.delimiters)try{i.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}"select"==i.mode&&(i.dropdown.enabled=0),i.dropdown.appendTarget=e.dropdown&&e.dropdown.appendTarget?e.dropdown.appendTarget:document.body},getAttributes(t){if("[object Object]"!=Object.prototype.toString.call(t))return"";var e,i,s=Object.keys(t),a="";for(i=s.length;i--;)"class"!=(e=s[i])&&t.hasOwnProperty(e)&&void 0!==t[e]&&(a+=" "+e+(void 0!==t[e]?`="${t[e]}"`:""));return a},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCaretGlobalPosition(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(({value:t,unit:e})=>"s"==e?1e3*t:t)(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t)),this.settings.dropdown.enabled>=0&&this.dropdown.init.call(this)},destroy(){this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide.call(this,!0),clearTimeout(this.dropdownHide__bindEventsTimeout)},loadOriginalValues(t){var e,i=this.settings;if(void 0===t&&(t=i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value),this.removeAllTags({withoutChangeEvent:!0}),t)if("mix"==i.mode)this.parseMixTags(t.trim()),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","
");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value,this.state.loadedOriginalValues=!0},cloneEvent(t){var e={};for(var i in t)e[i]=t[i];return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:u,fixFirefoxLastTagNoCaret(){},placeCaretAfterNode(t){if(t&&t.parentNode){var e=t.nextSibling,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartBefore(e||t),s.setEndBefore(e||t),i.removeAllRanges(),i.addRange(s))}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide.call(this);var i=this.settings;function s(){return t.querySelector(i.classNames.tagTextSelector)}var a=s(),n=this.getNodeIndex(t),o=this.tagData(t),l=this.events.callbacks,d=this,h=!0;if(a){if(!(o instanceof Object&&"editable"in o)||o.editable)return a.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),this.tagData(t,{__originalData:r({},o),__originalHTML:t.innerHTML}),a.addEventListener("focus",l.onEditTagFocus.bind(this,t)),a.addEventListener("blur",(function(){setTimeout((()=>l.onEditTagBlur.call(d,s())))})),a.addEventListener("input",l.onEditTagInput.bind(this,a)),a.addEventListener("keydown",(e=>l.onEditTagkeydown.call(this,e,t))),a.focus(),this.setRangeAtStartEnd(!1,a),e.skipValidation||(h=this.editTagToggleValidity(t,o.value)),a.originalIsValid=h,this.trigger("edit:start",{tag:t,index:n,data:o,isValid:h}),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i,s=this.tagData(t);if(s)return i=!(!s.__isValid||1==s.__isValid),t.classList.toggle(this.settings.classNames.tagInvalid,i),s.__isValid;console.warn("tag has no data: ",t,s)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:this.tagData(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(this.editTagToggleValidity(t),t=this.replaceTag(t,e),this.settings.a11y.focusableTags&&t.focus()):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide.call(this),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&r(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(this.tagData(t))})),this.update()},setRangeAtStartEnd(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){console.warn("Tagify: ",t)}},injectAtCaret(t,e){if(e=e||this.state.selection.range)return"string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this},input:{set(t="",e=!0){var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=a(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.settings.trim&&(i=i.replace(/^\s+/,"")),i},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide.call(this),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(...t){var e="."+[...this.settings.classNames.tag.split(" "),...t].join(".");return[].slice.call(this.DOM.scope.querySelectorAll(e))},getLastTag(){var t=this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);return t[t.length-1]},tagData:(t,e,i)=>t?(e&&(t.__tagifyTagData=i?e:r({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag elment doesn't exist",t,e),e),isTagDuplicate(e,i){var s=this.settings;return"select"!=s.mode&&this.value.reduce(((a,n)=>t(this.trim(""+e),n.value,i||s.dropdown.caseSensitive)?a+1:a),0)},getTagIndexByValue(e){var i=[];return this.getTagElms().forEach(((s,a)=>{t(this.trim(s.textContent),e,this.settings.dropdown.caseSensitive)&&i.push(a)})),i},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(e,i,s){i=i||"value";var a,n=this.settings;return(s=s||n.whitelist).some((s=>{var o="string"==typeof s?s:s[i]||s.value;if(t(o,e,n.dropdown.caseSensitive,n.trim))return a="string"==typeof s?{value:s}:s,!0})),a||"value"!=i||"value"==n.tagTextProp||(a=this.getWhitelistItem(e,n.tagTextProp,s)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,this.state.editing)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t){var e=this.settings;document.activeElement.blur(),e.readonly=t,this.DOM.scope[(t?"set":"remove")+"Attribute"]("readonly",!0),"mix"==e.mode&&(this.DOM.input.contentEditable=!t)},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp;e.enforceWhitelist;var o=[],r=!!i&&i[0]instanceof Object,l=t instanceof Array,d=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=d(t)}else l&&(t=[].concat(...t.map((t=>t.value?t:d(t)))));return r&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0}).filter((t=>!e.includes(t.value))),s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),t=o),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];return t=t.split(i[0]).map(((t,e)=>{var d,h,g,c=t.split(i[1]),p=c[0],u=l.length==o;try{if(p==+p)throw Error;h=JSON.parse(p)}catch(t){h=this.normalizeTags(p)[0]||{value:p}}if(u||!(c.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else a.call(this,h),h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),c[0]=g.outerHTML,this.value.push(h);return c.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach(((t,e)=>this.tagData(t,l[e]))),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),i=n.nodeValue.lastIndexOf(e),s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0}},selectTag(t,e){if(!this.settings.enforceWhitelist||this.isTagWhitelisted(e.value))return this.input.set.call(this,e[this.settings.tagTextProp||"value"],!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this)),this.getLastTag()?this.replaceTag(this.getLastTag(),e):this.appendTag(t),this.value[0]=e,this.trigger("add",{tag:t,data:e}),this.update(),[t]},addEmptyTag(t){var e=r({value:""},t||{}),i=this.createTagElem(e);this.tagData(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i=this.settings.skipInvalid){var s=[],a=this.settings,n=document.createDocumentFragment();return t&&0!=t.length?(t=this.normalizeTags(t),"mix"==a.mode?this.addMixTags(t):("select"==a.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,o={},l=Object.assign({},t,{value:t.value+""});if((t=Object.assign({},l)).__isValid=this.hasMaxTags()||this.validateTag(t),a.transformTag.call(this,t),!0!==t.__isValid){if(i)return;r(o,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:l}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value))}if(t.readonly&&(o["aria-readonly"]=!0),e=this.createTagElem(t,o),s.push(e),"select"==a.mode)return this.selectTag(e,t);n.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.update(),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position.call(this)})),this.appendTag(n),t.length&&e&&this.input.set.call(this),this.dropdown.refilter.call(this),s)):("select"==a.mode&&this.removeAllTags(),s)},addMixTags(t){if(t[0].prefix||this.state.tag)this.prefixedTextToTag(t[0]);else{"string"==typeof t&&(t=[{value:t}]);var e=!!this.state.selection,i=document.createDocumentFragment();t.forEach((t=>{var e=this.createTagElem(t);i.appendChild(e),this.insertAfterTag(e)})),e?this.injectAtCaret(i):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(i),this.updateValueByDOMTags(),this.update())}},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;this.placeCaretAfterNode(a)}return this.state.tag=null,this.trigger("add",r({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.scope.lastElementChild;i===e.input?e.scope.insertBefore(t,i):e.scope.appendChild(t)},createTagElem(t,e){t.__tagId=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)));var i,s=r({},t,{value:a(t.value+"")});return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(i=this.parseTemplate("tag",[s])),this.tagData(i,t),i},reCheckInvalidTags(){var t=this.settings,e=`${t.classNames.tagSelector}${t.classNames.tagNotAllowedSelector}`,i=this.DOM.scope.querySelectorAll(e);[].forEach.call(i,(t=>{var e=this.tagData(t),i=t.getAttribute("title")==this.TEXTS.duplicate,s=!0===this.validateTag(e);i&&s&&(e=e.__preInvalidData?e.__preInvalidData:{value:e.value},this.replaceTag(t,e))}))},removeTags(t,e,i){var s;t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>(e&&"string"==typeof e&&(e=this.getTagElmByValue(e)),e&&t.push({node:e,idx:this.getTagIdx(this.tagData(e)),data:this.tagData(e,{__removed:!0})}),t)),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==this.settings.mode&&(i=0,this.input.set.call(this)),1==s.length&&s[0].node.classList.contains(this.settings.classNames.tagNotAllowed)&&(e=!0),s.length&&this.settings.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?this.settings.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter.call(this),this.dropdown.position.call(this),this.DOM.input.normalize(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(this.settings.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(s.forEach((t=>{var e=Object.assign({},t.data);delete e.__removed;var i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)})),this.update())})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.position.call(this),"select"==this.settings.mode&&this.input.set.call(this),this.update(t)},postUpdate(){var t=this.settings.classNames,e="mix"==this.settings.mode?this.settings.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value:this.value.length;this.toggleClass(t.hasMaxTags,this.value.length>=this.settings.maxTags),this.toggleClass(t.hasNoTags,!this.value.length),this.toggleClass(t.empty,!e)},update(t){var e=this.DOM.originalInput,i=(t||{}).withoutChangeEvent;this.settings.mixMode.integrated||(e.value=this.getInputValue()),this.postUpdate(),!i&&this.state.loadedOriginalValues&&this.triggerChangeEvent()},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return i=t||this.value,s=this.dataProps,i&&n(i)&&i.map((t=>e(t,s)));var i,s},getMixedTagsAsString(){var t="",i=this,s=this.settings.mixTagsInterpolator;return function a(n){n.childNodes.forEach((n=>{if(1==n.nodeType){if(n.classList.contains(i.settings.classNames.tag)&&i.tagData(n)){if(i.tagData(n).__removed)return;return void(t+=s[0]+JSON.stringify(e(i.tagData(n),i.dataProps))+s[1])}"BR"!=n.tagName||n.parentNode!=i.DOM.input&&1!=n.parentNode.childNodes.length?"DIV"!=n.tagName&&"P"!=n.tagName||(t+="\r\n",a(n)):t+="\r\n"}else t+=n.textContent}))}(this.DOM.input),t}},m.prototype.removeTag=m.prototype.removeTags,m})); diff --git a/src/Jackett.Common/Indexers/BaseIndexer.cs b/src/Jackett.Common/Indexers/BaseIndexer.cs index cd7526f87..58393685d 100644 --- a/src/Jackett.Common/Indexers/BaseIndexer.cs +++ b/src/Jackett.Common/Indexers/BaseIndexer.cs @@ -34,6 +34,8 @@ namespace Jackett.Common.Indexers public Encoding Encoding { get; protected set; } public virtual bool IsConfigured { get; protected set; } + public virtual string[] Tags { get; protected set; } + protected Logger logger; protected IIndexerConfigurationService configurationService; protected IProtectionService protectionService; @@ -148,6 +150,8 @@ namespace Jackett.Common.Indexers // check whether the site link is well-formatted var siteUri = new Uri(configData.SiteLink.Value); SiteLink = configData.SiteLink.Value; + + Tags = configData.Tags.Values.Select(t => t.ToLowerInvariant()).ToArray(); } public void LoadFromSavedConfiguration(JToken jsonConfig) diff --git a/src/Jackett.Common/Indexers/IIndexer.cs b/src/Jackett.Common/Indexers/IIndexer.cs index 484839171..0d9227102 100644 --- a/src/Jackett.Common/Indexers/IIndexer.cs +++ b/src/Jackett.Common/Indexers/IIndexer.cs @@ -40,6 +40,8 @@ namespace Jackett.Common.Indexers // Whether this indexer has been configured, verified and saved in the past and has the settings required for functioning bool IsConfigured { get; } + string[] Tags { get; } + // Retrieved for starting setup for the indexer via web API Task GetConfigurationForSetup(); diff --git a/src/Jackett.Common/Indexers/Meta/BaseMetaIndexer.cs b/src/Jackett.Common/Indexers/Meta/BaseMetaIndexer.cs index 87690dbac..846dfb0e9 100644 --- a/src/Jackett.Common/Indexers/Meta/BaseMetaIndexer.cs +++ b/src/Jackett.Common/Indexers/Meta/BaseMetaIndexer.cs @@ -67,7 +67,7 @@ namespace Jackett.Common.Indexers.Meta protected override async Task> PerformQuery(TorznabQuery query) { - var indexers = validIndexers; + var indexers = ValidIndexers; IEnumerable> supportedTasks = indexers.Where(i => i.CanHandleQuery(query)).Select(i => i.ResultsForQuery(query, true)).ToList(); // explicit conversion to List to execute LINQ query var fallbackStrategies = fallbackStrategyProvider.FallbackStrategiesForQuery(query); @@ -109,11 +109,13 @@ namespace Jackett.Common.Indexers.Meta return result; } - public override TorznabCapabilities TorznabCaps => validIndexers.Select(i => i.TorznabCaps).Aggregate(new TorznabCapabilities(), TorznabCapabilities.Concat); + public override TorznabCapabilities TorznabCaps => ValidIndexers.Select(i => i.TorznabCaps).Aggregate(new TorznabCapabilities(), TorznabCapabilities.Concat); public override bool IsConfigured => Indexers != null; - private IEnumerable validIndexers => Indexers?.Where(i => i.IsConfigured && filterFunc(i)); + public override string[] Tags => Array.Empty(); + + public IEnumerable ValidIndexers => Indexers?.Where(i => i.IsConfigured && filterFunc(i)); public IEnumerable Indexers; diff --git a/src/Jackett.Common/Indexers/Meta/MetaIndexers.cs b/src/Jackett.Common/Indexers/Meta/MetaIndexers.cs index bd8459b12..d5b2fe639 100644 --- a/src/Jackett.Common/Indexers/Meta/MetaIndexers.cs +++ b/src/Jackett.Common/Indexers/Meta/MetaIndexers.cs @@ -1,7 +1,10 @@ +using System; +using System.Linq; using Jackett.Common.Models; using Jackett.Common.Models.IndexerConfig; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils.Clients; + using NLog; namespace Jackett.Common.Indexers.Meta @@ -37,4 +40,41 @@ namespace Jackett.Common.Indexers.Meta } } } + + public class FilterIndexer : BaseMetaIndexer + { + public FilterIndexer(string filter, IFallbackStrategyProvider fallbackStrategyProvider, + IResultFilterProvider resultFilterProvider, IIndexerConfigurationService configService, + WebClient client, Logger logger, IProtectionService ps, ICacheService cs, Func filterFunc) + : base(id: filter, + name: filter, + description: "This feed includes all configured trackers filter by "+filter, + configService: configService, + client: client, + logger: logger, + ps: ps, + cs: cs, + configData: new ConfigurationData(), + fallbackStrategyProvider: fallbackStrategyProvider, + resultFilterProvider: resultFilterProvider, + filter: filterFunc + ) + { + } + + public override TorznabCapabilities TorznabCaps + { + get + { + // increase the limits (workaround until proper paging is supported, issue #1661) + var caps = base.TorznabCaps; + caps.LimitsMax = caps.LimitsDefault = 1000; + return caps; + } + } + + public override bool IsConfigured => base.IsConfigured && (ValidIndexers?.Any() ?? false); + + public override void SaveConfig() { } + } } diff --git a/src/Jackett.Common/Jackett.Common.csproj b/src/Jackett.Common/Jackett.Common.csproj index 3fef61ff9..7dd3d4789 100644 --- a/src/Jackett.Common/Jackett.Common.csproj +++ b/src/Jackett.Common/Jackett.Common.csproj @@ -58,6 +58,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -124,9 +127,15 @@ PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Jackett.Common/Models/DTO/Indexer.cs b/src/Jackett.Common/Models/DTO/Indexer.cs index 8c4e15cb2..bc822c493 100644 --- a/src/Jackett.Common/Models/DTO/Indexer.cs +++ b/src/Jackett.Common/Models/DTO/Indexer.cs @@ -34,6 +34,8 @@ namespace Jackett.Common.Models.DTO [DataMember] public string language { get; private set; } [DataMember] + public IEnumerable tags { get; private set; } + [DataMember] public string last_error { get; private set; } [DataMember] public bool potatoenabled { get; private set; } @@ -55,6 +57,8 @@ namespace Jackett.Common.Models.DTO alternativesitelinks = indexer.AlternativeSiteLinks; + tags = indexer.Tags; + caps = indexer.TorznabCaps.Categories.GetTorznabCategoryList(true) .Select(c => new Capability { diff --git a/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs b/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs index afc539140..17de59a63 100644 --- a/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs +++ b/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; + using Newtonsoft.Json.Linq; namespace Jackett.Common.Models.IndexerConfig @@ -12,9 +14,10 @@ namespace Jackett.Common.Models.IndexerConfig private const string PASSWORD_REPLACEMENT = "|||%%PREVJACKPASSWD%%|||"; protected Dictionary dynamics = new Dictionary(); // list for dynamic items - public HiddenStringConfigurationItem CookieHeader { get; private set; } = new HiddenStringConfigurationItem(name:"CookieHeader"); - public HiddenStringConfigurationItem LastError { get; private set; } = new HiddenStringConfigurationItem(name:"LastError"); - public StringConfigurationItem SiteLink { get; private set; } = new StringConfigurationItem(name:"Site Link"); + public HiddenStringConfigurationItem CookieHeader { get; private set; } = new HiddenStringConfigurationItem(name: "CookieHeader"); + public HiddenStringConfigurationItem LastError { get; private set; } = new HiddenStringConfigurationItem(name: "LastError"); + public StringConfigurationItem SiteLink { get; private set; } = new StringConfigurationItem(name: "Site Link"); + public TagsConfigurationItem Tags { get; private set; } = new TagsConfigurationItem(name: "Tags", charSet:"A-Za-z0-9\\-\\._~"); public ConfigurationData() { @@ -36,67 +39,10 @@ namespace Jackett.Common.Models.IndexerConfig var jsonToken = jsonArray.FirstOrDefault(f => f.Value("id") == item.ID); if (jsonToken == null) continue; - - switch (item) - { - case StringConfigurationItem stringItem: - { - if (HasPasswordValue(item)) - { - var pw = ReadValueAs(jsonToken); - if (pw != PASSWORD_REPLACEMENT) - { - stringItem.Value = ps != null ? ps.UnProtect(pw) : pw; - } - } - else - { - stringItem.Value = ReadValueAs(jsonToken); - } - break; - } - case HiddenStringConfigurationItem hiddenStringItem: - { - hiddenStringItem.Value = ReadValueAs(jsonToken); - break; - } - case BoolConfigurationItem boolItem: - { - boolItem.Value = ReadValueAs(jsonToken); - break; - } - case SingleSelectConfigurationItem singleSelectItem: - { - singleSelectItem.Value = ReadValueAs(jsonToken); - break; - } - case MultiSelectConfigurationItem multiSelectItem: - { - var values = jsonToken.Value("values"); - if (values != null) - { - multiSelectItem.Values = values.Values().ToArray(); - } - break; - } - case PasswordConfigurationItem passwordItem: - { - var pw = ReadValueAs(jsonToken); - if (pw != PASSWORD_REPLACEMENT) - { - passwordItem.Value = ps != null ? ps.UnProtect(pw) : pw; - } - break; - } - } + item.FromJson(jsonToken, ps); } } - private T ReadValueAs(JToken jToken) => jToken.Value("value"); - - private bool HasPasswordValue(ConfigurationItem item) - => string.Equals(item.Name, "password", StringComparison.InvariantCultureIgnoreCase); - public JToken ToJson(IProtectionService ps, bool forDisplay = true) { var jArray = new JArray(); @@ -104,43 +50,7 @@ namespace Jackett.Common.Models.IndexerConfig var configurationItems = GetConfigurationItems(forDisplay); foreach (var configurationItem in configurationItems) { - JObject jObject = null; - - switch (configurationItem) - { - case ConfigurationItemMaybePassword maybePassword: - { - // Remove this code and give each derived ConfigurationItem class its own ToJson method - // as soon as everyone is using PasswordConfigurationItem for passwords. - jObject = maybePassword.ToJson(ps); - break; - } - case BoolConfigurationItem boolItem: - { - jObject = boolItem.ToJson(); - break; - } - case SingleSelectConfigurationItem singleSelectItem: - { - jObject = singleSelectItem.ToJson(); - break; - } - case MultiSelectConfigurationItem multiSelectItem: - { - jObject = multiSelectItem.ToJson(); - break; - } - case DisplayImageConfigurationItem imageItem: - { - jObject = imageItem.ToJson(); - break; - } - case PasswordConfigurationItem passwordItem: - { - jObject = passwordItem.ToJson(forDisplay, ps); - break; - } - } + var jObject = configurationItem.ToJson(ps, forDisplay); if (jObject != null) { @@ -163,8 +73,13 @@ namespace Jackett.Common.Models.IndexerConfig properties.Remove(SiteLink); properties.Insert(0, SiteLink); + // remove/insert Tags manualy to make sure it shows up last + properties.Remove(Tags); + properties.AddRange(dynamics.Values); + properties.Add(Tags); + return properties; } @@ -204,6 +119,14 @@ namespace Jackett.Common.Models.IndexerConfig ["name"] = Name }; } + + protected static T ReadValueAs(JToken jToken) => jToken.Value("value"); + + protected static bool HasPasswordValue(ConfigurationItem item) + => string.Equals(item.Name, "password", StringComparison.InvariantCultureIgnoreCase); + + public virtual JObject ToJson(IProtectionService protectionService = null, bool forDisplay = true) => null; + public virtual void FromJson(JToken jsonToken, IProtectionService protectionService = null) { } } /// @@ -218,7 +141,7 @@ namespace Jackett.Common.Models.IndexerConfig { } - public JObject ToJson(IProtectionService protectionService = null) + public override JObject ToJson(IProtectionService protectionService = null, bool forDisplay = true) { var jObject = CreateJObject(); @@ -245,6 +168,22 @@ namespace Jackett.Common.Models.IndexerConfig : base(name, itemType: "inputstring") { } + + public override void FromJson(JToken jsonToken, IProtectionService ps = null) + { + if (HasPasswordValue(this)) + { + var pw = ReadValueAs(jsonToken); + if (pw != PASSWORD_REPLACEMENT) + { + Value = ps != null ? ps.UnProtect(pw) : pw; + } + } + else + { + Value = ReadValueAs(jsonToken); + } + } } public class HiddenStringConfigurationItem : ConfigurationItemMaybePassword @@ -253,6 +192,11 @@ namespace Jackett.Common.Models.IndexerConfig : base(name, itemType: "hiddendata", canBeShownToUser: false) { } + + public override void FromJson(JToken jsonToken, IProtectionService ps = null) + { + Value = ReadValueAs(jsonToken); + } } public class DisplayInfoConfigurationItem : ConfigurationItemMaybePassword @@ -273,12 +217,17 @@ namespace Jackett.Common.Models.IndexerConfig { } - public JObject ToJson() + public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true) { var jObject = CreateJObject(); jObject["value"] = Value; return jObject; } + + public override void FromJson(JToken jsonToken, IProtectionService ps = null) + { + Value = ReadValueAs(jsonToken); + } } public class DisplayImageConfigurationItem : ConfigurationItem @@ -290,7 +239,7 @@ namespace Jackett.Common.Models.IndexerConfig { } - public JObject ToJson() + public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true) { var jObject = CreateJObject(); @@ -310,7 +259,7 @@ namespace Jackett.Common.Models.IndexerConfig public SingleSelectConfigurationItem(string name, Dictionary options) : base(name, itemType: "inputselect") => Options = options; - public JObject ToJson() + public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true) { var jObject = CreateJObject(); @@ -323,6 +272,11 @@ namespace Jackett.Common.Models.IndexerConfig return jObject; } + + public override void FromJson(JToken jsonToken, IProtectionService ps = null) + { + Value = ReadValueAs(jsonToken); + } } public class MultiSelectConfigurationItem : ConfigurationItem @@ -334,7 +288,7 @@ namespace Jackett.Common.Models.IndexerConfig public MultiSelectConfigurationItem(string name, Dictionary options) : base(name, itemType: "inputcheckbox") => Options = options; - public JObject ToJson() + public override JObject ToJson(IProtectionService ps, bool forDisplay) { var jObject = CreateJObject(); @@ -347,6 +301,15 @@ namespace Jackett.Common.Models.IndexerConfig return jObject; } + + public override void FromJson(JToken jsonToken, IProtectionService ps = null) + { + var values = jsonToken.Value("values"); + if (values != null) + { + Values = values.Values().ToArray(); + } + } } public class PasswordConfigurationItem : ConfigurationItem @@ -358,7 +321,7 @@ namespace Jackett.Common.Models.IndexerConfig { } - public JObject ToJson(bool forDisplay, IProtectionService protectionService = null) + public override JObject ToJson(IProtectionService protectionService = null, bool forDisplay = true) { var jObject = CreateJObject(); @@ -373,6 +336,76 @@ namespace Jackett.Common.Models.IndexerConfig return jObject; } + + public override void FromJson(JToken jsonToken, IProtectionService ps = null) + { + var pw = ReadValueAs(jsonToken); + if (pw != PASSWORD_REPLACEMENT) + { + Value = ps != null ? ps.UnProtect(pw) : pw; + } + } + } + + public class TagsConfigurationItem : ConfigurationItem + { + public HashSet Values { get; } + public string Pattern { get; set; } + public char Separator { get; set; } + public string Delimiters { get; set; } + + public HashSet Whitelist { get; } + public HashSet Blacklist { get; } + + public TagsConfigurationItem(string name, string charSet = null, char separator = ',') + : base(name, "inputtags") + { + Values = new HashSet(); + Whitelist = new HashSet(); + Blacklist = new HashSet(); + if (!string.IsNullOrWhiteSpace(charSet)) + { + Pattern = $"^[{charSet}]+$"; + Delimiters = $"[^{charSet}]+"; + } + Separator = separator; + } + + public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true) + { + var jObject = CreateJObject(); + var separator = Separator.ToString(); + jObject["value"] = string.Join(separator, Values); + if (forDisplay) + { + jObject["separator"] = separator; + if (!string.IsNullOrWhiteSpace(Delimiters)) + jObject["delimiters"] = Delimiters; + if (!string.IsNullOrWhiteSpace(Pattern)) + jObject["pattern"] = Pattern; + if (Whitelist.Count > 0) + jObject["whitelist"] = string.Join(separator, Whitelist); + if (Blacklist.Count > 0) + jObject["blacklist"] = string.Join(separator, Blacklist); + } + + return jObject; + } + + public override void FromJson(JToken jsonToken, IProtectionService ps) + { + var value = ReadValueAs(jsonToken); + if (value == null) + return; + Values.Clear(); + var tags = Regex.Split(value, !string.IsNullOrWhiteSpace(Delimiters) ? Delimiters : $"{Separator}+").Select(t => t.Trim().ToLowerInvariant()); + if (!string.IsNullOrWhiteSpace(Pattern)) + tags = tags.Where(t => Whitelist.Contains(t) || Regex.IsMatch(t, Pattern)); + if (Blacklist.Count > 0) + tags = tags.Where(t => !Blacklist.Contains(t)); + foreach (var tag in tags) + Values.Add(tag); + } } } } diff --git a/src/Jackett.Common/Services/IndexerManagerService.cs b/src/Jackett.Common/Services/IndexerManagerService.cs index a5a777b11..00e082267 100644 --- a/src/Jackett.Common/Services/IndexerManagerService.cs +++ b/src/Jackett.Common/Services/IndexerManagerService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,10 +9,12 @@ using Jackett.Common.Indexers.Meta; using Jackett.Common.Models; using Jackett.Common.Models.Config; using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; using Jackett.Common.Utils.Clients; using NLog; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using FilterFunc = Jackett.Common.Utils.FilterFunc; namespace Jackett.Common.Services { @@ -29,6 +32,7 @@ namespace Jackett.Common.Services private readonly Dictionary indexers = new Dictionary(); private AggregateIndexer aggregateIndexer; + private ConcurrentDictionary availableFilters = new ConcurrentDictionary(); // this map is used to maintain backward compatibility when renaming the id of an indexer // (the id is used in the torznab/download/search urls and in the indexer configuration file) @@ -79,7 +83,7 @@ namespace Jackett.Common.Services MigrateRenamedIndexers(); InitIndexers(); InitCardigannIndexers(path); - InitAggregateIndexer(); + InitMetaIndexers(); RemoveLegacyConfigurations(); } @@ -218,28 +222,25 @@ namespace Jackett.Common.Services logger.Info($"Loaded {indexers.Count} indexers in total"); } - public void InitAggregateIndexer() + public void InitMetaIndexers() { - var omdbApiKey = serverConfig.OmdbApiKey; - IFallbackStrategyProvider fallbackStrategyProvider; - IResultFilterProvider resultFilterProvider; - if (!string.IsNullOrWhiteSpace(omdbApiKey)) - { - var imdbResolver = new OmdbResolver(webClient, omdbApiKey, serverConfig.OmdbApiUrl); - fallbackStrategyProvider = new ImdbFallbackStrategyProvider(imdbResolver); - resultFilterProvider = new ImdbTitleResultFilterProvider(imdbResolver); - } - else - { - fallbackStrategyProvider = new NoFallbackStrategyProvider(); - resultFilterProvider = new NoResultFilterProvider(); - } + var (fallbackStrategyProvider, resultFilterProvider) = GetStrategyProviders(); logger.Info("Adding aggregate indexer ('all' indexer) ..."); aggregateIndexer = new AggregateIndexer(fallbackStrategyProvider, resultFilterProvider, configService, webClient, logger, protectionService, cacheService) { Indexers = indexers.Values }; + + var predefinedFilters = + new[] { "public", "private", "semi-public" } + .Select(type => (filter: FilterFunc.Type.ToFilter(type), func: FilterFunc.Type.ToFunc(type))) + .Concat( + indexers.Values.SelectMany(x => x.Tags).Distinct() + .Select(tag => (filter: FilterFunc.Tag.ToFilter(tag), func: FilterFunc.Tag.ToFunc(tag))) + ).Select(x => new KeyValuePair(x.filter, CreateFilterIndexer(x.filter, x.func))); + + availableFilters = new ConcurrentDictionary(predefinedFilters); } public void RemoveLegacyConfigurations() @@ -271,16 +272,10 @@ namespace Jackett.Common.Services This may stop working in the future."); } - if (indexers.ContainsKey(realName)) - return indexers[realName]; - - if (realName == "all") - return aggregateIndexer; - - logger.Error($"Request for unknown indexer: {realName}"); - throw new Exception($"Unknown indexer: {realName}"); + return GetWebIndexer(realName); } + public IWebIndexer GetWebIndexer(string name) { if (indexers.ContainsKey(name)) @@ -289,6 +284,12 @@ namespace Jackett.Common.Services if (name == "all") return aggregateIndexer; + if (availableFilters.TryGetValue(name, out var indexer)) + return indexer; + + if (FilterFunc.TryParse(name, out var filterFunc)) + return availableFilters.GetOrAdd(name, x => CreateFilterIndexer(name, filterFunc)); + logger.Error($"Request for unknown indexer: {name}"); throw new Exception($"Unknown indexer: {name}"); } @@ -318,5 +319,47 @@ namespace Jackett.Common.Services configService.Delete(indexer); indexer.Unconfigure(); } + + private IWebIndexer CreateFilterIndexer(string filter, Func filterFunc) + { + var (fallbackStrategyProvider, resultFilterProvider) = GetStrategyProviders(); + logger.Info($"Adding filter indexer ('{filter}' indexer) ..."); + return new FilterIndexer( + filter, + fallbackStrategyProvider, + resultFilterProvider, + configService, + webClient, + logger, + protectionService, + cacheService, + filterFunc + ) + { + Indexers = indexers.Values + }; + } + + private (IFallbackStrategyProvider fallbackStrategyProvider, IResultFilterProvider resultFilterProvider) + GetStrategyProviders() + { + var omdbApiKey = serverConfig.OmdbApiKey; + IFallbackStrategyProvider fallbackStrategyProvider; + IResultFilterProvider resultFilterProvider; + if (!string.IsNullOrWhiteSpace(omdbApiKey)) + { + var imdbResolver = new OmdbResolver(webClient, omdbApiKey, serverConfig.OmdbApiUrl); + fallbackStrategyProvider = new ImdbFallbackStrategyProvider(imdbResolver); + resultFilterProvider = new ImdbTitleResultFilterProvider(imdbResolver); + } + else + { + fallbackStrategyProvider = new NoFallbackStrategyProvider(); + resultFilterProvider = new NoResultFilterProvider(); + } + + return (fallbackStrategyProvider, resultFilterProvider); + } + } } diff --git a/src/Jackett.Common/Services/Interfaces/IIndexerManagerService.cs b/src/Jackett.Common/Services/Interfaces/IIndexerManagerService.cs index 408b47e9a..c21bee977 100644 --- a/src/Jackett.Common/Services/Interfaces/IIndexerManagerService.cs +++ b/src/Jackett.Common/Services/Interfaces/IIndexerManagerService.cs @@ -13,6 +13,6 @@ namespace Jackett.Common.Services.Interfaces IEnumerable GetAllIndexers(); void InitIndexers(IEnumerable path); - void InitAggregateIndexer(); + void InitMetaIndexers(); } } diff --git a/src/Jackett.Common/Utils/FilterFunc.cs b/src/Jackett.Common/Utils/FilterFunc.cs new file mode 100644 index 000000000..2f7f71a29 --- /dev/null +++ b/src/Jackett.Common/Utils/FilterFunc.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; + +using Jackett.Common.Indexers; +using Jackett.Common.Utils.FilterFuncs; + +namespace Jackett.Common.Utils +{ + public abstract class FilterFunc + { + public static readonly FilterFuncExpression Expression; + public static readonly FilterFuncComponent Tag = Component("tag", args => + { + var tag = args.ToLowerInvariant(); + return indexer => Array.IndexOf(indexer.Tags, tag) > -1; + }); + public static readonly FilterFuncComponent Language = Component("lang", args => indexer => indexer.Language.StartsWith(args, StringComparison.InvariantCultureIgnoreCase)); + public static readonly FilterFuncComponent Type = Component("type", args => indexer => string.Equals(indexer.Type, args, StringComparison.InvariantCultureIgnoreCase)); + + static FilterFunc() + { + Expression = new FilterFuncExpression(Tag, Language, Type); + } + + public static bool TryParse(string source, out Func func) + { + func = Expression.FromFilter(source); + return func != null; + } + + public abstract Func FromFilter(string source); + + public static FilterFuncComponent Component(string id, Func> builder) + { + return new LambdaFilterFuncComponent(id, builder); + } + + private class LambdaFilterFuncComponent : FilterFuncComponent + { + private readonly Func> builder; + + internal LambdaFilterFuncComponent(string id, Func> builder) : base(id) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + this.builder = builder; + } + + public override Func ToFunc(string args) + { + var func = builder(args); + return indexer => indexer != null + ? indexer.IsConfigured && func(indexer) + : throw new ArgumentNullException(nameof(indexer)); + } + } + } +} diff --git a/src/Jackett.Common/Utils/FilterFuncs/FilterFuncComponent.cs b/src/Jackett.Common/Utils/FilterFuncs/FilterFuncComponent.cs new file mode 100644 index 000000000..e8579f941 --- /dev/null +++ b/src/Jackett.Common/Utils/FilterFuncs/FilterFuncComponent.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using Jackett.Common.Indexers; + +namespace Jackett.Common.Utils.FilterFuncs +{ + public abstract class FilterFuncComponent : FilterFunc + { + private static readonly char Separator = ':'; + + protected FilterFuncComponent(string id) + { + if (id == null) + throw new ArgumentNullException(nameof(id)); + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentException("ID cannot be an empty string or whitespaces", nameof(id)); + ID = id; + } + + public string ID { get; } + + public override Func FromFilter(string source) + { + if (string.IsNullOrWhiteSpace(source)) + return null; + + var parts = source.Split(new []{Separator}, 2); + if (parts.Length != 2) + return null; + if (!string.Equals(parts[0], ID, StringComparison.InvariantCultureIgnoreCase)) + return null; + var args = parts[1]; + if (string.IsNullOrWhiteSpace(args)) + return null; + + return ToFunc(args); + } + + public abstract Func ToFunc(string args); + + public string ToFilter(string args) + { + return $"{ID}{Separator}{args}"; + } + } +} diff --git a/src/Jackett.Common/Utils/FilterFuncs/FilterFuncExpression.cs b/src/Jackett.Common/Utils/FilterFuncs/FilterFuncExpression.cs new file mode 100644 index 000000000..f40290332 --- /dev/null +++ b/src/Jackett.Common/Utils/FilterFuncs/FilterFuncExpression.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackett.Common.Indexers; + +namespace Jackett.Common.Utils.FilterFuncs +{ + public class FilterFuncExpression : FilterFunc + { + private static readonly char Separator = ':'; + private static readonly char NotOperator = '!'; + private static readonly char OrOperator = ','; + private static readonly char AndOperator = '+'; + + private readonly IReadOnlyDictionary>> components; + + public FilterFuncExpression(params FilterFuncComponent[] components) + { + if (components == null) + throw new ArgumentNullException(nameof(components)); + if (components.Length == 0) + throw new ArgumentException("Filters cannot be an empty collection.", nameof(components)); + if (components.Any(x => x == null)) + throw new ArgumentException("Filters cannot contains null values.", nameof(components)); + this.components = components.ToDictionary>>(x => x.ID, x => x.ToFunc, StringComparer.InvariantCultureIgnoreCase); + } + + public override Func FromFilter(string source) + { + if (string.IsNullOrWhiteSpace(source)) + return null; + if (source.Contains(OrOperator)) + return source.Split(OrOperator).Select(FromFilter).Aggregate(Or); + if (source.Contains(AndOperator)) + return source.Split(AndOperator).Select(FromFilter).Aggregate(And); + if (source[0] == NotOperator) + return Not(FromFilter(source.Substring(1))); + if (source.Contains(Separator)) + { + var parts = source.Split(new[] {Separator}, 2); + if (parts.Length == 2 && components.TryGetValue(parts[0], out var toFunc)) + return toFunc(parts[1]); + } + return null; + } + + private static Func Not(Func u) => i => !u(i); + private static Func And(Func l, Func r) => i => l(i) && r(i); + private static Func Or(Func l, Func r) => i => l(i) || r(i); + } +} diff --git a/src/Jackett.Server/Controllers/ResultsController.cs b/src/Jackett.Server/Controllers/ResultsController.cs index 37e383db3..72e0049ee 100644 --- a/src/Jackett.Server/Controllers/ResultsController.cs +++ b/src/Jackett.Server/Controllers/ResultsController.cs @@ -212,7 +212,7 @@ namespace Jackett.Server.Controllers var manualResult = new ManualSearchResult(); var trackers = CurrentIndexer is BaseMetaIndexer - ? (CurrentIndexer as BaseMetaIndexer).Indexers.Where(t => t.IsConfigured) + ? (CurrentIndexer as BaseMetaIndexer).ValidIndexers : (new[] { CurrentIndexer }); // Filter current trackers list on Tracker query parameter if available diff --git a/src/Jackett.Server/Controllers/ServerConfigurationController.cs b/src/Jackett.Server/Controllers/ServerConfigurationController.cs index fceca52cf..13e7081f4 100644 --- a/src/Jackett.Server/Controllers/ServerConfigurationController.cs +++ b/src/Jackett.Server/Controllers/ServerConfigurationController.cs @@ -132,7 +132,7 @@ namespace Jackett.Server.Controllers serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/'); configService.SaveConfig(serverConfig); // HACK - indexerService.InitAggregateIndexer(); + indexerService.InitMetaIndexers(); } if (config.proxy_type != serverConfig.ProxyType || diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncComponentTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncComponentTests.cs new file mode 100644 index 000000000..9edb823fe --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncComponentTests.cs @@ -0,0 +1,98 @@ +using System; +using Jackett.Common.Indexers; +using Jackett.Common.Utils.FilterFuncs; +using NUnit.Framework; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + [TestFixture] + public class FilterFuncComponentTests + { + private readonly FilterFuncComponent target = new FilterFuncComponentStub("filter"); + private static readonly Func Func = _ => true; + + private class FilterFuncComponentStub : FilterFuncComponent + { + public FilterFuncComponentStub(string id) : base(id) + { + } + + public override Func ToFunc(string args) => Func; + } + + [Test] + public void Ctor_NullID_ThrowsException() + { + Assert.Throws(() => new FilterFuncComponentStub(null)); + } + + [Test] + public void Ctor_EmptyID_ThrowsException() + { + Assert.Throws(() => new FilterFuncComponentStub(string.Empty)); + } + + [Test] + public void Ctor_WhitespaceID_ThrowsException() + { + Assert.Throws(() => new FilterFuncComponentStub(" ")); + } + + [Test] + public void FromFilter_NullSource_Null() + { + var actual = target.FromFilter(null); + Assert.IsNull(actual); + } + + [Test] + public void FromFilter_EmptySource_Null() + { + var actual = target.FromFilter(string.Empty); + Assert.IsNull(actual); + } + + [Test] + public void FromFilter_WhitespaceSource_Null() + { + var actual = target.FromFilter(" "); + Assert.IsNull(actual); + } + + [Test] + public void FromFilter_WrongSource_Null() + { + var actual = target.FromFilter("wrong:args"); + Assert.IsNull(actual); + } + + [Test] + public void FromFilter_NoArgsSource_Null() + { + var actual = target.FromFilter(target.ID); + Assert.IsNull(actual); + } + + [Test] + public void FromFilter_EmptyArgsSource_Null() + { + var actual = target.FromFilter($"{target.ID}:"); + Assert.IsNull(actual); + } + + [Test] + public void FromFilter_SourceWithArgs() + { + var actual = target.FromFilter($"{target.ID.ToUpper()}:args"); + Assert.AreSame(Func, actual); + } + + [Test] + public void FromFilter_CaseInsensitivePrefixSource() + { + var actual = target.FromFilter($"{target.ID.ToUpper()}:args"); + Assert.AreSame(Func, actual); + } + + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncExpressionTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncExpressionTests.cs new file mode 100644 index 000000000..c7128c390 --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/FilterFuncExpressionTests.cs @@ -0,0 +1,128 @@ +using System; +using Jackett.Common.Indexers; +using Jackett.Common.Utils.FilterFuncs; +using Jackett.Test.TestHelpers; +using NUnit.Framework; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + [TestFixture] + public class FilterFuncExpressionTests + { + private class FilterFuncComponentStub : FilterFuncComponent + { + private readonly Func> builderFunc; + + public FilterFuncComponentStub(string id, Func> builderFunc) : base(id) + { + this.builderFunc = builderFunc; + } + + public override Func ToFunc(string args) => builderFunc(args); + } + + private static readonly FilterFuncComponentStub _BoolFilterFunc = + new FilterFuncComponentStub("bool", + args => bool.Parse(args) ? (Func)(_ => true) : _ => false + ); + + [Test] + public void Ctor_NoFilters_ThrowsException() + { + Assert.Throws(() => new FilterFuncExpression()); + } + + [Test] + public void Ctor_NullFilters_ThrowsException() + { + Assert.Throws(() => new FilterFuncExpression(null)); + } + + [Test] + public void Ctor_EmptyFilters_ThrowsException() + { + Assert.Throws(() => + new FilterFuncExpression(Array.Empty()) + ); + } + + [Test] + public void Ctor_WithNullFilter_ThrowsException() + { + Assert.Throws(() => + new FilterFuncExpression(default(FilterFuncComponent)) + ); + } + + [Test] + public void Ctor_WithDuplicatedPrefixFilter_ThrowsException() + { + const string id = "f1"; + Func> func = _ => throw TestExceptions.UnexpectedInvocation; + + Assert.Throws(() => + { + new FilterFuncExpression( + new FilterFuncComponentStub(id, func), + new FilterFuncComponentStub(id, func)); + }); + } + + [Test] + public void SingleSource() + { + Func expectedFunc1 = _ => throw TestExceptions.UnexpectedInvocation; + Func expectedFunc2 = _ => throw TestExceptions.UnexpectedInvocation; + + var target = new FilterFuncExpression( + new FilterFuncComponentStub("f1", _ => expectedFunc1), + new FilterFuncComponentStub("f2", _ => expectedFunc2) + ); + + var actualFunc1 = target.FromFilter("f1:args"); + Assert.AreSame(expectedFunc1, actualFunc1); + var actualFunc2 = target.FromFilter("f2:args"); + Assert.AreSame(expectedFunc2, actualFunc2); + var actualFunc3 = target.FromFilter("f3:args"); + Assert.IsNull(actualFunc3); + } + + [Test] + public void SingleSource_NotOperator() + { + var target = new FilterFuncExpression(_BoolFilterFunc); + + var filterFunc = target.FromFilter("!bool:true"); + Assert.IsFalse(filterFunc(null)); + } + + [Test] + public void SingleSource_AndOperator() + { + var target = new FilterFuncExpression(_BoolFilterFunc); + + var filterFunc = target.FromFilter("bool:true+bool:false"); + Assert.IsFalse(filterFunc(null)); + } + + [Test] + public void SingleSource_OrOperator() + { + var target = new FilterFuncExpression(_BoolFilterFunc); + + var filterFunc = target.FromFilter("bool:false,bool:true"); + Assert.IsTrue(filterFunc(null)); + } + + [Test] + public void SingleSource_OperatorPrecedence() + { + var target = new FilterFuncExpression(_BoolFilterFunc); + + var filterFunc1 = target.FromFilter("bool:false+bool:true,bool:true"); + Assert.IsTrue(filterFunc1(null)); + var filterFunc2 = target.FromFilter("bool:true,bool:true+bool:false"); + Assert.IsTrue(filterFunc2(null)); + } + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs new file mode 100644 index 000000000..d22db58a9 --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs @@ -0,0 +1,55 @@ +using System.Text; +using System.Threading.Tasks; +using Jackett.Common.Indexers; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; +using Jackett.Test.TestHelpers; +using Newtonsoft.Json.Linq; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + public class IndexerStub : IIndexer + { + public virtual string SiteLink => throw TestExceptions.UnexpectedInvocation; + + public virtual string[] AlternativeSiteLinks => throw TestExceptions.UnexpectedInvocation; + + public virtual string DisplayName => throw TestExceptions.UnexpectedInvocation; + + public virtual string DisplayDescription => throw TestExceptions.UnexpectedInvocation; + + public virtual string Type => throw TestExceptions.UnexpectedInvocation; + + public virtual string Language => throw TestExceptions.UnexpectedInvocation; + + public virtual string LastError + { + get => throw TestExceptions.UnexpectedInvocation; + set => throw TestExceptions.UnexpectedInvocation; + } + + public virtual string Id => throw TestExceptions.UnexpectedInvocation; + + public virtual Encoding Encoding => throw TestExceptions.UnexpectedInvocation; + + public virtual TorznabCapabilities TorznabCaps => throw TestExceptions.UnexpectedInvocation; + + public virtual bool IsConfigured => throw TestExceptions.UnexpectedInvocation; + + public virtual string[] Tags => throw TestExceptions.UnexpectedInvocation; + + public virtual Task GetConfigurationForSetup() => throw TestExceptions.UnexpectedInvocation; + + public virtual Task ApplyConfiguration(JToken configJson) => throw TestExceptions.UnexpectedInvocation; + + public virtual void LoadFromSavedConfiguration(JToken jsonConfig) => throw TestExceptions.UnexpectedInvocation; + + public virtual void SaveConfig() => throw TestExceptions.UnexpectedInvocation; + + public virtual void Unconfigure() => throw TestExceptions.UnexpectedInvocation; + + public virtual Task ResultsForQuery(TorznabQuery query, bool isMetaIndexer = false) => throw TestExceptions.UnexpectedInvocation; + + public virtual bool CanHandleQuery(TorznabQuery query) => throw TestExceptions.UnexpectedInvocation; + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs new file mode 100644 index 000000000..9fc427dbe --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs @@ -0,0 +1,55 @@ +using NUnit.Framework; +using static Jackett.Common.Utils.FilterFunc; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + [TestFixture] + public class LanguageFuncTests + { + private class LanguageIndexerStub : IndexerStub + { + public LanguageIndexerStub(string language) + { + Language = language; + } + + public override bool IsConfigured => true; + + public override string Language { get; } + } + + [Test] + public void CaseInsensitiveSource_CaseInsensitiveFilter() + { + var language = "en"; + var region = "US"; + + var lrLanguage = new LanguageIndexerStub($"{language.ToLower()}-{region.ToLower()}"); + var LRFilterFunc = Language.ToFunc($"{language.ToUpper()}-{region.ToUpper()}"); + Assert.IsTrue(LRFilterFunc(lrLanguage)); + + var lRLanguage = new LanguageIndexerStub($"{language.ToLower()}-{region.ToUpper()}"); + var LrFilterFunc = Language.ToFunc($"{language.ToUpper()}-{region.ToLower()}"); + Assert.IsTrue(LrFilterFunc(lRLanguage)); + + var LrLanguage = new LanguageIndexerStub($"{language.ToUpper()}-{region.ToLower()}"); + var lRFilterFunc = Language.ToFunc($"{language.ToLower()}-{region.ToUpper()}"); + Assert.IsTrue(lRFilterFunc(LrLanguage)); + + var LRLanguage = new LanguageIndexerStub($"{language.ToUpper()}-{region.ToUpper()}"); + var lrFilterFunc = Language.ToFunc($"{language.ToLower()}-{region.ToLower()}"); + Assert.IsTrue(lrFilterFunc(LRLanguage)); + } + + [Test] + public void LanguageWithoutRegion() + { + var language = "en"; + var funcFilter = Language.ToFunc(language); + + Assert.IsTrue(funcFilter(new LanguageIndexerStub(language))); + Assert.IsTrue(funcFilter(new LanguageIndexerStub($"{language}-region1"))); + Assert.IsFalse(funcFilter(new LanguageIndexerStub($"language2-{language}"))); + } + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs new file mode 100644 index 000000000..af7795163 --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using static Jackett.Common.Utils.FilterFunc; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + [TestFixture] + public class TagFuncTests + { + private class TagsIndexerStub : IndexerStub + { + public TagsIndexerStub(params string[] tags) + { + Tags = tags; + } + + public override bool IsConfigured => true; + + public override string[] Tags { get; } + } + + [Test] + public void CaseInsensitiveFilter() + { + var tagId = "g1"; + + var tag = new TagsIndexerStub(tagId); + + var upperTarget = Tag.ToFunc(tagId.ToUpper()); + Assert.IsTrue(upperTarget(tag)); + + var lowerTarget = Tag.ToFunc(tagId.ToLower()); + Assert.IsTrue(lowerTarget(tag)); + } + + [Test] + public void ContainsTagId() + { + var tagId = "g1"; + var target = Tag.ToFunc(tagId); + + Assert.IsTrue(target(new TagsIndexerStub(tagId))); + Assert.IsTrue(target(new TagsIndexerStub(tagId, "g2"))); + Assert.IsTrue(target(new TagsIndexerStub("g2", tagId))); + Assert.IsFalse(target(new TagsIndexerStub("g2"))); + } + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs new file mode 100644 index 000000000..28ead99b0 --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using static Jackett.Common.Utils.FilterFunc; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + [TestFixture] + public class TypeFuncTests + { + private class TypeIndexerStub : IndexerStub + { + public TypeIndexerStub(string type) + { + Type = type; + } + + public override bool IsConfigured => true; + + public override string Type { get; } + } + + [Test] + public void CaseInsensitiveSource_CaseInsensitiveFilter() + { + var typeId = "type-id"; + + var lowerType = new TypeIndexerStub(typeId.ToLower()); + var upperType = new TypeIndexerStub(typeId.ToUpper()); + + var upperFilterFunc = Type.ToFunc(typeId.ToUpper()); + Assert.IsTrue(upperFilterFunc(lowerType)); + Assert.IsTrue(upperFilterFunc(upperType)); + + var lowerFilterFunc = Type.ToFunc(typeId.ToLower()); + Assert.IsTrue(lowerFilterFunc(lowerType)); + Assert.IsTrue(lowerFilterFunc(upperType)); + } + + [Test] + public void PartialType() + { + var typeId = "type-id"; + + var funcFilter = Type.ToFunc($"{typeId}"); + + Assert.IsFalse(funcFilter(new TypeIndexerStub($"{typeId}suffix"))); + Assert.IsFalse(funcFilter(new TypeIndexerStub($"prefix{typeId}"))); + Assert.IsFalse(funcFilter(new TypeIndexerStub($"prefix{typeId}suffix"))); + } + } + + +} diff --git a/src/Jackett.Test/TestHelpers/TestExceptions.cs b/src/Jackett.Test/TestHelpers/TestExceptions.cs new file mode 100644 index 000000000..b9d954ab4 --- /dev/null +++ b/src/Jackett.Test/TestHelpers/TestExceptions.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; + +namespace Jackett.Test.TestHelpers +{ + internal static class TestExceptions + { + public static AssertionException UnexpectedInvocation => new AssertionException("Unexpected Invocation"); + } +} diff --git a/src/Jackett.Test/TestHelpers/TestIIndexerManagerServiceHelper.cs b/src/Jackett.Test/TestHelpers/TestIIndexerManagerServiceHelper.cs index aee453ca5..0c8147c5d 100644 --- a/src/Jackett.Test/TestHelpers/TestIIndexerManagerServiceHelper.cs +++ b/src/Jackett.Test/TestHelpers/TestIIndexerManagerServiceHelper.cs @@ -25,6 +25,6 @@ namespace Jackett.Test.TestHelpers public Task TestIndexer(string name) => throw new NotImplementedException(); - public void InitAggregateIndexer() => throw new NotImplementedException(); + public void InitMetaIndexers() => throw new NotImplementedException(); } }