model-explorer / js /ui /graphSearch.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* GraphSearch - Tìm Kiếm Node Trong Đồ Thị
* Cung cấp ô tìm kiếm trong khu vực đồ thị, highlight và auto-zoom đến node tìm thấy.
* Requirements: 30.1, 30.2, 30.3, 30.4, 30.5, 30.6
*/
class GraphSearch {
/**
* @param {string} graphContainerSelector - CSS selector cho container đồ thị (ví dụ: '#graphContainer')
*/
constructor(graphContainerSelector) {
/** @type {string} */
this._graphContainerSelector = graphContainerSelector;
/** @type {HTMLElement|null} */
this._graphContainer = null;
/** @type {HTMLElement|null} */
this._searchContainer = null;
/** @type {HTMLInputElement|null} */
this._searchInput = null;
/** @type {HTMLElement|null} */
this._countDisplay = null;
/** @type {HTMLElement|null} */
this._noResultsDisplay = null;
/** @type {HTMLElement|null} */
this._navContainer = null;
/** @type {Array<string>} - List of matched node IDs */
this._results = [];
/** @type {number} - Current result index */
this._currentIndex = -1;
/** @type {number|null} - Debounce timer ID */
this._debounceTimer = null;
/** @type {number} - Debounce delay in ms */
this._debounceDelay = 300;
}
/**
* Khởi tạo: tạo ô tìm kiếm, gắn event listeners.
*/
init() {
this._graphContainer = document.querySelector(this._graphContainerSelector);
if (!this._graphContainer) {
console.warn('[GraphSearch] Graph container not found:', this._graphContainerSelector);
return;
}
this._createSearchUI();
this._bindEvents();
console.log('[GraphSearch] Initialized');
}
/**
* Tìm kiếm node theo từ khóa (tên hoặc opType).
* @param {string} query - Từ khóa tìm kiếm
* @returns {Array<string>} Danh sách node IDs khớp
*/
search(query) {
var cy = this._getCy();
this._clearHighlights();
this._results = [];
this._currentIndex = -1;
if (!query || !query.trim() || !cy) {
this._updateDisplay();
return this._results;
}
var lowerQuery = query.trim().toLowerCase();
cy.nodes().forEach(function (node) {
var label = (node.data('label') || '').toLowerCase();
var name = (node.data('name') || '').toLowerCase();
var opType = (node.data('opType') || '').toLowerCase();
if (label.indexOf(lowerQuery) !== -1 ||
name.indexOf(lowerQuery) !== -1 ||
opType.indexOf(lowerQuery) !== -1) {
this._results.push(node.id());
}
}.bind(this));
// Highlight all matched nodes
for (var i = 0; i < this._results.length; i++) {
var node = cy.getElementById(this._results[i]);
if (node && node.length > 0) {
node.addClass('search-highlighted');
}
}
// Navigate to first result
if (this._results.length > 0) {
this._currentIndex = 0;
this._goToCurrentResult();
}
this._updateDisplay();
return this._results;
}
/**
* Highlight và zoom đến node tiếp theo trong kết quả.
*/
next() {
if (this._results.length === 0) return;
this._currentIndex = (this._currentIndex + 1) % this._results.length;
this._goToCurrentResult();
this._updateDisplay();
}
/**
* Highlight và zoom đến node trước đó trong kết quả.
*/
prev() {
if (this._results.length === 0) return;
this._currentIndex = (this._currentIndex - 1 + this._results.length) % this._results.length;
this._goToCurrentResult();
this._updateDisplay();
}
/**
* Xóa tất cả highlight tìm kiếm.
*/
clearSearch() {
this._clearHighlights();
this._results = [];
this._currentIndex = -1;
if (this._searchInput) {
this._searchInput.value = '';
}
this._updateDisplay();
}
/**
* Hủy và dọn dẹp.
*/
destroy() {
if (this._debounceTimer) {
clearTimeout(this._debounceTimer);
this._debounceTimer = null;
}
this._clearHighlights();
if (this._searchContainer && this._searchContainer.parentNode) {
this._searchContainer.parentNode.removeChild(this._searchContainer);
}
this._searchContainer = null;
this._searchInput = null;
this._countDisplay = null;
this._noResultsDisplay = null;
this._navContainer = null;
this._results = [];
this._currentIndex = -1;
}
// ─── Private Helpers ───────────────────────────────────────────────────────
/**
* Get the Cytoscape instance from the app.
* @returns {cytoscape.Core|null}
* @private
*/
_getCy() {
try {
if (window._onnxApp && typeof window._onnxApp.getGraphVisualizer === 'function') {
var gv = window._onnxApp.getGraphVisualizer();
if (gv && gv._cy) {
return gv._cy;
}
}
} catch (e) {
console.warn('[GraphSearch] Could not access Cytoscape instance:', e);
}
return null;
}
/**
* Create the search UI elements and insert above the graph container.
* @private
*/
_createSearchUI() {
// Main container
var container = document.createElement('div');
container.className = 'graph-search-container';
// Search input
var input = document.createElement('input');
input.type = 'text';
input.className = 'graph-search-input';
input.placeholder = 'Tìm kiếm node (tên hoặc opType)...';
input.setAttribute('aria-label', 'Search graph nodes');
this._searchInput = input;
// Navigation container (hidden by default)
var navContainer = document.createElement('div');
navContainer.className = 'graph-search-nav';
navContainer.style.display = 'none';
this._navContainer = navContainer;
// Count display
var countDisplay = document.createElement('span');
countDisplay.className = 'graph-search-count';
this._countDisplay = countDisplay;
// Prev button
var prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.innerHTML = '<i class="fas fa-chevron-up"></i>';
prevBtn.title = 'Previous result';
prevBtn.setAttribute('aria-label', 'Previous search result');
prevBtn.addEventListener('click', this.prev.bind(this));
// Next button
var nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.innerHTML = '<i class="fas fa-chevron-down"></i>';
nextBtn.title = 'Next result';
nextBtn.setAttribute('aria-label', 'Next search result');
nextBtn.addEventListener('click', this.next.bind(this));
navContainer.appendChild(countDisplay);
navContainer.appendChild(prevBtn);
navContainer.appendChild(nextBtn);
// No results display (hidden by default)
var noResults = document.createElement('span');
noResults.className = 'graph-search-count';
noResults.textContent = 'Không tìm thấy kết quả';
noResults.style.display = 'none';
noResults.style.color = '#dc3545';
this._noResultsDisplay = noResults;
container.appendChild(input);
container.appendChild(navContainer);
container.appendChild(noResults);
// Insert above #graphContainer in the card body
var parent = this._graphContainer.parentNode;
if (parent) {
parent.insertBefore(container, this._graphContainer);
}
this._searchContainer = container;
}
/**
* Bind event listeners.
* @private
*/
_bindEvents() {
if (!this._searchInput) return;
this._searchInput.addEventListener('input', function () {
if (this._debounceTimer) {
clearTimeout(this._debounceTimer);
}
this._debounceTimer = setTimeout(function () {
var query = this._searchInput.value;
this.search(query);
}.bind(this), this._debounceDelay);
}.bind(this));
// Allow Enter to go to next result
this._searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
if (e.shiftKey) {
this.prev();
} else {
this.next();
}
}
if (e.key === 'Escape') {
this.clearSearch();
this._searchInput.blur();
}
}.bind(this));
}
/**
* Navigate to the current result node (highlight + zoom).
* @private
*/
_goToCurrentResult() {
var cy = this._getCy();
if (!cy || this._currentIndex < 0 || this._currentIndex >= this._results.length) return;
// Remove 'search-active' from all nodes
cy.nodes().removeClass('search-active');
var nodeId = this._results[this._currentIndex];
var node = cy.getElementById(nodeId);
if (node && node.length > 0) {
node.addClass('search-active');
cy.animate({
center: { eles: node },
zoom: 1.5,
duration: 300
});
}
}
/**
* Clear all search highlights from the graph.
* @private
*/
_clearHighlights() {
var cy = this._getCy();
if (!cy) return;
cy.nodes().removeClass('search-highlighted');
cy.nodes().removeClass('search-active');
}
/**
* Update the display (count, nav visibility, no-results message).
* @private
*/
_updateDisplay() {
var hasQuery = this._searchInput && this._searchInput.value.trim().length > 0;
var hasResults = this._results.length > 0;
// Navigation container
if (this._navContainer) {
this._navContainer.style.display = (hasQuery && hasResults) ? 'flex' : 'none';
}
// Count display
if (this._countDisplay) {
if (hasResults) {
this._countDisplay.textContent = (this._currentIndex + 1) + '/' + this._results.length;
} else {
this._countDisplay.textContent = '';
}
}
// No results message
if (this._noResultsDisplay) {
this._noResultsDisplay.style.display = (hasQuery && !hasResults) ? 'inline' : 'none';
}
}
}
// Export as global for browser usage
window.GraphSearch = GraphSearch;