'use strict'; (function defineSidebarController(root, factory) { const treeApi = typeof module !== 'undefined' && module.exports ? require('./device-tree') : root && root.AptDeviceTree; const api = factory(treeApi); if (typeof module !== 'undefined' && module.exports) module.exports = api; if (root) root.AptSidebarController = api; }(typeof window !== 'undefined' ? window : undefined, function sidebarFactory(treeApi) { function validKey(key) { return typeof key === 'string' && /^(site|group|camera):[^\s]+$/.test(key); } function copySet(value) { return value instanceof Set ? new Set(value) : new Set(); } function createInitialState(initial = {}) { return { expandedKeys: copySet(initial.expandedKeys), activeKey: validKey(initial.activeKey) ? initial.activeKey : null, selectedKey: validKey(initial.selectedKey) ? initial.selectedKey : null, searchQuery: typeof initial.searchQuery === 'string' ? initial.searchQuery : '', searchCameraKeys: initial.searchCameraKeys instanceof Set ? new Set(initial.searchCameraKeys) : null, searchExpandedKeys: initial.searchExpandedKeys instanceof Set ? new Set(initial.searchExpandedKeys) : null, searchGeneration: Number.isInteger(initial.searchGeneration) ? initial.searchGeneration : 0, scrollTop: Number.isFinite(initial.scrollTop) ? Math.max(0, initial.scrollTop) : 0, viewportHeight: Number.isFinite(initial.viewportHeight) ? Math.max(0, initial.viewportHeight) : 320, }; } function findRow(rows, key) { if (!validKey(key) || !Array.isArray(rows)) return null; return rows.find((row) => row && row.key === key) || null; } function expansionState(state) { return state.searchQuery && state.searchExpandedKeys instanceof Set ? { field: 'searchExpandedKeys', keys: state.searchExpandedKeys } : { field: 'expandedKeys', keys: state.expandedKeys }; } function changeExpansion(state, row, force) { if (!row || row.kind === 'camera') return state; const current = expansionState(state); const expandedKeys = copySet(current.keys); const shouldExpand = force === undefined ? !expandedKeys.has(row.key) : force; if (shouldExpand) expandedKeys.add(row.key); else expandedKeys.delete(row.key); return Object.assign({}, state, { [current.field]: expandedKeys, activeKey: row.key }); } function activate(state, row) { if (!row) return state; if (row.kind === 'camera') return Object.assign({}, state, { activeKey: row.key, selectedKey: row.key }); return changeExpansion(state, row); } function moveActive(state, rows, targetIndex) { if (!rows.length) return state; const index = Math.max(0, Math.min(rows.length - 1, targetIndex)); return Object.assign({}, state, { activeKey: rows[index].key }); } function reduceSidebar(state, action, rows = []) { if (!state || !action || typeof action.type !== 'string') return state; if (action.type === 'ACTIVATE' || action.type === 'CLICK') { return activate(state, findRow(rows, action.rowKey)); } if (action.type === 'SET_SELECTION') { if (!validKey(action.rowKey) || !action.rowKey.startsWith('camera:') || (!action.allowHidden && !findRow(rows, action.rowKey))) return state; return Object.assign({}, state, { selectedKey: action.rowKey, activeKey: action.rowKey }); } if (action.type === 'SET_ACTIVE') { const row = findRow(rows, action.rowKey); return row ? Object.assign({}, state, { activeKey: row.key }) : state; } if (action.type === 'SET_VIEWPORT') { const scrollTop = Number.isFinite(action.scrollTop) ? Math.max(0, action.scrollTop) : state.scrollTop; const viewportHeight = Number.isFinite(action.viewportHeight) ? Math.max(0, action.viewportHeight) : state.viewportHeight; if (scrollTop === state.scrollTop && viewportHeight === state.viewportHeight) return state; return Object.assign({}, state, { scrollTop, viewportHeight }); } if (action.type === 'SET_SEARCH_QUERY') { const searchQuery = typeof action.query === 'string' ? action.query : ''; return Object.assign({}, state, { searchQuery, searchCameraKeys: searchQuery ? state.searchCameraKeys : null, searchExpandedKeys: searchQuery ? state.searchExpandedKeys : null, }); } if (action.type === 'SET_SEARCH_RESULTS') { if (!Number.isInteger(action.generation) || action.generation < state.searchGeneration) return state; return Object.assign({}, state, { searchQuery: typeof action.query === 'string' ? action.query : state.searchQuery, searchCameraKeys: action.cameraKeys instanceof Set ? new Set(action.cameraKeys) : null, searchExpandedKeys: action.expandedKeys instanceof Set ? new Set(action.expandedKeys) : null, searchGeneration: action.generation, }); } if (action.type !== 'KEY' || typeof action.key !== 'string') return state; if (action.key === 'Escape') { if (!state.searchQuery && !state.searchCameraKeys) return state; return Object.assign({}, state, { searchQuery: '', searchCameraKeys: null, searchExpandedKeys: null }); } if (!rows.length) return state; let index = rows.findIndex((row) => row.key === state.activeKey); if (index < 0) { if (action.key === 'ArrowDown' || action.key === 'Home') return moveActive(state, rows, 0); if (action.key === 'ArrowUp' || action.key === 'End') return moveActive(state, rows, rows.length - 1); index = 0; } const row = rows[index]; const expandedKeys = expansionState(state).keys; switch (action.key) { case 'ArrowDown': return moveActive(state, rows, index + 1); case 'ArrowUp': return moveActive(state, rows, index - 1); case 'Home': return moveActive(state, rows, 0); case 'End': return moveActive(state, rows, rows.length - 1); case 'ArrowRight': { if (row.kind === 'camera') return state; if (!expandedKeys.has(row.key)) return changeExpansion(state, row, true); const child = rows[index + 1]; return child && child.parentKey === row.key ? Object.assign({}, state, { activeKey: child.key }) : state; } case 'ArrowLeft': if (row.kind !== 'camera' && expandedKeys.has(row.key)) return changeExpansion(state, row, false); return row.parentKey && validKey(row.parentKey) ? Object.assign({}, state, { activeKey: row.parentKey }) : state; case 'Enter': case ' ': case 'Spacebar': return activate(state, row); default: return state; } } function createSidebarController(options = {}) { if (!treeApi || typeof treeApi.flattenVisibleRows !== 'function' || typeof treeApi.calculateVirtualWindow !== 'function') { throw new Error('AptDeviceTree must be loaded before AptSidebarController.'); } if (!options.model || !options.view || typeof options.view.commit !== 'function') { throw new TypeError('Sidebar controller requires a model and view.commit adapter.'); } const model = options.model; const view = options.view; const rowHeight = Number.isFinite(options.rowHeight) ? options.rowHeight : 30; const overscan = Number.isInteger(options.overscan) ? options.overscan : 16; const searchRunner = options.searchRunner || treeApi.createSearchRunner({ scheduler: options.scheduler, chunkSize: options.chunkSize }); let state = createInitialState(options.initialState); let lastSnapshot; function effectiveExpandedKeys() { return state.searchQuery && state.searchExpandedKeys ? state.searchExpandedKeys : state.expandedKeys; } function commit(options = {}) { const rows = treeApi.flattenVisibleRows(model, { expandedKeys: effectiveExpandedKeys(), selectedKey: state.selectedKey, matchCameraKeys: state.searchQuery ? state.searchCameraKeys : null, }); const activeIndex = rows.findIndex((row) => row.key === state.activeKey); const maximumScrollTop = Math.max(0, (rows.length * rowHeight) - state.viewportHeight); const clampedScrollTop = Math.max(0, Math.min(state.scrollTop, maximumScrollTop)); if (clampedScrollTop !== state.scrollTop) { state = Object.assign({}, state, { scrollTop: clampedScrollTop }); } if (options.ensureActive === true && activeIndex >= 0 && state.viewportHeight > 0) { const activeTop = activeIndex * rowHeight; const activeBottom = activeTop + rowHeight; let scrollTop = state.scrollTop; if (activeTop < scrollTop) scrollTop = activeTop; else if (activeBottom > scrollTop + state.viewportHeight) scrollTop = activeBottom - state.viewportHeight; if (scrollTop !== state.scrollTop) state = Object.assign({}, state, { scrollTop }); } const window = treeApi.calculateVirtualWindow(rows, { scrollTop: state.scrollTop, viewportHeight: state.viewportHeight, rowHeight, overscan, }); lastSnapshot = Object.freeze({ state, rows, window }); view.commit(lastSnapshot); return lastSnapshot; } function dispatch(action) { const visibleRows = lastSnapshot ? lastSnapshot.rows : []; let safeAction = action; if (action && action.type === 'SET_SELECTION') { if (!model.cameraByKey.has(action.rowKey)) return lastSnapshot; safeAction = Object.assign({}, action, { allowHidden: true }); } const next = reduceSidebar(state, safeAction, visibleRows); if (next === state) return lastSnapshot; state = next; const ensureActive = safeAction.type === 'KEY' || safeAction.type === 'SET_ACTIVE'; return commit({ ensureActive }); } async function search(query) { const normalizedQuery = typeof query === 'string' ? query : ''; if (!normalizedQuery.trim()) { searchRunner.cancel(); return dispatch({ type: 'SET_SEARCH_QUERY', query: '' }); } state = reduceSidebar(state, { type: 'SET_SEARCH_QUERY', query: normalizedQuery }, lastSnapshot ? lastSnapshot.rows : []); const result = await searchRunner.search(model, normalizedQuery, { expandedKeys: state.expandedKeys }); if (result.cancelled) return result; dispatch({ type: 'SET_SEARCH_RESULTS', query: normalizedQuery, generation: result.generation, cameraKeys: result.cameraKeys, expandedKeys: result.expandedKeys, }); return result; } commit(); return Object.freeze({ dispatch, search, refresh: commit, getState: () => state, getSnapshot: () => lastSnapshot, destroy() { searchRunner.cancel(); }, }); } return Object.freeze({ createInitialState, reduceSidebar, sidebarReducer: reduceSidebar, createSidebarController, }); }));