Отзывы и предложения к софту от AleXStam
Поговорим о...
<div class="tree-container">
{{ partial "tree" (dict "nodes" .Page.Params.tree) }}
</div>
<div class="error-proof-tree">
{{ $level := .level | default 1 }}
<ul class="tree-ul level-{{ $level }}">
{{ range .nodes }}
<li class="tree-li {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}">
<div class="tree-line">
{{ if .children }}
<span class="tree-arrow">›</span>
{{ else }}
<span class="tree-dot">•</span>
{{ end }}
<span class="tree-name">{{ .name }}</span>
</div>
{{ if .children }}
{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}
{{ end }}
</li>
{{ end }}
</ul>
</div>

<style>
.error-proof-tree {
font-family: system-ui, sans-serif;
font-size: 14px;
line-height: 1.4;
}
.tree-ul {
list-style: none;
padding-left: 20px;
margin: 0;
}
.tree-li {
margin: 2px 0;
}
.tree-line {
display: flex;
align-items: center;
cursor: pointer;
padding: 2px 0;
}
.tree-arrow, .tree-dot {
width: 16px;
margin-right: 4px;
text-align: center;
}
.tree-arrow {
transition: transform 0.2s;
}
.tree-li.open > .tree-line > .tree-arrow {
transform: rotate(90deg);
}
.tree-li > .tree-ul {
display: none;
}
.tree-li.open > .tree-ul {
display: block;
}
</style>

<script>
document.addEventListener('DOMContentLoaded', function() {
// Инициализация состояний
document.querySelectorAll('.tree-li.has-children').forEach(item => {
const children = item.querySelector('.tree-ul');
if (item.classList.contains('open')) {
children.style.display = 'block';
} else {
children.style.display = 'none';
}
});

// Обработчик кликов
document.addEventListener('click', function(e) {
const line = e.target.closest('.tree-line');
if (!line) return;

const item = line.parentElement;
if (!item.classList.contains('has-children')) return;

const children = item.querySelector('.tree-ul');
const isOpen = children.style.display === 'block';

children.style.display = isOpen ? 'none' : 'block';
item.classList.toggle('open', !isOpen);
});
});
</script>
<div class="tree-container">
{{ partial "tree" (dict "nodes" .Page.Params.tree) }}
</div>
<div class="final-tree-solution">
<ul class="tree-root">
{{ range .nodes }}
{{ partial "tree-node" (dict "node" . "level" 1) }}
{{ end }}
</ul>
</div>

<style>
.final-tree-solution {
font-family: sans-serif;
font-size: 14px;
line-height: 1.4;
}
.tree-root {
list-style: none;
padding-left: 0;
margin: 0;
}
.tree-node > ul {
display: none;
padding-left: 20px;
}
.tree-node.open > ul {
display: block;
}
.tree-toggle {
cursor: pointer;
margin-right: 5px;
display: inline-block;
width: 15px;
}
.tree-content {
display: flex;
align-items: center;
padding: 2px 0;
}
</style>

<script>
document.addEventListener('DOMContentLoaded', function() {
// Инициализация состояний
document.querySelectorAll('.tree-node').forEach(node => {
const children = node.querySelector('ul');
if (children) {
children.style.display = node.classList.contains('open') ? 'block' : 'none';
}
});

// Обработчик кликов
document.addEventListener('click', function(e) {
const toggle = e.target.closest('.tree-toggle');
if (!toggle) return;

const node = toggle.closest('.tree-node');
const children = node.querySelector('ul');
if (!children) return;

const isOpen = children.style.display === 'block';
children.style.display = isOpen ? 'none' : 'block';
node.classList.toggle('open', !isOpen);
});
});
</script>
<li class="tree-node {{ if .node.children }}has-children{{ end }} {{ .node.state | default "closed" }}">
<div class="tree-content">
{{ if .node.children }}
<span class="tree-toggle">›</span>
{{ else }}
<span class="tree-toggle">•</span>
{{ end }}
<span class="tree-label">{{ .node.name }}</span>
</div>

{{ if .node.children }}
<ul>
{{ range .node.children }}
{{ partial "tree-node" (dict "node" . "level" (add $.level 1)) }}
{{ end }}
</ul>
{{ end }}
</li>
<div class="tree-container" style="padding: 12px; border: 1px solid #eee; border-radius: 8px;">
{{ partial "tree" (dict "nodes" .Page.Params.tree) }}
</div>
<div class="working-tree">
{{ $level := .level | default 1 }}
<ul class="tree-list level-{{ $level }}" data-level="{{ $level }}">
{{ range .nodes }}
<li class="tree-item {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}"
data-id="{{ md5 .name }}" data-name="{{ lower .name }}">
<div class="tree-line" onclick="toggleTreeItem(this.parentElement)">
{{ if .children }}<span class="tree-arrow">›</span>{{ else }}<span class="tree-dot">•</span>{{ end }}
<span class="tree-name">{{ .name }}</span>
</div>
{{ if .children }}{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}{{ end }}
</li>
{{ end }}
</ul>
</div>

<script>
// Базовая функция переключения
function toggleTreeItem(item, forceOpen = false) {
if (!item.classList.contains('has-children')) return;

if (forceOpen) {
item.classList.add('open');
item.querySelector('.tree-list').style.display = 'block';
} else {
item.classList.toggle('open');
item.querySelector('.tree-list').style.display = item.classList.contains('open') ? 'block' : 'none';
}
}

// Инициализация и обработка поиска
document.addEventListener('DOMContentLoaded', function() {
// Инициализация состояний
document.querySelectorAll('.tree-item.has-children').forEach(item => {
item.querySelector('.tree-list').style.display = item.classList.contains('open') ? 'block' : 'none';
});

// Обработка параметров URL (для поиска)
const urlParams = new URLSearchParams(window.location.search);
const searchTerm = urlParams.get('q')?.toLowerCase();

if (searchTerm) {
document.querySelectorAll('.tree-item').forEach(item => {
const itemName = item.getAttribute('data-name');
if (itemName.includes(searchTerm)) {
// Раскрываем всех родителей
let parent = item.closest('.tree-list').parentElement;
while (parent && parent.classList.contains('tree-item')) {
toggleTreeItem(parent, true);
parent = parent.closest('.tree-list')?.parentElement;
}
}
});
}
});
</script>

<style>
/* Ваши существующие стили остаются без изменений */
.working-tree { font-family: sans-serif; font-size: 14px; line-height: 1.4; }
.tree-list { list-style: none; padding-left: 20px; margin: 0; }
.tree-item { margin: 2px 0; }
.tree-line { display: flex; align-items: center; cursor: pointer; padding: 2px 0; }
.tree-arrow, .tree-dot { width: 16px; margin-right: 4px; text-align: center; }
.tree-arrow { transition: transform 0.2s; }
.tree-item.open > .tree-line > .tree-arrow { transform: rotate(90deg); color: #0066cc; }
.tree-item > .tree-list { display: none; }
.tree-item.open > .tree-list { display: block; }
</style>
// Search functionality using FlexSearch.

// Change shortcut key to cmd+k on Mac, iPad or iPhone.
document.addEventListener("DOMContentLoaded", function () {
if (/iPad|iPhone|Macintosh/.test(navigator.userAgent)) {
// select the kbd element under the .search-wrapper class
const keys = document.querySelectorAll(".search-wrapper kbd");
keys.forEach(key => {
key.innerHTML = '<span class="hx-text-xs">⌘</span>K';
});
}
});

// Render the search data as JSON.
//
//
//
//

(function () {
const searchDataURL = '/hugo/ru.search-data.json';

const inputElements = document.querySelectorAll('.search-input');
for (const el of inputElements) {
el.addEventListener('focus', init);
el.addEventListener('keyup', search);
el.addEventListener('keydown', handleKeyDown);
el.addEventListener('input', handleInputChange);
}

const shortcutElements = document.querySelectorAll('.search-wrapper kbd');

function setShortcutElementsOpacity(opacity) {
shortcutElements.forEach(el => {
el.style.opacity = opacity;
});
}

function handleInputChange(e) {
const opacity = e.target.value.length > 0 ? 0 : 100;
setShortcutElementsOpacity(opacity);
}

// Get the search wrapper, input, and results elements.
function getActiveSearchElement() {
const inputs = Array.from(document.querySelectorAll('.search-wrapper')).filter(el => el.clientHeight > 0);
if (inputs.length === 1) {
return {
wrapper: inputs[0],
inputElement: inputs[0].querySelector('.search-input'),
resultsElement: inputs[0].querySelector('.search-results')
};
}
return undefined;
}

const INPUTS = ['input', 'select', 'button', 'textarea']

// Focus the search input when pressing ctrl+k/cmd+k or /.
document.addEventListener('keydown', function (e) {
const { inputElement } = getActiveSearchElement();
if (!inputElement) return;

const activeElement = document.activeElement;
const tagName = activeElement && activeElement.tagName;
if (
inputElement === activeElement ||
!tagName ||
INPUTS.includes(tagName) ||
(activeElement && activeElement.isContentEditable))
return;

if (
e.key === '/' ||
(e.key === 'k' &&
(e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey))
) {
e.preventDefault();
inputElement.focus();
} else if (e.key === 'Escape' && inputElement.value) {
inputElement.blur();
}
});

// Dismiss the search results when clicking outside the search box.
document.addEventListener('mousedown', function (e) {
const { inputElement, resultsElement } = getActiveSearchElement();
if (!inputElement || !resultsElement) return;
if (
e.target !== inputElement &&
e.target !== resultsElement &&
!resultsElement.contains(e.target)
) {
setShortcutElementsOpacity(100);
hideSearchResults();
}
});

// Get the currently active result and its index.
function getActiveResult() {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return { result: undefined, index: -1 };

const result = resultsElement.querySelector('.active');
if (!result) return { result: undefined, index: -1 };

const index = parseInt(result.dataset.index, 10);
return { result, index };
}

// Set the active result by index.
function setActiveResult(index) {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return;

const { result: activeResult } = getActiveResult();
activeResult && activeResult.classList.remove('active');
const result = resultsElement.querySelector(`[data-index="${index}"]`);
if (result) {
result.classList.add('active');
result.focus();
}
}

// Get the number of search results from the DOM.
function getResultsLength() {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return 0;
return resultsElement.dataset.count;
}

// Finish the search by hiding the results and clearing the input.
function finishSearch() {
const { inputElement } = getActiveSearchElement();
if (!inputElement) return;
hideSearchResults();
inputElement.value = '';
inputElement.blur();
}

function hideSearchResults() {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return;
resultsElement.classList.add('hx-hidden');
}

// Handle keyboard events.
function handleKeyDown(e) {
const { inputElement } = getActiveSearchElement();
if (!inputElement) return;

const resultsLength = getResultsLength();
const { result: activeResult, index: activeIndex } = getActiveResult();

switch (e.key) {
case 'ArrowUp':
e.preventDefault();
if (activeIndex > 0) setActiveResult(activeIndex - 1);
break;
case 'ArrowDown':
e.preventDefault();
if (activeIndex + 1 < resultsLength) setActiveResult(activeIndex + 1);
break;
case 'Enter':
e.preventDefault();
if (activeResult) {
activeResult.click();
}
finishSearch();
case 'Escape':
e.preventDefault();
hideSearchResults();
// Clear the input when pressing escape
inputElement.value = '';
inputElement.dispatchEvent(new Event('input'));
// Remove focus from the input
inputElement.blur();
break;
}
}

// Initializes the search.
function init(e) {
e.target.removeEventListener('focus', init);
if (!(window.pageIndex && window.sectionIndex)) {
preloadIndex();
}
}

/**
* Preloads the search index by fetching data and adding it to the FlexSearch index.
* @returns {Promise<void>} A promise that resolves when the index is preloaded.
*/
async function preloadIndex() {
const tokenize = 'forward';

const isCJK = () => {
const lang = document.documentElement.lang || "en";
return lang.startsWith("zh") || lang.startsWith("ja") || lang.startsWith("ko");
}

const encodeCJK = (str) => str.replace(/[\x00-\x7F]/g, "").split("");
const encodeDefault = (str) => (""+str).toLocaleLowerCase().split(/[\p{Z}\p{S}\p{P}\p{C}]+/u);
const encodeFunction = isCJK() ? encodeCJK : encodeDefault;

window.pageIndex = new FlexSearch.Document({
tokenize,
encode: encodeFunction,
cache: 100,
document: {
id: 'id',
store: ['title', 'crumb'],
index: "content"
}
});

window.sectionIndex = new FlexSearch.Document({
tokenize,
encode: encodeFunction,
cache: 100,
document: {
id: 'id',
store: ['title', 'content', 'url', 'display', 'crumb'],
index: "content",
tag: 'pageId'
}
});

const resp = await fetch(searchDataURL);
const data = await resp.json();
let pageId = 0;
for (const route in data) {
let pageContent = '';
++pageId;
const urlParts = route.split('/').filter(x => x != "" && !x.startsWith('#'));

let crumb = '';
let searchUrl = '/'
for (let i = 0; i < urlParts.length; i++) {
const urlPart = urlParts[i];
searchUrl += urlPart + '/'

const crumbData = data[searchUrl];
if (!crumbData) {
console.warn('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route);
continue;
}

let title = data[searchUrl].title;
if (title == "_index") {
title = urlPart.split("-").map(x => x).join(" ");
}
crumb += title;

if (i < urlParts.length - 1) {
crumb += ' > ';
}
}

for (const heading in data[route].data) {
const [hash, text] = heading.split('#');
const url = route.trimEnd('/') + (hash ? '#' + hash : '');
const title = text || data[route].title;

const content = data[route].data[heading] || '';
const paragraphs = content.split('\n').filter(Boolean);

sectionIndex.add({
id: url,
url,
title,
crumb,
pageId: `page_${pageId}`,
content: title,
...(paragraphs[0] && { display: paragraphs[0] })
});

for (let i = 0; i < paragraphs.length; i++) {
sectionIndex.add({
id: `${url}_${i}`,
url,
title,
crumb,
pageId: `page_${pageId}`,
content: paragraphs[i]
});
}

pageContent += ` ${title} ${content}`;
}

window.pageIndex.add({
id: pageId,
title: data[route].title,
crumb,
content: pageContent
});

}
}

/**
* Performs a search based on the provided query and displays the results.
* @param {Event} e - The event object.
*/
function search(e) {
const query = e.target.value;
if (!e.target.value) {
hideSearchResults();
return;
}

const { resultsElement } = getActiveSearchElement();
while (resultsElement.firstChild) {
resultsElement.removeChild(resultsElement.firstChild);
}
resultsElement.classList.remove('hx-hidden');

const pageResults = window.pageIndex.search(query, 5, { enrich: true, suggest: true })[0]?.result || [];

const results = [];
const pageTitleMatches = {};

for (let i = 0; i < pageResults.length; i++) {
const result = pageResults[i];
pageTitleMatches[i] = 0;

// Show the top 5 results for each page
const sectionResults = window.sectionIndex.search(query, 5, { enrich: true, suggest: true, tag: `page_${result.id}` })[0]?.result || [];
let isFirstItemOfPage = true
const occurred = {}

for (let j = 0; j < sectionResults.length; j++) {
const { doc } = sectionResults[j]
const isMatchingTitle = doc.display !== undefined
if (isMatchingTitle) {
pageTitleMatches[i]++
}
const { url, title } = doc
const content = doc.display || doc.content

if (occurred[url + '@' + content]) continue
occurred[url + '@' + content] = true
results.push({
_page_rk: i,
_section_rk: j,
route: url,
prefix: isFirstItemOfPage ? result.doc.crumb : undefined,
children: { title, content }
})
isFirstItemOfPage = false
}
}
const sortedResults = results
.sort((a, b) => {
// Sort by number of matches in the title.
if (a._page_rk === b._page_rk) {
return a._section_rk - b._section_rk
}
if (pageTitleMatches[a._page_rk] !== pageTitleMatches[b._page_rk]) {
return pageTitleMatches[b._page_rk] - pageTitleMatches[a._page_rk]
}
return a._page_rk - b._page_rk
})
.map(res => ({
id: `${res._page_rk}_${res._section_rk}`,
route: res.route,
prefix: res.prefix,
children: res.children
}));
displayResults(sortedResults, query);
}

/**
* Displays the search results on the page.
*
* @param {Array} results - The array of search results.
* @param {string} query - The search query.
*/
function displayResults(results, query) {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return;

if (!results.length) {
resultsElement.innerHTML = `<span class="no-result">Ничего не найдено.</span>`;
return;
}

// Highlight the query in the result text.
function highlightMatches(text, query) {
const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
const regex = new RegExp(escapedQuery, 'gi');
return text.replace(regex, (match) => `<span class="match">${match}</span>`);
}

// Create a DOM element from the HTML string.
function createElement(str) {
const div = document.createElement('div');
div.innerHTML = str.trim();
return div.firstChild;
}

function handleMouseMove(e) {
const target = e.target.closest('a');
if (target) {
const active = resultsElement.querySelector('a.active');
if (active) {
active.classList.remove('active');
}
target.classList.add('active');
}
}

const fragment = document.createDocumentFragment();
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.prefix) {
fragment.appendChild(createElement(`
<div class="prefix">${result.prefix}</div>`));
}
let li = createElement(`
<li>
<a data-index="${i}" href="${result.route}" class=${i === 0 ? "active" : ""}>
<div class="title">`+ highlightMatches(result.children.title, query) + `</div>` +
(result.children.content ?
`<div class="excerpt">` + highlightMatches(result.children.content, query) + `</div>` : '') + `
</a>
</li>`);
li.addEventListener('mousemove', handleMouseMove);
li.addEventListener('keydown', handleKeyDown);
li.querySelector('a').addEventListener('click', finishSearch);
fragment.appendChild(li);
}
resultsElement.appendChild(fragment);
resultsElement.dataset.count = results.length;
}
})();
<div class="searchable-tree">
{{ $level := .level | default 1 }}
<ul class="tree-list level-{{ $level }}">
{{ range .nodes }}
<li class="tree-item {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}"
data-search-name="{{ lower .name }}">
<div class="tree-line" onclick="toggleTreeItem(this.parentElement)">
{{ if .children }}<span class="tree-arrow">›</span>{{ else }}<span class="tree-dot">•</span>{{ end }}
<span class="tree-name">{{ .name }}</span>
</div>
{{ if .children }}{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}{{ end }}
</li>
{{ end }}
</ul>
</div>

<script>
// Базовая функция переключения
function toggleTreeItem(item, forceState) {
if (!item.classList.contains('has-children')) return;

if (typeof forceState !== 'undefined') {
if (forceState) {
item.classList.add('open');
item.querySelector('.tree-list').style.display = 'block';
} else {
item.classList.remove('open');
item.querySelector('.tree-list').style.display = 'none';
}
} else {
const wasOpen = item.classList.contains('open');
item.classList.toggle('open', !wasOpen);
item.querySelector('.tree-list').style.display = wasOpen ? 'none' : 'block';
}
}

// Функция для раскрытия дерева при поиске
function expandTreeForSearch(searchTerm) {
const term = searchTerm.toLowerCase();
if (!term) return;

document.querySelectorAll('.tree-item').forEach(item => {
const itemName = item.getAttribute('data-search-name');
if (itemName.includes(term)) {
// Раскрываем всех родителей
let parent = item.closest('.tree-list').parentElement;
while (parent && parent.classList.contains('tree-item')) {
toggleTreeItem(parent, true);
parent = parent.closest('.tree-list')?.parentElement;
}

// Прокручиваем к найденному элементу
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
}

// Инициализация дерева
document.addEventListener('DOMContentLoaded', function() {
// Стандартная инициализация
document.querySelectorAll('.tree-item.has-children').forEach(item => {
item.querySelector('.tree-list').style.display = item.classList.contains('open') ? 'block' : 'none';
});

// Интеграция с поиском Hugo
const searchParams = new URLSearchParams(window.location.search);
const searchQuery = searchParams.get('q');
if (searchQuery) {
expandTreeForSearch(searchQuery);
}

// Отслеживание изменений в поиске (для динамического поиска)
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
expandTreeForSearch(e.target.value);
});
}
});
</script>

<style>
.searchable-tree { font-family: sans-serif; font-size: 14px; line-height: 1.4; }
.tree-list { list-style: none; padding-left: 20px; margin: 0; }
.tree-item { margin: 2px 0; }
.tree-line { display: flex; align-items: center; cursor: pointer; padding: 2px 0; }
.tree-arrow, .tree-dot { width: 16px; margin-right: 4px; text-align: center; }
.tree-arrow { transition: transform 0.2s; }
.tree-item.open > .tree-line > .tree-arrow { transform: rotate(90deg); color: #0066cc; }
.tree-item > .tree-list { display: none; }
.tree-item.open > .tree-list { display: block; }
</style>
<div class="reliable-tree">
{{ $level := .level | default 1 }}
<ul class="tree-ul level-{{ $level }}">
{{ range .nodes }}
<li class="tree-node {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}"
data-search-text="{{ lower .name }}">
<div class="tree-head">
{{ if .children }}
<span class="tree-toggle">›</span>
{{ else }}
<span class="tree-bullet">•</span>
{{ end }}
<span class="tree-title">{{ .name }}</span>
</div>
{{ if .children }}
{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}
{{ end }}
</li>
{{ end }}
</ul>
</div>

<script>
(function() {
// Функция для раскрытия/закрытия узла
function toggleTreeNode(node, forceOpen) {
if (!node.classList.contains('has-children')) return;

const childrenList = node.querySelector('ul');
if (typeof forceOpen !== 'undefined') {
if (forceOpen) {
node.classList.add('open');
childrenList.style.display = 'block';
} else {
node.classList.remove('open');
childrenList.style.display = 'none';
}
} else {
const isOpen = node.classList.contains('open');
node.classList.toggle('open', !isOpen);
childrenList.style.display = isOpen ? 'none' : 'block';
}
}

// Функция для раскрытия пути к найденному элементу
function expandTreeForSearch(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) return;

document.querySelectorAll('.tree-node').forEach(node => {
const nodeText = node.getAttribute('data-search-text');
if (nodeText.includes(term)) {
// Раскрываем всех родителей
let parent = node.parentElement.closest('.tree-node');
while (parent) {
toggleTreeNode(parent, true);
parent = parent.parentElement.closest('.tree-node');
}

// Прокручиваем к элементу
setTimeout(() => {
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
}
});
}

// Инициализация дерева
function initTree() {
// Устанавливаем начальные состояния
document.querySelectorAll('.tree-node.has-children').forEach(node => {
const children = node.querySelector('ul');
if (node.classList.contains('open')) {
children.style.display = 'block';
} else {
children.style.display = 'none';
}
});

// Обработчики кликов
document.querySelectorAll('.tree-head').forEach(head => {
head.addEventListener('click', function() {
toggleTreeNode(this.parentElement);
});
});

// Интеграция с поиском
const urlSearch = new URLSearchParams(window.location.search).get('q');
if (urlSearch) expandTreeForSearch(urlSearch);
}

// Запускаем инициализацию
if (document.readyState === 'complete') {
initTree();
} else {
document.addEventListener('DOMContentLoaded', initTree);
}

// Экспортируем функцию для вызова из поиска
window.expandTreeForSearch = expandTreeForSearch;
})();
</script>

<style>
.reliable-tree {
font-family: -apple-system, sans-serif;
font-size: 14px;
line-height: 1.4;
color: #333;
}

.tree-ul {
list-style: none;
padding-left: 20px;
margin: 0;
}

.tree-node {
margin: 2px 0;
}

.tree-head {
display: flex;
align-items: center;
padding: 3px 0;
cursor: pointer;
}

.tree-toggle, .tree-bullet {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
}

.tree-toggle {
font-size: 14px;
color: #666;
transition: transform 0.2s;
}

.tree-bullet {
color: #999;
font-size: 16px;
}

.tree-node > .tree-ul {
display: none;
}

.tree-node.open > .tree-ul {
display: block;
}

.tree-node.open > .tree-head > .tree-toggle {
transform: rotate(90deg);
color: #0066cc;
}

.tree-head:hover {
color: #0066cc;
}
</style>
<div class="interactive-tree">
{{ $level := .level | default 1 }}
<ul class="tree-list level-{{ $level }}">
{{ range .nodes }}
<li class="tree-item {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}"
data-search-name="{{ lower .name }}">
<div class="tree-line">
{{ if .children }}<span class="tree-arrow">›</span>{{ else }}<span class="tree-dot">•</span>{{ end }}
<span class="tree-name">{{ .name }}</span>
</div>
{{ if .children }}{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}{{ end }}
</li>
{{ end }}
</ul>
</div>

<script>
// Глобальная функция для раскрытия дерева
window.expandTreeForSearch = function(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) return;

// Сначала закрываем все узлы
document.querySelectorAll('.tree-item.has-children').forEach(item => {
item.classList.remove('open');
item.querySelector('.tree-list').style.display = 'none';
});

// Находим и раскрываем соответствующие узлы
document.querySelectorAll('.tree-item').forEach(item => {
const itemName = item.getAttribute('data-search-name');
if (itemName.includes(term)) {
// Раскрываем всех родителей
let parent = item.parentElement.closest('.tree-item');
while (parent) {
parent.classList.add('open');
parent.querySelector('.tree-list').style.display = 'block';
parent = parent.parentElement.closest('.tree-item');
}
}
});
};

// Инициализация дерева
document.addEventListener('DOMContentLoaded', function() {
// Обработчики кликов
document.querySelectorAll('.tree-line').forEach(line => {
line.addEventListener('click', function() {
const item = this.parentElement;
if (item.classList.contains('has-children')) {
const isOpen = item.classList.contains('open');
item.classList.toggle('open', !isOpen);
item.querySelector('.tree-list').style.display = isOpen ? 'none' : 'block';
}
});
});

// Обработка URL параметра
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get('q');
if (searchQuery) {
window.expandTreeForSearch(searchQuery);
}
});
</script>

<style>
.interactive-tree { font-family: sans-serif; font-size: 14px; }
.tree-list { list-style: none; padding-left: 20px; margin: 0; }
.tree-item { margin: 2px 0; }
.tree-line { display: flex; align-items: center; cursor: pointer; padding: 2px 0; }
.tree-arrow, .tree-dot { width: 16px; margin-right: 5px; }
.tree-arrow { transition: transform 0.2s; }
.tree-item.open > .tree-line > .tree-arrow { transform: rotate(90deg); color: #0066cc; }
.tree-item > .tree-list { display: none; }
.tree-item.open > .tree-list { display: block; }
</style>
function displayResults(results, query) {
// ... ваш существующий код ...

// Добавьте этот вызов:
if (window.expandTreeForSearch) {
window.expandTreeForSearch(query);
}
}

// Для мгновенного поиска при вводе
document.querySelector('.search-input').addEventListener('input', function(e) {
if (window.expandTreeForSearch) {
window.expandTreeForSearch(e.target.value);
}
});
<div class="smart-tree">
{{ $level := .level | default 1 }}
<ul class="tree-ul level-{{ $level }}">
{{ range .nodes }}
<li class="tree-node {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}"
data-search-name="{{ lower .name }}">
<div class="tree-head">
{{ if .children }}
<span class="tree-toggle">›</span>
{{ else }}
<span class="tree-bullet">•</span>
{{ end }}
<span class="tree-title">{{ .name }}</span>
</div>
{{ if .children }}
{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}
{{ end }}
</li>
{{ end }}
</ul>
</div>

<script>
(function() {
// Функция управления узлами
function toggleTreeNode(node, forceState) {
if (!node.classList.contains('has-children')) return;

const childrenList = node.querySelector('ul');
if (typeof forceState !== 'undefined') {
node.classList.toggle('open', forceState);
childrenList.style.display = forceState ? 'block' : 'none';
} else {
const isOpen = node.classList.contains('open');
node.classList.toggle('open', !isOpen);
childrenList.style.display = isOpen ? 'none' : 'block';
}
}

// Функция поиска
window.highlightTreeSearch = function(searchTerm) {
const term = searchTerm.toLowerCase().trim();

// Сначала закрываем все узлы (кроме изначально открытых)
document.querySelectorAll('.tree-node.has-children').forEach(node => {
if (node.classList.contains('open') && !node.hasAttribute('data-keep-open')) {
toggleTreeNode(node, false);
}
});

if (!term) return;

// Находим и раскрываем совпадения
document.querySelectorAll('.tree-node').forEach(node => {
const nodeText = node.getAttribute('data-search-name');
if (nodeText.includes(term)) {
// Раскрываем родителей
let parent = node.parentElement.closest('.tree-node');
while (parent) {
toggleTreeNode(parent, true);
parent.setAttribute('data-keep-open', '');
parent = parent.parentElement.closest('.tree-node');
}
}
});
};

// Инициализация
document.addEventListener('DOMContentLoaded', function() {
// Закрываем все узлы по умолчанию
document.querySelectorAll('.tree-node.has-children').forEach(node => {
if (!node.classList.contains('open')) {
node.querySelector('ul').style.display = 'none';
}
});

// Обработчики кликов
document.querySelectorAll('.tree-head').forEach(head => {
head.addEventListener('click', function() {
const node = this.parentElement;
toggleTreeNode(node);
});
});

// Обработка URL параметра
const urlSearch = new URLSearchParams(window.location.search).get('q');
if (urlSearch) highlightTreeSearch(urlSearch);
});
})();
</script>

<style>
.smart-tree {
font-family: -apple-system, sans-serif;
font-size: 14px;
line-height: 1.4;
}

.tree-ul {
list-style: none;
padding-left: 20px;
margin: 0;
}

.tree-node {
margin: 2px 0;
}

.tree-head {
display: flex;
align-items: center;
padding: 3px 0;
cursor: pointer;
}

.tree-toggle, .tree-bullet {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
}

.tree-toggle {
transition: transform 0.2s;
}

.tree-node.open > .tree-head > .tree-toggle {
transform: rotate(90deg);
}

.tree-node > .tree-ul {
display: none;
}

.tree-node.open > .tree-ul {
display: block;
}
</style>
function displayResults(results, query) {
// ... ваш существующий код ...

// Добавьте этот вызов:
if (window.highlightTreeSearch) {
window.highlightTreeSearch(query);
}
}

// Для мгновенного поиска
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
if (window.highlightTreeSearch) {
window.highlightTreeSearch(e.target.value);
}
});
}
<div class="dynamic-tree">
{{ $level := .level | default 1 }}
<ul class="tree-ul level-{{ $level }}">
{{ range .nodes }}
<li class="tree-node {{ if .children }}has-children{{ end }} {{ .state | default "closed" }}"
data-search-text="{{ lower .name }}">
<div class="tree-head">
{{ if .children }}
<span class="tree-toggle">›</span>
{{ else }}
<span class="tree-bullet">•</span>
{{ end }}
<span class="tree-title">{{ .name }}</span>
</div>
{{ if .children }}
{{ partial "tree" (dict "nodes" .children "level" (add $level 1)) }}
{{ end }}
</li>
{{ end }}
</ul>
</div>

<script>
(function() {
// Глобальная функция для управления деревом
window.manageTreeSearch = function(searchQuery) {
const term = searchQuery.toLowerCase().trim();

// 1. Сброс предыдущего поиска
document.querySelectorAll('.tree-node').forEach(node => {
node.classList.remove('search-match');
if (!node.hasAttribute('data-initially-open')) {
node.classList.remove('open');
const children = node.querySelector('.tree-ul');
if (children) children.style.display = 'none';
}
});

// 2. Если поиск пустой - выходим
if (!term) return;

// 3. Поиск и раскрытие совпадений
document.querySelectorAll('.tree-node').forEach(node => {
const nodeText = node.getAttribute('data-search-text');
if (nodeText.includes(term)) {
node.classList.add('search-match');

// Раскрываем всех родителей
let parent = node.parentElement.closest('.tree-node');
while (parent) {
parent.classList.add('open');
const children = parent.querySelector('.tree-ul');
if (children) children.style.display = 'block';
parent = parent.parentElement.closest('.tree-node');
}
}
});
};

// Инициализация
document.addEventListener('DOMContentLoaded', function() {
// Помечаем изначально открытые узлы
document.querySelectorAll('.tree-node.open').forEach(node => {
node.setAttribute('data-initially-open', '');
});

// Обработчики кликов
document.querySelectorAll('.tree-head').forEach(head => {
head.addEventListener('click', function() {
const node = this.parentElement;
if (node.classList.contains('has-children')) {
const wasOpen = node.classList.contains('open');
node.classList.toggle('open', !wasOpen);
const children = node.querySelector('.tree-ul');
if (children) children.style.display = wasOpen ? 'none' : 'block';
}
});
});

// Интеграция с поиском при загрузке
const urlSearch = new URLSearchParams(window.location.search).get('q');
if (urlSearch) manageTreeSearch(urlSearch);
});
})();
</script>

<style>
.dynamic-tree {
font-family: -apple-system, sans-serif;
font-size: 14px;
line-height: 1.4;
}

.tree-ul {
list-style: none;
padding-left: 20px;
margin: 0;
}

.tree-node {
margin: 2px 0;
}

.tree-head {
display: flex;
align-items: center;
padding: 3px 0;
cursor: pointer;
}

.tree-toggle {
transition: transform 0.2s;
}

.tree-node.open > .tree-head > .tree-toggle {
transform: rotate(90deg);
}

.tree-node > .tree-ul {
display: none;
}

.tree-node.open > .tree-ul,
.tree-node[data-initially-open] > .tree-ul {
display: block;
}

.tree-node.search-match > .tree-head > .tree-title {
background-color: #fff8c5;
border-radius: 3px;
padding: 0 2px;
}
</style>
Поиск:
Новый ответ
Имя:
Текст сообщения: