diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 42f31c81b..814cdc855 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -54,7 +54,7 @@ $.when($.ready).then(() => { const sortableEl = document.getElementById("sortable"); let sortable; - if (sortableEl !== null) { + if (sortableEl !== null && typeof Sortable !== "undefined") { // eslint-disable-next-line no-undef sortable = Sortable.create(sortableEl, { disabled: true, @@ -102,154 +102,216 @@ $.when($.ready).then(() => { $(".tooltip", this).removeClass("active"); }); - $(".searchform > form").on("submit", (event) => { - if ($("#search-container select[name=provider]").val() === "tiles") { - event.preventDefault(); + const TAG_USAGE_STORAGE_KEY = "heimdall.tagUsageHistory"; + const TAG_USAGE_WINDOW = 100; + const ALL_TAG = "all"; + + function getTagUsageHistory() { + try { + const raw = JSON.parse(localStorage.getItem(TAG_USAGE_STORAGE_KEY) || "[]"); + if (Array.isArray(raw)) { + return raw.filter((entry) => typeof entry === "string"); + } + + // Legacy fallback: old object-based counters. + if (raw && typeof raw === "object") { + const expanded = []; + Object.keys(raw).forEach((tag) => { + const count = Number(raw[tag] || 0); + for (let i = 0; i < count; i += 1) { + expanded.push(tag); + } + }); + return expanded.slice(-TAG_USAGE_WINDOW); + } + } catch (_error) { + return []; } - }); - // Autocomplete functionality - let autocompleteTimeout = null; - let currentAutocompleteRequest = null; + return []; + } - function hideAutocomplete() { - $("#search-autocomplete").remove(); + function saveTagUsageHistory(history) { + try { + localStorage.setItem( + TAG_USAGE_STORAGE_KEY, + JSON.stringify(history.slice(-TAG_USAGE_WINDOW)) + ); + } catch (_error) { + // Ignore localStorage failures and continue with default ordering. + } } - function showAutocomplete(suggestions, inputElement) { - hideAutocomplete(); + function getTagUsageCounts() { + const history = getTagUsageHistory(); + const counts = {}; + + history.forEach((tag) => { + counts[tag] = (counts[tag] || 0) + 1; + }); + + return counts; + } - if (!suggestions || suggestions.length === 0) { + function updateTagButtonCounts() { + const items = $("#sortable").find(".item-container"); + const provider = $("#search-container select[name=provider]").val(); + const search = ( + $("#search-container input[name=q]").val() || "" + ).toLowerCase(); + + const datasetItems = + provider === "tiles" + ? items.filter(function () { + const name = String($(this).data("name") || "").toLowerCase(); + return name.includes(search); + }) + : items; + + $("#taglist .tag").each(function () { + const button = $(this); + const tag = button.data("tag"); + + if (button.data("base-text") === undefined) { + const baseText = button + .text() + .replace(/\s*\(\d+\)\s*$/, "") + .trim(); + button.data("base-text", baseText); + } + + const baseText = button.data("base-text"); + const count = + tag === ALL_TAG + ? datasetItems.length + : datasetItems.filter(function () { + const item = $(this); + if (String(tag).startsWith("cat-")) { + return item.closest(".category").hasClass(tag); + } + return item.hasClass(tag); + }).length; + + button.text(`${baseText} (${count})`); + }); + } + + function reorderTagButtonsByUsage() { + const taglist = $("#taglist"); + if (taglist.length === 0) { return; } - const $input = $(inputElement); - const position = $input.position(); - const width = $input.outerWidth(); + const usage = getTagUsageCounts(); + const buttons = taglist.find(".tag").get(); - const $autocomplete = $('
'); + buttons.sort((firstButton, secondButton) => { + const first = $(firstButton); + const second = $(secondButton); + const firstTag = String(first.data("tag") || ""); + const secondTag = String(second.data("tag") || ""); - suggestions.forEach((suggestion) => { - const $item = $('') - .text(suggestion) - .on("click", () => { - $input.val(suggestion); - hideAutocomplete(); - $input.closest("form").submit(); - }); - $autocomplete.append($item); - }); + if (firstTag === ALL_TAG) return -1; + if (secondTag === ALL_TAG) return 1; - $autocomplete.css({ - position: "absolute", - top: `${position.top + $input.outerHeight()}px`, - left: `${position.left}px`, - width: `${width}px`, + const usageDelta = (usage[secondTag] || 0) - (usage[firstTag] || 0); + if (usageDelta !== 0) { + return usageDelta; + } + + return (first.data("order-index") || 0) - (second.data("order-index") || 0); }); - $input.closest("#search-container").append($autocomplete); + buttons.forEach((button) => { + taglist.append(button); + }); } - function fetchAutocomplete(query, provider) { - // Cancel previous request if any - if (currentAutocompleteRequest) { - currentAutocompleteRequest.abort(); + function applyTileFilters() { + const sortable = $("#sortable"); + const items = sortable.find(".item-container"); + const categoryWrappers = sortable.find(".category"); + const categoryTitles = sortable.find(".category > .title"); + const provider = $("#search-container select[name=provider]").val(); + const search = ( + $("#search-container input[name=q]").val() || "" + ).toLowerCase(); + const selectedTag = + String($("#taglist .tag.current").first().data("tag") || ALL_TAG) || ALL_TAG; + + if (provider !== "tiles") { + items.show(); + categoryWrappers.show(); + categoryTitles.show(); + updateTagButtonCounts(); + return; } - if (!query || query.trim().length < 2) { - hideAutocomplete(); - return; + items.hide(); + items + .filter(function () { + const item = $(this); + const name = String(item.data("name") || "").toLowerCase(); + const matchesSearch = search.length === 0 || name.includes(search); + const matchesTag = + selectedTag === ALL_TAG || + (String(selectedTag).startsWith("cat-") + ? item.closest(".category").hasClass(selectedTag) + : item.hasClass(selectedTag)); + return matchesSearch && matchesTag; + }) + .show(); + + if (categoryWrappers.length > 0) { + // Reset wrapper visibility first so cross-category switching works reliably. + categoryWrappers.show(); + + categoryWrappers.each(function () { + const wrapper = $(this); + const matchingChildren = wrapper + .find(".item-container") + .filter(function () { + return $(this).css("display") !== "none"; + }).length; + wrapper.toggle(matchingChildren > 0); + }); + + const isFiltered = selectedTag !== ALL_TAG || search.length > 0; + if (isFiltered) { + categoryTitles.hide(); + } else { + categoryTitles.show(); + } } - currentAutocompleteRequest = $.ajax({ - url: `${base}search/autocomplete`, - method: "GET", - data: { - q: query, - provider, - }, - success(data) { - const inputElement = $("#search-container input[name=q]")[0]; - showAutocomplete(data, inputElement); - }, - error() { - hideAutocomplete(); - }, - complete() { - currentAutocompleteRequest = null; - }, - }); + updateTagButtonCounts(); } - $("#search-container") - .on("input", "input[name=q]", function () { - const search = this.value; - const items = $("#sortable").find(".item-container"); - // Get provider from either select or hidden input - const provider = - $("#search-container select[name=provider]").val() || - $("#search-container input[name=provider]").val(); - - if (provider === "tiles") { - hideAutocomplete(); - if (search.length > 0) { - items.hide(); - items - .filter(function () { - const name = $(this).data("name").toLowerCase(); - return name.includes(search.toLowerCase()); - }) - .show(); - } else { - items.show(); - } - } else { - items.show(); + $("#taglist .tag").each(function (index) { + $(this).data("order-index", index); + }); - // Debounce autocomplete requests - clearTimeout(autocompleteTimeout); - autocompleteTimeout = setTimeout(() => { - fetchAutocomplete(search, provider); - }, 300); - } + reorderTagButtonsByUsage(); + + $(".searchform > form").on("submit", (event) => { + if ($("#search-container select[name=provider]").val() === "tiles") { + event.preventDefault(); + } + }); + + $("#search-container") + .on("input", "input[name=q]", () => { + applyTileFilters(); }) .on("change", "select[name=provider]", function () { - const items = $("#sortable").find(".item-container"); if ($(this).val() === "tiles") { $("#search-container button").hide(); - const search = $("#search-container input[name=q]").val(); - if (search.length > 0) { - items.hide(); - items - .filter(function () { - const name = $(this).data("name").toLowerCase(); - return name.includes(search.toLowerCase()); - }) - .show(); - } else { - items.show(); - } } else { $("#search-container button").show(); - items.show(); - hideAutocomplete(); } + applyTileFilters(); }); - // Hide autocomplete when clicking outside - $(document).on("click", (e) => { - if (!$(e.target).closest("#search-container").length) { - hideAutocomplete(); - } - }); - - // Hide autocomplete on Escape key - $(document).on("keydown", (e) => { - if (e.key === "Escape") { - hideAutocomplete(); - } - }); - $("#search-container select[name=provider]").trigger("change"); $("#app") @@ -275,13 +337,19 @@ $.when($.ready).then(() => { }) .on("click", ".tag", (e) => { e.preventDefault(); - const tag = $(e.target).data("tag"); + const tagButton = $(e.currentTarget); + const tag = String(tagButton.data("tag") || ALL_TAG); $("#taglist .tag").removeClass("current"); - $(e.target).addClass("current"); - $("#sortable .item-container").show(); - if (tag !== "all") { - $(`#sortable .item-container:not(.${tag})`).hide(); + tagButton.addClass("current"); + + if (tag !== ALL_TAG) { + const usageHistory = getTagUsageHistory(); + usageHistory.push(tag); + saveTagUsageHistory(usageHistory); } + + reorderTagButtonsByUsage(); + applyTileFilters(); }) .on("click", "#add-item, #pin-item", (e) => { e.preventDefault(); @@ -343,6 +411,7 @@ $.when($.ready).then(() => { const inner = $(data).filter("#sortable").html(); $("#sortable").html(inner); current.toggleClass("active"); + applyTileFilters(); }); }); $("#itemform").on("submit", () => { diff --git a/resources/views/partials/taglist.blade.php b/resources/views/partials/taglist.blade.php index 741832fc9..0848321ea 100644 --- a/resources/views/partials/taglist.blade.php +++ b/resources/views/partials/taglist.blade.php @@ -1,8 +1,9 @@ -@if( $treat_tags_as == 'tags') - @if($taglist->first()) + +@if($treat_tags_as == 'tags') + @if(isset($taglist) && $taglist->first())