Skip to content
Open
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
193 changes: 192 additions & 1 deletion scancodeio/static/js/resource_tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,196 @@
}
}

function isTypingElement(element) {
if (!element) return false;
const tagName = element.tagName ? element.tagName.toLowerCase() : "";
return (
["input", "textarea", "select"].includes(tagName) ||
element.isContentEditable
);
}

function initSearchInteractions() {
const searchContainer = document.getElementById('resource-search-container');
const searchInput = document.getElementById('file-search-input');
const searchResults = document.getElementById('search-results');
const clearSearchBtn = document.getElementById('clear-search');
if (!searchContainer || !searchInput || !searchResults || !clearSearchBtn) return;

let activeIndex = -1;

if (searchResults.parentNode !== document.body) {
searchResults.classList.add('search-dropdown-portal');
document.body.appendChild(searchResults);
}

function getResultItems() {
return Array.from(searchResults.querySelectorAll('.search-result-item'));
}

function updateDropdownPosition() {
if (searchResults.classList.contains('is-hidden')) return;

const rect = searchContainer.getBoundingClientRect();
const viewportPadding = 8;
const baseWidth = Math.max(rect.width + 300, window.innerWidth * 0.5);
const width = Math.min(baseWidth, window.innerWidth - viewportPadding * 2);
const left = Math.max(
viewportPadding,
Math.min(rect.left, window.innerWidth - width - viewportPadding)
);
const dropdownTop = rect.bottom + 4;
const availableHeight = window.innerHeight - dropdownTop - viewportPadding;
const maxHeight = Math.max(180, Math.min(window.innerHeight * 0.62, availableHeight));

searchResults.style.left = `${left}px`;
searchResults.style.top = `${dropdownTop}px`;
searchResults.style.width = `${width}px`;
searchResults.style.maxHeight = `${maxHeight}px`;
}

function showDropdown() {
if (searchInput.value.trim()) {
searchResults.classList.remove('is-hidden');
updateDropdownPosition();
}
}

function hideDropdown() {
searchResults.classList.add('is-hidden');
setActiveItem(-1);
}

function updateClearButtonVisibility() {
clearSearchBtn.classList.toggle('is-hidden', !searchInput.value.trim());
}

function setActiveItem(nextIndex) {
const items = getResultItems();
if (!items.length) {
activeIndex = -1;
return;
}

items.forEach(item => item.classList.remove('is-active'));
if (nextIndex < 0) {
activeIndex = -1;
return;
}

activeIndex = ((nextIndex % items.length) + items.length) % items.length;
const activeItem = items[activeIndex];
activeItem.classList.add('is-active');
activeItem.scrollIntoView({ block: 'nearest' });
}

function triggerActiveItem() {
const items = getResultItems();
if (!items.length) return;

const index = activeIndex >= 0 ? activeIndex : 0;
const activeItem = items[index];
activeItem.click();
}

function clearSearch() {
searchInput.value = '';
updateClearButtonVisibility();
hideDropdown();
searchResults.innerHTML = '';
searchInput.focus();
}

clearSearchBtn.addEventListener('click', clearSearch);

searchInput.addEventListener('input', function() {
activeIndex = -1;
updateClearButtonVisibility();
if (!searchInput.value.trim()) {
hideDropdown();
} else {
updateDropdownPosition();
}
});

searchInput.addEventListener('focus', showDropdown);

window.addEventListener('resize', updateDropdownPosition);
window.addEventListener('scroll', updateDropdownPosition, true);

searchInput.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
hideDropdown();
searchInput.blur();
return;
}

if (event.key === 'ArrowDown') {
event.preventDefault();
const items = getResultItems();
if (!items.length) return;
showDropdown();
const nextIndex = activeIndex < 0 ? 0 : activeIndex + 1;
setActiveItem(nextIndex);
return;
}

if (event.key === 'ArrowUp') {
event.preventDefault();
const items = getResultItems();
if (!items.length) return;
showDropdown();
const nextIndex = activeIndex < 0 ? items.length - 1 : activeIndex - 1;
setActiveItem(nextIndex);
return;
}

if (event.key === 'Enter') {
const items = getResultItems();
if (!items.length || searchResults.classList.contains('is-hidden')) return;
event.preventDefault();
triggerActiveItem();
}
});

document.addEventListener('click', function(event) {
const resultItem = event.target.closest('.search-result-item');
if (resultItem && searchResults.contains(resultItem)) {
hideDropdown();
searchInput.blur();
expandToPath(resultItem.dataset.path);
return;
}

if (!searchContainer.contains(event.target) && !searchResults.contains(event.target)) {
hideDropdown();
}
});

document.addEventListener('keydown', function(event) {
const target = event.target;
if (event.key.toLowerCase() === 't' && !event.metaKey && !event.ctrlKey && !event.altKey) {
if (isTypingElement(target)) return;
event.preventDefault();
searchInput.focus();
}
});

document.body.addEventListener('htmx:afterSettle', function(event) {
if (event.target !== searchResults) return;
activeIndex = -1;
updateClearButtonVisibility();
if (searchInput.value.trim()) {
showDropdown();
updateDropdownPosition();
} else {
hideDropdown();
}
});

updateClearButtonVisibility();
}

document.addEventListener("click", async e => {
const node = e.target.closest("[data-folder], .is-file[data-file], .expand-in-tree, [data-chevron]");
if (!node) return;
Expand Down Expand Up @@ -142,5 +332,6 @@
});
});

initSearchInteractions();
});
})();
})();
89 changes: 87 additions & 2 deletions scancodeio/static/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -551,13 +551,98 @@ body.full-screen #resource-viewer .message-header {
min-width: 0;
max-width: 100%;
border-right: 1px solid #ccc;
overflow-y: auto;
overflow-x: hidden;
overflow: visible;
display: flex;
flex-direction: column;
min-height: 0;
flex-basis: 25%;
transition: opacity 0.2s ease;
position: relative;
z-index: 3000;
}
#resource-tree-container .resource-tree-scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#resource-tree-container .search-container {
position: sticky;
top: 0;
z-index: 20;
background: var(--bulma-scheme-main);
padding: .25rem 0 .5rem;
}
#resource-tree-container #clear-search {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
cursor: pointer;
pointer-events: auto;
}
#resource-tree-container .search-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 1000;
}
#resource-tree-container .search-dropdown,
.search-dropdown.search-dropdown-portal {
border: 1px solid var(--bulma-border);
border-radius: 12px;
background: var(--bulma-scheme-main);
box-shadow: var(--bulma-shadow);
min-height: 0;
max-height: 62vh;
overflow-y: auto;
overflow-x: hidden;
}
.search-dropdown.search-dropdown-portal {
position: fixed;
top: 0;
left: 0;
right: auto;
z-index: 11000;
}
#resource-tree-container .search-result-item,
.search-dropdown.search-dropdown-portal .search-result-item {
display: flex;
align-items: center;
gap: .5rem;
color: var(--bulma-text);
font-size: 14px;
line-height: 20px;
font-weight: 400;
white-space: nowrap;
}
#resource-tree-container .search-result-item:hover,
#resource-tree-container .search-result-item.is-active,
.search-dropdown.search-dropdown-portal .search-result-item:hover,
.search-dropdown.search-dropdown-portal .search-result-item.is-active {
background-color: var(--bulma-background-hover);
}
#resource-tree-container .search-result-item .icon,
.search-dropdown.search-dropdown-portal .search-result-item .icon {
color: inherit;
width: 16px;
min-width: 16px;
height: 16px;
}
#resource-tree-container .search-result-item .icon i,
.search-dropdown.search-dropdown-portal .search-result-item .icon i {
font-size: 13px;
}
#resource-tree-container .search-result-path,
.search-dropdown.search-dropdown-portal .search-result-path {
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
#resource-tree-container .left-pane.collapsed {
opacity: 0;
pointer-events: none;
Expand Down
8 changes: 5 additions & 3 deletions scanpipe/templates/scanpipe/tree/resource_left_pane.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{% include "scanpipe/tree/resource_left_pane_header.html" only %}
<div id="resource-tree">
{% include "scanpipe/tree/resource_left_pane_tree.html" with children=children path=path %}
</div>
{% include "scanpipe/tree/resource_search_bar.html" %}

<div id="resource-tree" class="resource-tree-scroll">
{% include "scanpipe/tree/resource_left_pane_tree.html" with children=children %}
</div>
30 changes: 30 additions & 0 deletions scanpipe/templates/scanpipe/tree/resource_search_bar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="mb-3 search-container" id="resource-search-container">
<div class="field mb-0">
<div class="control has-icons-left has-icons-right is-expanded">
<input
id="file-search-input"
class="input is-small"
type="text"
placeholder="Go to file..."
autocomplete="off"
hx-get="{% url 'project_resource_tree_search' project.slug %}"
hx-target="#search-results"
hx-trigger="input changed delay:200ms"
hx-include="this"
name="search"
>
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
<button
id="clear-search"
class="icon is-small is-right is-hidden has-text-grey"
type="button"
aria-label="Clear search"
>
<i class="fas fa-times-circle fa-lg"></i>
</button>
</div>
</div>
<div id="search-results" class="search-dropdown is-hidden"></div>
</div>
33 changes: 33 additions & 0 deletions scanpipe/templates/scanpipe/tree/resource_search_results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% if search_results %}
<div class="search-results m-0">
{% for resource in search_results %}
<a
class="search-result-item px-4 py-2 is-clickable"
{% if resource.is_dir %}
hx-get="{% url 'project_resource_tree_right_pane' project.slug resource.path %}"
{% else %}
hx-get="{% url 'resource_detail' project.slug resource.path %}"
{% endif %}
hx-target="#right-pane"
hx-push-url="{% url 'project_resource_tree' project.slug resource.path %}"
data-path="{{ resource.path }}"
title="{{ resource.path }}">
<span class="icon mr-2">
{% if resource.is_dir %}
<i class="fas fa-folder"></i>
{% else %}
<i class="far fa-file"></i>
{% endif %}
</span>
<span class="search-result-path">{{ resource.path }}</span>
</a>
{% endfor %}
</div>
{% elif query %}
<div class="has-text-centered px-4 py-5">
<div class="icon is-large has-text-grey-light mb-3">
<i class="fas fa-search fa-2x"></i>
</div>
<p class="has-text-grey">No files found matching "{{ query }}"</p>
</div>
{% endif %}
Loading