Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 191 additions & 122 deletions resources/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = $('<div id="search-autocomplete"></div>');
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 = $('<div class="autocomplete-item"></div>')
.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")
Expand All @@ -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();
Expand Down Expand Up @@ -343,6 +411,7 @@ $.when($.ready).then(() => {
const inner = $(data).filter("#sortable").html();
$("#sortable").html(inner);
current.toggleClass("active");
applyTileFilters();
});
});
$("#itemform").on("submit", () => {
Expand Down
Loading