diff --git a/FilterEngine.js.html b/FilterEngine.js.html new file mode 100644 index 0000000..b97680a --- /dev/null +++ b/FilterEngine.js.html @@ -0,0 +1,141 @@ + diff --git a/Index.html b/Index.html index c9a2cbd..ebf4441 100644 --- a/Index.html +++ b/Index.html @@ -476,6 +476,7 @@

PDF 下載選項 + diff --git a/JavaScript.html b/JavaScript.html index b2067f5..f57e48d 100644 --- a/JavaScript.html +++ b/JavaScript.html @@ -398,37 +398,8 @@ } }, - // --- FILTER LOGIC --- - loadAndApplyPersistedFilters: function () { - const persistedTagsJSON = localStorage.getItem('activeTagFilters'); - if (persistedTagsJSON) { - try { - const persistedTags = JSON.parse(persistedTagsJSON); - const allCurrentTags = new Set(this.getAllTags()); - - const validPersistedTags = persistedTags.filter(tag => allCurrentTags.has(tag)); - - this.activeFilters = validPersistedTags.map(tag => ({ type: 'tag', value: tag })); - - if (this.tagFilterTagify) { - this.tagFilterTagify.loadOriginalValues(validPersistedTags); - } - - if (persistedTags.length !== validPersistedTags.length) { - localStorage.setItem('activeTagFilters', JSON.stringify(validPersistedTags)); - } - } catch (e) { - console.error("Failed to parse persisted tag filters:", e); - localStorage.removeItem('activeTagFilters'); - this.activeFilters = []; - } - } else { - this.activeFilters = []; - } - this.ui.updateClearFilterButtonVisibility(); - this.ui.updateAdvancedFilterButtonState(); - this.ui.updateClearAllFiltersButtonVisibility(); - }, + // loadAndApplyPersistedFilters, toggleAllFilterCheckboxes, applyFilters — moved to FilterEngine.js.html (Phase 1 PR4) + // clearAdvancedFilters, clearAllFilters — moved to FilterEngine.js.html (Phase 1 PR4) applyTagFilters: async function () { if (!this.tagFilterTagify) return; @@ -460,47 +431,6 @@ this.ui.updateClearAllFiltersButtonVisibility(); }, - toggleAllFilterCheckboxes: function (shouldBeChecked) { - AppElements.filterCourseList.querySelectorAll('input[type="checkbox"]').forEach(cb => { - cb.checked = shouldBeChecked; - }); - }, - - applyFilters: function () { - const filterType = AppElements.filterTypeSelector.querySelector('input[name="filterType"]:checked').value; - const selectedItems = Array.from(AppElements.filterCourseList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value); - - const tagFilters = this.activeFilters.filter(f => f.type === 'tag'); - const newAdvancedFilters = selectedItems.map(item => ({ type: filterType, value: item })); - this.activeFilters = [...tagFilters, ...newAdvancedFilters]; - - this.ui.renderScheduleTable(); - this.ui.updateAdvancedFilterButtonState(); - this.ui.updateClearAllFiltersButtonVisibility(); - AppElements.filterModal.style.display = 'none'; - }, - - clearAdvancedFilters: function () { - this.activeFilters = this.activeFilters.filter(f => f.type === 'tag'); - this.modals.populateFilterModal(); - this.ui.renderScheduleTable(); - this.ui.updateAdvancedFilterButtonState(); - this.ui.updateClearAllFiltersButtonVisibility(); - }, - - clearAllFilters: function () { - this.activeFilters = []; - localStorage.removeItem('activeTagFilters'); - if (this.tagFilterTagify) { - this.tagFilterTagify.removeAllTags(); - } - this.modals.populateFilterModal(); - this.ui.renderScheduleTable(); - this.ui.updateAdvancedFilterButtonState(); - this.ui.updateClearFilterButtonVisibility(); - this.ui.updateClearAllFiltersButtonVisibility(); - }, - // --- API COMMUNICATION --- loadVersions: async function () { const select = AppElements.versionHistorySelect; @@ -733,52 +663,7 @@ // formatTimestampForFilename — moved to UtilityFunctions.js.html (Phase 1 PR1) - _filterScheduleData: function (data, filterPredicate) { - const filteredData = {}; - for (const classroom in data) { - const dayData = data[classroom]; - const newDayData = {}; - let classroomHasCourses = false; - for (const day in dayData) { - const filteredCourses = dayData[day].filter(filterPredicate); - if (filteredCourses.length > 0) { - newDayData[day] = filteredCourses; - classroomHasCourses = true; - } - } - if (classroomHasCourses) { - filteredData[classroom] = newDayData; - } - } - return filteredData; - }, - - filterDataByTags: function (data) { - const tagFilters = new Set(this.activeFilters.filter(f => f.type === 'tag').map(f => f.value)); - if (tagFilters.size === 0) { - return data; - } - return this._filterScheduleData(data, course => - course.tags && course.tags.some(d => tagFilters.has(d)) - ); - }, - - filterDataByActiveFilters: function (data) { - const nameFilters = new Set(this.activeFilters.filter(f => f.type === 'name').map(f => f.value)); - const tagFilters = new Set(this.activeFilters.filter(f => f.type === 'tag').map(f => f.value)); - const teacherFilters = new Set(this.activeFilters.filter(f => f.type === 'teacher').map(f => f.value)); - - if (nameFilters.size === 0 && tagFilters.size === 0 && teacherFilters.size === 0) { - return data; - } - - return this._filterScheduleData(data, course => { - const nameMatch = nameFilters.size === 0 || nameFilters.has(course.name); - const tagMatch = tagFilters.size === 0 || (course.tags && course.tags.some(d => tagFilters.has(d))); - const teacherMatch = teacherFilters.size === 0 || teacherFilters.has(course.teacher); - return nameMatch && tagMatch && teacherMatch; - }); - }, + // _filterScheduleData, filterDataByTags, filterDataByActiveFilters — moved to FilterEngine.js.html (Phase 1 PR4) // _forEachCourse, countOccurrences, updateAllOccurrences — moved to DataCollection.js.html (Phase 1 PR3) diff --git a/tests/unit/appWiringContracts.test.js b/tests/unit/appWiringContracts.test.js index f2c09e5..b8017ad 100644 --- a/tests/unit/appWiringContracts.test.js +++ b/tests/unit/appWiringContracts.test.js @@ -62,14 +62,10 @@ const privateMethods = extractPrivateMethods(jsHtmlSource); const EXTRACTED_TO_LIB = new Set([ // stateHelpers.js 'handleEditClassroom', 'saveDataToServer', - // frontendUtils.js - 'filterDataByTags', 'filterDataByActiveFilters', // interactionHelpers.js (handleDrop → applyDrop) 'handleDrop', // appLifecycleHelpers.js (new — this wave) 'loadInitialSchedules', 'loadSchedule', 'canManageCurrentScheduleSettings', - 'loadAndApplyPersistedFilters', 'applyFilters', 'clearAdvancedFilters', - 'clearAllFilters', 'saveSchedulesToLocal', ]); @@ -85,7 +81,6 @@ const NOT_EXTRACTABLE = new Set([ 'handleScheduleListClick', // DOM event delegation + ServerApi + modals 'handleScheduleSelectChange', // DOM event + modal confirm + state (aggregateScheduleData already extracted) 'applyTagFilters', // Tagify instance + modal confirm + DOM - 'toggleAllFilterCheckboxes', // DOM querySelectorAll 'loadVersions', // DOM + ServerApi 'handleLoadVersion', // DOM + ServerApi + state 'saveDataToLocal', // localStorage + Tagify + DOM (core sync logic extracted as processServerLoadResult) @@ -99,7 +94,7 @@ const NOT_EXTRACTABLE = new Set([ * These are tested indirectly through their public callers. */ const PRIVATE_HELPERS = new Set([ - '_filterScheduleData', + // _filterScheduleData — moved to FilterEngine.js.html (Phase 1 PR4) ]); /** @@ -121,6 +116,12 @@ const IIFE_EXTRACTED = new Set([ 'checkTimeConflict', // DataCollection.js.html (PR3) — private helpers also IIFE-extracted '_forEachCourse', '_collectFromScheduleData', '_collectFromAllCourses', + // FilterEngine.js.html (PR4) + 'loadAndApplyPersistedFilters', 'toggleAllFilterCheckboxes', 'applyFilters', + 'clearAdvancedFilters', 'clearAllFilters', + 'filterDataByTags', 'filterDataByActiveFilters', + // FilterEngine.js.html (PR4) — private helpers also IIFE-extracted + '_filterScheduleData', ]); // ─── Tests ───────────────────────────────────────────────────────────────── @@ -133,8 +134,8 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { it('should have expected total App method count', () => { // All public methods (excluding underscore-prefixed private helpers) const publicMethods = appMethods.filter(m => !m.startsWith('_')); - // 48 original - 7 PR1 - 4 PR2 public - 11 PR3 public = 26 remaining in JavaScript.html - expect(publicMethods.length).toBe(26); + // 48 original - 7 PR1 - 4 PR2 public - 11 PR3 public - 7 PR4 public = 19 remaining in JavaScript.html + expect(publicMethods.length).toBe(19); }); it('every public App method should be classified (extracted OR not-extractable)', () => { diff --git a/tests/unit/syncMethodsWiring.test.js b/tests/unit/syncMethodsWiring.test.js index cd856ba..b13a7ab 100644 --- a/tests/unit/syncMethodsWiring.test.js +++ b/tests/unit/syncMethodsWiring.test.js @@ -25,6 +25,12 @@ const lockManagerSource = readFileSync( 'utf-8' ); +// FilterEngine methods moved to separate IIFE module (Phase 1 PR4) +const filterEngineSource = readFileSync( + resolve(import.meta.dirname, '../../FilterEngine.js.html'), + 'utf-8' +); + // ─── Helpers (shared pattern from Wave 2) ──────────────────────────────── /** @@ -107,19 +113,19 @@ const SYNC_METHOD_WIRING = [ 'input\\[type=.checkbox.\\]', ]], ['clearAdvancedFilters', [ - 'this\\.activeFilters', - 'this\\.modals\\.populateFilterModal', - 'this\\.ui\\.renderScheduleTable', - 'this\\.ui\\.updateAdvancedFilterButtonState', - 'this\\.ui\\.updateClearAllFiltersButtonVisibility', + 'App\\.activeFilters', + 'App\\.modals\\.populateFilterModal', + 'App\\.ui\\.renderScheduleTable', + 'App\\.ui\\.updateAdvancedFilterButtonState', + 'App\\.ui\\.updateClearAllFiltersButtonVisibility', ]], ['clearAllFilters', [ - 'this\\.activeFilters', + 'App\\.activeFilters', 'localStorage\\.removeItem', - 'this\\.tagFilterTagify', + 'App\\.tagFilterTagify', 'removeAllTags', - 'this\\.modals\\.populateFilterModal', - 'this\\.ui\\.renderScheduleTable', + 'App\\.modals\\.populateFilterModal', + 'App\\.ui\\.renderScheduleTable', ]], ['_getLocks', [ 'localStorage\\.getItem', @@ -169,9 +175,10 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { // ─── 1. Method existence ────────────────────────────────────────────── describe('all sync methods exist in JavaScript.html or IIFE modules', () => { - // Non-lock methods from JavaScript.html + // Non-lock, non-filter methods from JavaScript.html const jsHtmlMethods = SYNC_METHOD_WIRING.filter(([n]) => - !['_getLocks', '_saveLocks', 'releaseCurrentLock', 'refreshLockHeartbeat'].includes(n) + !['_getLocks', '_saveLocks', 'releaseCurrentLock', 'refreshLockHeartbeat', + 'toggleAllFilterCheckboxes', 'clearAdvancedFilters', 'clearAllFilters'].includes(n) ); it.each(jsHtmlMethods)( '%s is declared in JavaScript.html', @@ -193,6 +200,18 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { expect(body).not.toBeNull(); } ); + + // Filter methods from FilterEngine.js.html (Phase 1 PR4) + const filterMethods = SYNC_METHOD_WIRING.filter(([n]) => + ['toggleAllFilterCheckboxes', 'clearAdvancedFilters', 'clearAllFilters'].includes(n) + ); + it.each(filterMethods)( + '%s is declared in FilterEngine.js.html', + (methodName, _patterns) => { + const body = extractMethodBody(filterEngineSource, methodName); + expect(body).not.toBeNull(); + } + ); }); // ─── 2. Wiring correctness ──────────────────────────────────────────── @@ -237,11 +256,12 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { }); describe('filter methods — DOM state management', () => { - const filterMethods = ['toggleAllFilterCheckboxes', 'clearAdvancedFilters', 'clearAllFilters']; + const filterMethodNames = ['toggleAllFilterCheckboxes', 'clearAdvancedFilters', 'clearAllFilters']; - for (const methodName of filterMethods) { + for (const methodName of filterMethodNames) { const [, patterns] = SYNC_METHOD_WIRING.find(([n]) => n === methodName); - const body = extractMethodBody(jsHtmlSource, methodName); + // Filter methods now in FilterEngine.js.html + const body = extractMethodBody(filterEngineSource, methodName); describe(methodName, () => { it.each(patterns)(