переключение тэгов поиска в боковом меню, подсветка найденных элементов
This commit is contained in:
parent
1e8919e33c
commit
45fedcd359
15
index.html
15
index.html
@ -48,6 +48,13 @@
|
||||
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
||||
</div>
|
||||
</aside>
|
||||
<div
|
||||
class="sidebar-resizer"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Изменить ширину бокового меню"
|
||||
data-sidebar-resizer
|
||||
></div>
|
||||
|
||||
<div class="settings-overlay" data-settings-overlay></div>
|
||||
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
||||
@ -95,6 +102,14 @@
|
||||
<input type="checkbox" data-show-numbers />
|
||||
</label>
|
||||
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Показывать теги дерева</strong>
|
||||
<small>Тип сервиса рядом с элементом</small>
|
||||
</span>
|
||||
<input type="checkbox" data-show-tree-tags />
|
||||
</label>
|
||||
|
||||
<div class="control-group accent-group">
|
||||
<span>Акцент</span>
|
||||
<button
|
||||
|
||||
116
script.js
116
script.js
@ -138,6 +138,8 @@ const defaultPreferences = {
|
||||
view: "cards",
|
||||
accent: "violet",
|
||||
showNumbers: false,
|
||||
showTreeTags: true,
|
||||
sidebarWidth: 390,
|
||||
};
|
||||
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
|
||||
|
||||
@ -145,6 +147,7 @@ const preferences = { ...defaultPreferences, ...readPreferences() };
|
||||
const collapseState = readCollapseState();
|
||||
const catalog = document.querySelector("[data-catalog-root]");
|
||||
const navTree = document.querySelector("[data-nav-tree]");
|
||||
const sidebarResizer = document.querySelector("[data-sidebar-resizer]");
|
||||
const searchInput = document.querySelector("[data-search-input]");
|
||||
const settingsToggle = document.querySelector("[data-settings-toggle]");
|
||||
const settingsPanel = document.querySelector("[data-settings-panel]");
|
||||
@ -154,13 +157,17 @@ const themeButtons = document.querySelectorAll("[data-theme]");
|
||||
const viewButtons = document.querySelectorAll("[data-view]");
|
||||
const accentButtons = document.querySelectorAll("[data-accent]");
|
||||
const showNumbersInput = document.querySelector("[data-show-numbers]");
|
||||
const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
|
||||
const emptyTemplate = document.querySelector("#empty-template");
|
||||
|
||||
renderNavigationTree();
|
||||
applyPreferences(preferences);
|
||||
renderCatalog();
|
||||
|
||||
searchInput.addEventListener("input", renderCatalog);
|
||||
searchInput.addEventListener("input", () => {
|
||||
renderNavigationTree();
|
||||
renderCatalog();
|
||||
});
|
||||
|
||||
navTree.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-focus-target]");
|
||||
@ -234,6 +241,36 @@ showNumbersInput.addEventListener("change", () => {
|
||||
savePreferences(preferences);
|
||||
});
|
||||
|
||||
showTreeTagsInput.addEventListener("change", () => {
|
||||
preferences.showTreeTags = showTreeTagsInput.checked;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
});
|
||||
|
||||
sidebarResizer.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.add("is-resizing-sidebar");
|
||||
sidebarResizer.setPointerCapture(event.pointerId);
|
||||
|
||||
const handlePointerMove = (moveEvent) => {
|
||||
const layoutLeft = document.querySelector(".app-layout").getBoundingClientRect().left;
|
||||
preferences.sidebarWidth = Math.round(clamp(moveEvent.clientX - layoutLeft, 260, 560));
|
||||
applyPreferences(preferences);
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
document.body.classList.remove("is-resizing-sidebar");
|
||||
savePreferences(preferences);
|
||||
sidebarResizer.removeEventListener("pointermove", handlePointerMove);
|
||||
sidebarResizer.removeEventListener("pointerup", handlePointerUp);
|
||||
sidebarResizer.removeEventListener("pointercancel", handlePointerUp);
|
||||
};
|
||||
|
||||
sidebarResizer.addEventListener("pointermove", handlePointerMove);
|
||||
sidebarResizer.addEventListener("pointerup", handlePointerUp);
|
||||
sidebarResizer.addEventListener("pointercancel", handlePointerUp);
|
||||
});
|
||||
|
||||
function createItem(number, title, type, tags = [], url = "#") {
|
||||
return { number, title, type, tags, url };
|
||||
}
|
||||
@ -245,7 +282,7 @@ function renderCatalog() {
|
||||
let visibleItems = 0;
|
||||
|
||||
ecosystemSections.forEach((section) => {
|
||||
const sectionElement = createSectionElement(section, forceOpen);
|
||||
const sectionElement = createSectionElement(section, forceOpen, query);
|
||||
let visibleGroups = 0;
|
||||
let sectionVisibleItems = 0;
|
||||
|
||||
@ -258,7 +295,7 @@ function renderCatalog() {
|
||||
|
||||
visibleGroups += 1;
|
||||
sectionVisibleItems += filteredItems.length;
|
||||
sectionElement.querySelector(".section-groups").append(createGroupElement(group, filteredItems, forceOpen));
|
||||
sectionElement.querySelector(".section-groups").append(createGroupElement(group, filteredItems, forceOpen, query));
|
||||
});
|
||||
|
||||
if (visibleGroups) {
|
||||
@ -279,7 +316,7 @@ function renderCatalog() {
|
||||
|
||||
}
|
||||
|
||||
function createSectionElement(section, forceOpen = false) {
|
||||
function createSectionElement(section, forceOpen = false, query = "") {
|
||||
const sectionElement = document.createElement("details");
|
||||
sectionElement.className = "catalog-section";
|
||||
sectionElement.id = getSectionId(section);
|
||||
@ -289,9 +326,9 @@ function createSectionElement(section, forceOpen = false) {
|
||||
sectionElement.innerHTML = `
|
||||
<summary class="section-head">
|
||||
<div>
|
||||
<p class="section-number">${section.number} раздел</p>
|
||||
<h2>${section.title}</h2>
|
||||
<p>${section.description}</p>
|
||||
<p class="section-number">${highlightText(section.number, query)} раздел</p>
|
||||
<h2>${highlightText(section.title, query)}</h2>
|
||||
<p>${highlightText(section.description, query)}</p>
|
||||
</div>
|
||||
<span class="section-meta">
|
||||
<span class="section-count"><span data-section-count>0</span> элементов</span>
|
||||
@ -303,7 +340,7 @@ function createSectionElement(section, forceOpen = false) {
|
||||
return sectionElement;
|
||||
}
|
||||
|
||||
function createGroupElement(group, items, forceOpen = false) {
|
||||
function createGroupElement(group, items, forceOpen = false, query = "") {
|
||||
const groupElement = document.createElement("details");
|
||||
groupElement.className = "catalog-group";
|
||||
groupElement.id = getGroupId(group);
|
||||
@ -311,13 +348,13 @@ function createGroupElement(group, items, forceOpen = false) {
|
||||
groupElement.tabIndex = -1;
|
||||
groupElement.open = forceOpen || getSavedOpenState(groupElement.dataset.collapseKey);
|
||||
|
||||
const cards = items.map(createCardElement);
|
||||
const cards = items.map((item) => createCardElement(item, query));
|
||||
groupElement.innerHTML = `
|
||||
<summary class="group-head">
|
||||
<div>
|
||||
<p class="group-number">${group.number}</p>
|
||||
<h3>${group.title}</h3>
|
||||
<p>${group.description}</p>
|
||||
<p class="group-number">${highlightText(group.number, query)}</p>
|
||||
<h3>${highlightText(group.title, query)}</h3>
|
||||
<p>${highlightText(group.description, query)}</p>
|
||||
</div>
|
||||
<span class="group-meta">
|
||||
<span class="group-count">${items.length}</span>
|
||||
@ -334,7 +371,7 @@ function createGroupElement(group, items, forceOpen = false) {
|
||||
return groupElement;
|
||||
}
|
||||
|
||||
function createCardElement(item) {
|
||||
function createCardElement(item, query = "") {
|
||||
const meta = serviceMeta[item.type] || serviceMeta.config;
|
||||
const link = document.createElement("a");
|
||||
link.className = "nav-card";
|
||||
@ -354,10 +391,10 @@ function createCardElement(item) {
|
||||
<span class="shine"></span>
|
||||
<img src="${meta.icon}" alt="" loading="lazy" />
|
||||
<div class="card-content">
|
||||
<p class="item-number">${item.number}</p>
|
||||
<h4>${item.title}</h4>
|
||||
<p class="item-type">${meta.label}</p>
|
||||
<div class="tags">${item.tags.map((tag) => `<span>${tag}</span>`).join("")}</div>
|
||||
<p class="item-number">${highlightText(item.number, query)}</p>
|
||||
<h4>${highlightText(item.title, query)}</h4>
|
||||
<p class="item-type">${highlightText(meta.label, query)}</p>
|
||||
<div class="tags">${item.tags.map((tag) => `<span>${highlightText(tag, query)}</span>`).join("")}</div>
|
||||
</div>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
`;
|
||||
@ -387,6 +424,7 @@ function matchesQuery(item, group, section, query) {
|
||||
}
|
||||
|
||||
function renderNavigationTree() {
|
||||
const query = normalize(searchInput.value);
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
ecosystemSections.forEach((section) => {
|
||||
@ -399,7 +437,7 @@ function renderNavigationTree() {
|
||||
sectionSummary.innerHTML = `
|
||||
<span class="tree-caret" aria-hidden="true"></span>
|
||||
<button class="tree-node tree-folder" type="button" data-focus-target="${getSectionId(section)}">
|
||||
<span>${section.title}</span>
|
||||
<span>${highlightText(section.title, query)}</span>
|
||||
</button>
|
||||
`;
|
||||
sectionNode.append(sectionSummary);
|
||||
@ -414,7 +452,7 @@ function renderNavigationTree() {
|
||||
groupSummary.innerHTML = `
|
||||
<span class="tree-caret" aria-hidden="true"></span>
|
||||
<button class="tree-node tree-folder" type="button" data-focus-target="${getGroupId(group)}">
|
||||
<span>${group.title}</span>
|
||||
<span>${highlightText(group.title, query)}</span>
|
||||
</button>
|
||||
`;
|
||||
groupNode.append(groupSummary);
|
||||
@ -427,7 +465,7 @@ function renderNavigationTree() {
|
||||
itemButton.type = "button";
|
||||
itemButton.className = "tree-node tree-file";
|
||||
itemButton.dataset.focusTarget = getItemId(item);
|
||||
itemButton.innerHTML = `<span>${item.title}</span><small>${serviceMeta[item.type]?.label || item.type}</small>`;
|
||||
itemButton.innerHTML = `<span>${highlightText(item.title, query)}</span><small>${highlightText(item.tags.join(", "), query)}</small>`;
|
||||
itemList.append(itemButton);
|
||||
});
|
||||
|
||||
@ -528,10 +566,16 @@ function applyPreferences(nextPreferences) {
|
||||
|
||||
document.documentElement.dataset.theme = nextPreferences.theme;
|
||||
document.documentElement.dataset.accent = nextPreferences.accent;
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${clamp(Number(nextPreferences.sidebarWidth) || defaultPreferences.sidebarWidth, 260, 560)}px`,
|
||||
);
|
||||
catalog.classList.remove("cards-view", "list-view", "compact-cards-view", "compact-list-view");
|
||||
catalog.classList.add(`${nextPreferences.view}-view`);
|
||||
document.documentElement.dataset.showNumbers = String(Boolean(nextPreferences.showNumbers));
|
||||
document.documentElement.dataset.showTreeTags = String(Boolean(nextPreferences.showTreeTags));
|
||||
showNumbersInput.checked = Boolean(nextPreferences.showNumbers);
|
||||
showTreeTagsInput.checked = Boolean(nextPreferences.showTreeTags);
|
||||
setActiveButton(themeButtons, "theme", nextPreferences.theme);
|
||||
setActiveButton(viewButtons, "view", nextPreferences.view);
|
||||
setActiveButton(accentButtons, "accent", nextPreferences.accent);
|
||||
@ -549,6 +593,38 @@ function normalize(value) {
|
||||
return value.toLowerCase().replaceAll("ё", "е").trim();
|
||||
}
|
||||
|
||||
function highlightText(value, query) {
|
||||
const text = String(value);
|
||||
const normalizedQuery = normalize(query);
|
||||
|
||||
if (!normalizedQuery) {
|
||||
return escapeHTML(text);
|
||||
}
|
||||
|
||||
const normalizedText = normalize(text);
|
||||
let cursor = 0;
|
||||
let result = "";
|
||||
let matchIndex = normalizedText.indexOf(normalizedQuery, cursor);
|
||||
|
||||
while (matchIndex !== -1) {
|
||||
result += escapeHTML(text.slice(cursor, matchIndex));
|
||||
result += `<mark class="search-mark">${escapeHTML(text.slice(matchIndex, matchIndex + normalizedQuery.length))}</mark>`;
|
||||
cursor = matchIndex + normalizedQuery.length;
|
||||
matchIndex = normalizedText.indexOf(normalizedQuery, cursor);
|
||||
}
|
||||
|
||||
return result + escapeHTML(text.slice(cursor));
|
||||
}
|
||||
|
||||
function escapeHTML(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function getSectionId(section) {
|
||||
return `section-${section.id}`;
|
||||
}
|
||||
|
||||
57
styles.css
57
styles.css
@ -14,6 +14,7 @@
|
||||
--accent-rgb: 139, 92, 246;
|
||||
--card-x: 50%;
|
||||
--card-y: 50%;
|
||||
--sidebar-width: 390px;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
@ -109,11 +110,42 @@ a {
|
||||
|
||||
.app-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 390px) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
grid-template-columns: var(--sidebar-width) 8px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.sidebar-resizer {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
width: 8px;
|
||||
height: calc(100vh - 40px);
|
||||
border-radius: 999px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
transition: background 160ms ease;
|
||||
}
|
||||
|
||||
.sidebar-resizer::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
border-radius: inherit;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.sidebar-resizer:hover::before,
|
||||
body.is-resizing-sidebar .sidebar-resizer::before {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
body.is-resizing-sidebar {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
@ -187,6 +219,17 @@ html:not([data-show-numbers="true"]) .item-number {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-show-tree-tags="false"] .tree-node small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-mark {
|
||||
border-radius: 4px;
|
||||
padding: 0 2px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
z-index: 1001;
|
||||
@ -604,6 +647,10 @@ html:not([data-show-numbers="true"]) .item-number {
|
||||
}
|
||||
|
||||
.tree-node small {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--accent);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
@ -1024,7 +1071,7 @@ html:not([data-show-numbers="true"]) .item-number {
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.app-layout {
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 8px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
@ -1046,6 +1093,10 @@ html:not([data-show-numbers="true"]) .item-number {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user