필터

테이블 시트는 텍스트, 숫자날짜 등의 조건을 비롯하여 Excel과 유사한 필터 대화 상자를 지원할 수 있습니다. 데이터의 양이 많더라도 빠르게 필터링됩니다. 또한 allowSort, allowFilterByValue, allowFilterByList를 사용하여 특정 열에 대한 필터 및 정렬 영역 가시성 설정이 지원됩니다.

테이블 시트는 텍스트, 숫자, 날짜 등의 조건을 포함하는 Excel과 유사한 필터를 지원할 수 있습니다. 대용량의 데이터가 있을 때 필터 대화 상자를 여는 성능을 개선하기 위해 테이블 시트는 특정 필드 필터 인덱스 캐시를 만들 수 있는 옵션을 제공합니다. 경우에 따라 체크리스트 또는 필터 대화 상자의 일부분이 사용자에게 필요하지 않을 수 있으므로, 테이블 시트는 테이블 시트 필터 대화 상자의 각 부분의 가시성을 제어하기 위해 열에 여러 가지 옵션을 제공합니다. allowSort, allowFilterByValue, allowFilterByList가 모두 false인 경우에는 열 헤더의 필터 버튼이 보이지 않습니다. API tableSheet.filter(filterInfos?)로 활성 필터 정의를 가져오거나 설정하고, tableSheet.removeFilter()로 모든 활성 필터를 지웁니다. filterInfos를 전달하면 이전 필터 정의를 지우고 새 정의를 한 번에 적용합니다. 인수 없이 tableSheet.filter()를 호출하면 활성 필터 정의의 복제본을 반환하므로, 반환된 배열을 변경한 뒤 tableSheet.filter(filterInfos)에 다시 전달해야 테이블 시트가 업데이트됩니다. 필터 정의는 field를 식별하고 다음 형태 중 하나를 사용합니다. 값 필터는 values로 필드의 하나 이상의 값을 일치시킵니다. 직렬화 및 역직렬화 과정에서 유지해야 하는 날짜 값에는 type: "date"를 사용합니다. 날짜 값은 문자열, 숫자 또는 Date 인스턴스로 제공할 수 있습니다. null 또는 undefined 값에는 type: "blank"를 사용하며, 다른 값은 기본 "value" 유형을 사용합니다. 계층 필터는 표시할 정확한 계층 경로를 paths로 지정합니다. 조건 필터는 복잡한 규칙에 GC.Spread.Sheets.ConditionalFormatting.Condition을 사용합니다. 여러 조건을 결합하려면 관계 조건을 사용합니다. 생성된 열에서는 데이터 뷰에서 열 정보를 읽고 열 정보의 value를 필드 이름으로 사용합니다. 명령 명령 관리자를 통해 테이블 시트 필터 명령을 사용할 수 있으며 각 명령은 실행 취소와 다시 실행을 지원합니다. TableSheetFilterColumn은 한 열을 필터링합니다. 옵션은 sheetName, col 및 선택적 values, condition, paths입니다. 필터 옵션을 둘 이상 지정하면 values > condition > paths 순서로 적용됩니다. TableSheetRemoveFilterColumn은 한 열의 필터를 제거하며 sheetName과 col 옵션을 사용합니다. 이벤트 필터 대화 상자나 관련 테이블 시트 필터 명령으로 필터를 적용하거나 지울 때 다음 이벤트가 발생합니다. TableSheetFiltering: 열을 필터링하기 전에 발생하며 args.cancel = true로 취소할 수 있습니다. 페이로드는 sheetName, col, values, condition, paths, cancel입니다. TableSheetFiltered: 열 필터링 후 발생합니다. 페이로드는 sheetName, col, values, condition, paths입니다. TableSheetFilterClearing: 열 필터를 지우기 전에 발생하며 취소할 수 있습니다. 페이로드는 sheetName, col, cancel입니다. TableSheetFilterCleared: 열 필터를 지운 후 발생합니다. 페이로드는 sheetName, col입니다.
/*REPLACE_MARKER*/ /*DO NOT DELETE THESE COMMENTS*/ var tableName = "Employee"; var baseApiUrl = getBaseApiUrl(); var apiUrl = baseApiUrl + "/" + tableName; var sheet, spread; var budgetDepartmentField = '=CONCAT([@department]," (L",LEVEL(),"-",LEVELROWNUMBER(),")")'; var filterFieldCaptions = { id: 'ID', firstName: '이름', lastName: '성', birth: '생일', state: '주', dept: '부서 번호', title: '직책', salary: '급여', Course: '과목', Term: '학기', Credit: '학점', Score: '점수', Teacher: '교사', department: '부서', budget: '예산', location: '위치', phone: '전화', country: '국가' }; var filterFields = ['id', 'firstName', 'lastName', 'birth', 'state', 'dept', 'title', 'salary']; var conditionCompareTypes = { numberCondition: [ { value: 'equalsTo', text: '같음' }, { value: 'notEqualsTo', text: '같지 않음' }, { value: 'greaterThan', text: '보다 큼' }, { value: 'greaterThanOrEqualsTo', text: '보다 크거나 같음' }, { value: 'lessThan', text: '보다 작음' }, { value: 'lessThanOrEqualsTo', text: '보다 작거나 같음' } ], textCondition: [ { value: 'equalsTo', text: '같음' }, { value: 'notEqualsTo', text: '같지 않음' }, { value: 'beginsWith', text: '다음으로 시작' }, { value: 'doesNotBeginWith', text: '다음으로 시작하지 않음' }, { value: 'endsWith', text: '다음으로 끝남' }, { value: 'doesNotEndWith', text: '다음으로 끝나지 않음' }, { value: 'contains', text: '포함' }, { value: 'doesNotContain', text: '포함하지 않음' } ], dateCondition: [ { value: 'equalsTo', text: '같음' }, { value: 'notEqualsTo', text: '같지 않음' }, { value: 'before', text: '이전' }, { value: 'beforeEqualsTo', text: '이전 또는 같음' }, { value: 'after', text: '이후' }, { value: 'afterEqualsTo', text: '이후 또는 같음' } ], top10Condition: [ { value: 'top', text: '상위' }, { value: 'bottom', text: '하위' } ], averageCondition: [ { value: 'above', text: '초과' }, { value: 'below', text: '미만' } ] }; window.onload = function () { spread = new GC.Spread.Sheets.Workbook(document.getElementById("ss"), { sheetCount: 0 }); //register self-defined row action command initSpread(); bindEvents(); }; function initSpread() { spread = GC.Spread.Sheets.findControl(document.getElementById("ss")); spread.suspendPaint(); //1. init a sheet spread.clearSheets(); spread.clearSheetTabs(); sheet = spread.addSheetTab(0, "TableSheet1", GC.Spread.Sheets.SheetType.tableSheet); var data = generateData(+getElementById("dataRows").value); var timeBeforeCreate = new Date(); var dataManager = spread.dataManager(); var employeeTable = dataManager.addTable("employeeTable", { data: data.employees, schema: { columns: { id: { indexed: getProperty("createIdIndexes", 'checked') }, birth: { dataType: "date", indexed: getProperty("createBirthIndexes", 'checked') } } } }); var departmentTable = dataManager.addTable("departmentTable", { data: data.departments }); dataManager.addRelationship(employeeTable, "dept", "department", departmentTable, "dept_no", "employees"); dataManager.addRelationship(departmentTable, "leader_id", "manager", employeeTable, "id", "a"); spread.resumePaint(); var numericStyle = new GC.Spread.Sheets.Style(); numericStyle.formatter = "$ #,##0.00"; var formatStringStyle = new GC.Spread.Sheets.Style(); formatStringStyle.formatter = 'yyyy-mm-dd'; var visibleInfo_id = {}; if (!getProperty("sortByValue_id", 'checked')) { visibleInfo_id.sortByValue = false; } if (!getProperty("filterByValue_id", 'checked')) { visibleInfo_id.filterByValue = false; } if (!getProperty("listFilterArea_id", 'checked')) { visibleInfo_id.listFilterArea = false; } var visibleInfo_birthday = {}; if (!getProperty("sortByValue_birthday", 'checked')) { visibleInfo_birthday.sortByValue = false; } if (!getProperty("filterByValue_birthday", 'checked')) { visibleInfo_birthday.filterByValue = false; } if (!getProperty("listFilterArea_birthday", 'checked')) { visibleInfo_birthday.listFilterArea = false; } var cols = [ { value: 'id', caption: 'ID', allowSort: visibleInfo_id.sortByValue, allowFilterByValue: visibleInfo_id.filterByValue, allowFilterByList: visibleInfo_id.listFilterArea}, { value: 'firstName', caption: '이름', width: 100}, { value: 'lastName', caption: '성', width: 100}, { value: 'birth', caption: '생일', width: 100, style: formatStringStyle, allowSort: visibleInfo_birthday.sortByValue, allowFilterByValue: visibleInfo_birthday.filterByValue, allowFilterByList: visibleInfo_birthday.listFilterArea}, { value: 'state', caption: '주', width: 100}, { value: 'dept', caption: '부서 번호', width: 130}, { value: 'title', caption: '직책', width: 120}, { value: 'salary', caption: '급여', style: numericStyle, width: 100}, ]; var employeeView = employeeTable.addView("employeeView", cols, undefined); employeeView.fetch().then(function (args) { sheet.suspendPaint(); sheet.setDataView(employeeView); sheet.resumePaint(); var timeGap = new Date() - timeBeforeCreate; getElementById('showEventArgs').value = ("데이터 가져오기 및 표시 - " + (timeGap) + " ms"); var filterApiStart = new Date(); applyInitialEmployeeFilters(sheet); getElementById('showEventArgs').value = getElementById('showEventArgs').value + "\r\n필터 - " + (new Date() - filterApiStart) + " ms"; }); initMultiFilterSample(spread); } function initMultiFilterSample (spread) { spread.suspendPaint(); var dataManager = spread.dataManager(); var table = dataManager.addTable("CourseTable", { data: [ { Course: "Calculus", Term: 1, Credit: 5, Score: 80, Teacher: "Nancy Feehafer" }, { Course: "P.E.", Term: 1, Credit: 3.5, Score: 85, Teacher: "Andrew Cencini" }, { Course: "Political Economics", Term: 1, Credit: 3.5, Score: 95, Teacher: "Jan Kotas" }, { Course: "Basic of Computer", Term: 1, Credit: 2, Score: 85, Teacher: "Steven Thorpe" }, { Course: "Micro-Economics", Term: 1, Credit: 4, Score: 62, Teacher: "Jan Kotas" }, { Course: "Linear Algebra", Term: 2, Credit: 5, Score: 73, Teacher: "Nancy Feehafer" }, { Course: "Accounting", Term: 2, Credit: 3.5, Score: 86, Teacher: "Nancy Feehafer" }, { Course: "Statistics", Term: 2, Credit: 5, Score: 85, Teacher: "Robert Zare" }, { Course: "Marketing", Term: 2, Credit: 4, Score: 70, Teacher: "Laura Giussani" } ], schema: { type: 'json' } }); var sheet = spread.addSheetTab(1, "Course", GC.Spread.Sheets.SheetType.tableSheet); table.fetch().then(function () { var myView = table.addView("CourseTable", [ { value: "Course", caption: "과목", width: 130 }, { value: "Term", caption: "학기", width: 100 }, { value: "Credit", caption: "학점", width: 100 }, { value: "Score", caption: "점수", width: 100 }, { value: "Teacher", caption: "교사", width: 120 }, ]); spread.suspendPaint(); sheet.setDataView(myView); spread.resumePaint(); }); initMultiFilterHierarchySample(spread, dataManager); spread.resumePaint(); } function initMultiFilterHierarchySample(spread, dataManager) { var table = dataManager.addTable("Table", { remote: { read: { url: getBaseApiUrl() + "/Hierarchy_Formula" } }, schema: { hierarchy: { type: 'Parent', column: 'parent', summaryFields: { 'budget':'=SUM(CHILDREN(1,"budget"))' } }, columns: { id: { isPrimaryKey: true, }, }, } }); var sheet = spread.addSheetTab(2, "Budget", GC.Spread.Sheets.SheetType.tableSheet); sheet.options.allowAddNew = false; table.fetch().then(function () { var myView = table.addView("myView", [ { value: budgetDepartmentField, caption: '부서', width: 265, outlineColumn: true }, { value: "budget", width: 100, caption: '예산' }, { value: '=IF(LEVEL()=0,"",[@budget]/PARENT(1,"budget"))', width: 120, caption: '백분율', style: { formatter: '0.00%' } }, { value: "location", width: 100, caption: '위치' }, { value: "phone", width: 150, caption: '전화' }, { value: "country", width: 100, caption: '국가' }, ]); spread.suspendPaint(); sheet.setDataView(myView); applyInitialBudgetFilters(sheet); spread.resumePaint(); }); } function createNumberCondition(compareType, expected) { var conditionalFormatting = GC.Spread.Sheets.ConditionalFormatting; return new conditionalFormatting.Condition(conditionalFormatting.ConditionType.numberCondition, { compareType: compareType, expected: expected }); } function createSalaryRangeCondition() { var conditionalFormatting = GC.Spread.Sheets.ConditionalFormatting; return new conditionalFormatting.Condition(conditionalFormatting.ConditionType.relationCondition, { compareType: conditionalFormatting.LogicalOperators.and, item1: createNumberCondition(conditionalFormatting.GeneralComparisonOperators.greaterThan, 10000), item2: createNumberCondition(conditionalFormatting.GeneralComparisonOperators.lessThan, 19500) }); } function applyInitialEmployeeFilters(tableSheet) { tableSheet.filter([ { field: "state", values: [{ value: "New York" }, { value: "Texas" }] }, { field: "salary", condition: createSalaryRangeCondition() } ]); } function applyInitialBudgetFilters(tableSheet) { tableSheet.filter([ { field: budgetDepartmentField, paths: [{ path: ["Corporate Headquarters (L0-1)", "Sales and Marketing (L1-1)", "Field Office: East Coast (L2-2)"] }] }, { field: "location", paths: [{ path: ["Monterey", "San Francisco", "Boston"] }] } ]); } function randomFromList(list) { return list[~~(Math.random() * list.length)]; } function generateData(itemCount) { var data = {employees:[], departments: departments}; var states = ["Texas", "New York", "Florida", "Washington", "Ohio"]; var department_id = ["D001", "D002", "D003", "D004", "D005", "D006", "D007", "D008", "D009"]; var title = ["Senior Engineer", "Staff", "Engineer", "Senior Staff", "Assistant Engineer", "Technique Leader", "Manager"]; for (var i = 0; i < itemCount; i++) { var date = new Date(parseInt(Math.random() * 12052666) * 24 * 3600); //The timestamp date.setHours(0,0,0,0); var item = { id: i + 1, firstName: randomFromList(firstNames), lastName: randomFromList(lastNames), birth: date, state: randomFromList(states), dept: i < 9 ? department_id[i] : randomFromList(department_id), title: i < 9 ? "Manager" : randomFromList(title), salary: 3000 + parseInt(Math.random() * 100) * 500, }; data.employees.push(item); } return data; } function bindEvents() { var showButton = document.getElementById('setDataSource'); showButton.addEventListener('click', function () { initSpread(); }); var sortStart; spread.bind(GC.Spread.Sheets.Events.TableSheetSorting, function(e,args) { sortStart = new Date(); }); spread.bind(GC.Spread.Sheets.Events.TableSheetSorted, function(e,args) { getElementById('showEventArgs').value = (getElementById('showEventArgs').value + "\r\n정렬 - " + (new Date()-sortStart) + " ms"); }); var sortClearStart; spread.bind(GC.Spread.Sheets.Events.TableSheetSortClearing, function(e,args) { sortClearStart = new Date(); }); spread.bind(GC.Spread.Sheets.Events.TableSheetSortCleared, function(e,args) { var tableSheet = args.sheet || spread.getActiveSheetTab(); var sortInfos = tableSheet && tableSheet.sort ? tableSheet.sort() : []; var label = sortInfos.length > 1 ? "지우기 및 정렬" : "정렬 지우기"; getElementById('showEventArgs').value = (getElementById('showEventArgs').value + "\r\n" + label + " - " + (new Date()-sortClearStart) + " ms"); }); var filterStart; spread.bind(GC.Spread.Sheets.Events.TableSheetFiltering, function(e,args) { filterStart = new Date(); }); spread.bind(GC.Spread.Sheets.Events.TableSheetFiltered, function(e,args) { getElementById('showEventArgs').value = (getElementById('showEventArgs').value + "\r\n필터 - " + (new Date()-filterStart) + " ms"); if (document.getElementById('filterapi-tab').classList.contains('active')) { handleReadFilters(); } }); var filterClearStart; spread.bind(GC.Spread.Sheets.Events.TableSheetFilterClearing, function(e,args) { filterClearStart = new Date(); }); spread.bind(GC.Spread.Sheets.Events.TableSheetFilterCleared, function(e,args) { var tableSheet = args.sheet || spread.getActiveSheetTab(); var filterInfos = tableSheet && tableSheet.filter ? tableSheet.filter() : []; var label = filterInfos.length > 1 ? "지우기 및 필터" : "필터 지우기"; getElementById('showEventArgs').value = (getElementById('showEventArgs').value + "\r\n" + label + " - " + (new Date()-filterClearStart) + " ms"); if (document.getElementById('filterapi-tab').classList.contains('active')) { handleReadFilters(); } }); // Filter API tab events getElementById('addFilterEntry').addEventListener('click', function () { addFilterEntryRow(getFirstUnusedFilterField(), 'values'); }); getElementById('applyFilters').addEventListener('click', handleApplyFilters); getElementById('clearFilters').addEventListener('click', handleClearFilters); // Re-read filters when switching spreadsheet tabs spread.bind(GC.Spread.Sheets.Events.ActiveSheetChanged, function () { if (document.getElementById('filterapi-tab').classList.contains('active')) { handleReadFilters(); } }); // Initialize first tab document.querySelector('.options-tab-link').click(); } function setTooltip(options, tooltip) { options.tooltip = tooltip; return options; } // --- Tab switching --- function openOptionsTab(event, tabId) { var i, tabcontent, tablinks; tabcontent = document.getElementsByClassName("tab-content"); for (i = 0; i < tabcontent.length; i++) { tabcontent[i].style.display = "none"; tabcontent[i].classList.remove("active"); } tablinks = document.getElementsByClassName("options-tab-link"); for (i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(" active", ""); } document.getElementById(tabId).style.display = "block"; document.getElementById(tabId).classList.add("active"); event.currentTarget.className += " active"; if (tabId === 'filterapi-tab') { handleReadFilters(); } } // --- Filter entry management --- function getUsedFilterFields(excludeEntry) { var rows = getElementById('filterEntries').getElementsByClassName('filter-entry-row'); var usedFields = {}; for (var i = 0; i < rows.length; i++) { if (excludeEntry && rows[i] === excludeEntry) { continue; } var fieldSelect = rows[i].querySelector('.filter-field'); if (fieldSelect && fieldSelect.value) { usedFields[fieldSelect.value] = true; } } return usedFields; } function getFirstUnusedFilterField() { var sheetFields = getFilterFieldsFromSheet(); var usedFields = getUsedFilterFields(); for (var i = 0; i < sheetFields.fields.length; i++) { if (!usedFields[sheetFields.fields[i]]) { return sheetFields.fields[i]; } } return null; } function getAvailableFilterFields(entry) { var fieldSelect = entry.querySelector('.filter-field'); var currentField = fieldSelect ? fieldSelect.value : null; var sheetFields = getFilterFieldsFromSheet(); var usedFields = getUsedFilterFields(entry); var availableFields = []; var hasCurrentField = false; for (var i = 0; i < sheetFields.fields.length; i++) { var field = sheetFields.fields[i]; if (field === currentField) { hasCurrentField = true; } if (field === currentField || !usedFields[field]) { availableFields.push(field); } } if (currentField && !hasCurrentField) { availableFields.unshift(currentField); } return { fields: availableFields, captions: sheetFields.captions }; } function updateAddFilterEntryButton() { getElementById('addFilterEntry').disabled = !getFirstUnusedFilterField(); } function updateFilterFieldOptions() { var rows = getElementById('filterEntries').getElementsByClassName('filter-entry-row'); for (var i = 0; i < rows.length; i++) { var entry = rows[i]; var fieldSelect = entry.querySelector('.filter-field'); var currentField = fieldSelect.value; var availableFields = getAvailableFilterFields(entry); fieldSelect.innerHTML = ''; for (var j = 0; j < availableFields.fields.length; j++) { var field = availableFields.fields[j]; var opt = document.createElement('option'); opt.value = field; opt.textContent = availableFields.captions[field] || field; if (field === currentField) { opt.selected = true; } fieldSelect.appendChild(opt); } } updateAddFilterEntryButton(); } function getUniqueFilterInfos(filterInfos) { var usedFields = {}; var uniqueFilterInfos = []; for (var i = 0; i < filterInfos.length; i++) { var field = filterInfos[i].field; if (field && !usedFields[field]) { usedFields[field] = true; uniqueFilterInfos.push(filterInfos[i]); } } return uniqueFilterInfos; } function addFilterEntryRow(field, filterType, configData) { field = field || getFirstUnusedFilterField(); if (!field) { updateAddFilterEntryButton(); return; } var container = getElementById('filterEntries'); var entry = document.createElement('div'); entry.className = 'filter-entry-row'; var header = document.createElement('div'); header.className = 'filter-entry-header'; var fieldSelect = document.createElement('select'); fieldSelect.className = 'filter-field'; var sheetFields = getFilterFieldsFromSheet(); var usedFields = getUsedFilterFields(); for (var i = 0; i < sheetFields.fields.length; i++) { if (sheetFields.fields[i] !== field && usedFields[sheetFields.fields[i]]) { continue; } var opt = document.createElement('option'); opt.value = sheetFields.fields[i]; opt.textContent = sheetFields.captions[sheetFields.fields[i]] || sheetFields.fields[i]; if (sheetFields.fields[i] === field) opt.selected = true; fieldSelect.appendChild(opt); } fieldSelect.addEventListener('change', updateFilterFieldOptions); var typeSelect = document.createElement('select'); typeSelect.className = 'filter-type'; var types = [ { value: 'values', text: '값' }, { value: 'condition', text: '조건' } ]; if (currentSheetHasHierarchy() || filterType === 'paths') { types.push({ value: 'paths', text: '경로' }); } for (var j = 0; j < types.length; j++) { var tOpt = document.createElement('option'); tOpt.value = types[j].value; tOpt.textContent = types[j].text; if (types[j].value === (filterType || 'values')) tOpt.selected = true; typeSelect.appendChild(tOpt); } var removeBtn = document.createElement('button'); removeBtn.className = 'filter-entry-remove'; removeBtn.type = 'button'; removeBtn.textContent = 'X'; removeBtn.title = '필터 정보 제거'; removeBtn.onclick = function () { entry.remove(); updateFilterFieldOptions(); }; var row1 = document.createElement('div'); row1.className = 'filter-header-row'; row1.appendChild(createLabel('필드:')); row1.appendChild(fieldSelect); row1.appendChild(removeBtn); var row2 = document.createElement('div'); row2.className = 'filter-header-row'; row2.appendChild(createLabel('필터 유형:')); row2.appendChild(typeSelect); var spacer = document.createElement('span'); spacer.className = 'filter-header-spacer'; row2.appendChild(spacer); header.appendChild(row1); header.appendChild(row2); var configPanel = document.createElement('div'); configPanel.className = 'filter-config-panel'; var valuesPanel = document.createElement('div'); valuesPanel.className = 'filter-values-panel'; buildValuesPanel(valuesPanel, configData && configData.values); var conditionPanel = document.createElement('div'); conditionPanel.className = 'filter-condition-panel'; buildConditionPanel(conditionPanel, configData && configData.condition); var pathsPanel = document.createElement('div'); pathsPanel.className = 'filter-paths-panel'; buildPathsPanel(pathsPanel, configData && configData.paths); configPanel.appendChild(valuesPanel); configPanel.appendChild(conditionPanel); configPanel.appendChild(pathsPanel); entry.appendChild(header); entry.appendChild(configPanel); container.appendChild(entry); showFilterConfigPanel(configPanel, typeSelect.value); typeSelect.addEventListener('change', function () { showFilterConfigPanel(configPanel, typeSelect.value); }); updateFilterFieldOptions(); } function showFilterConfigPanel(configPanel, type) { var panels = configPanel.children; for (var i = 0; i < panels.length; i++) { panels[i].classList.remove('active'); } if (type === 'values') { panels[0].classList.add('active'); } else if (type === 'condition') { panels[1].classList.add('active'); } else if (type === 'paths') { panels[2].classList.add('active'); } } function clearFilterEntries() { getElementById('filterEntries').innerHTML = ''; updateFilterFieldOptions(); } function detectFilterType(filterInfo) { if (filterInfo.values) return 'values'; if (filterInfo.condition) return 'condition'; if (filterInfo.paths) return 'paths'; return 'values'; } // --- Values panel --- function buildValuesPanel(panel, valuesData) { var sectionLabel = document.createElement('div'); sectionLabel.className = 'filter-section-label'; sectionLabel.textContent = '값으로 필터링:'; panel.appendChild(sectionLabel); var entries = document.createElement('div'); entries.className = 'filter-value-entries'; panel.appendChild(entries); var addBtn = document.createElement('button'); addBtn.type = 'button'; addBtn.className = 'filter-value-add'; addBtn.textContent = '+ 값 추가'; addBtn.onclick = function () { addFilterValueRow(entries, '', 'value'); }; panel.appendChild(addBtn); if (valuesData && valuesData.length > 0) { for (var i = 0; i < valuesData.length; i++) { var v = valuesData[i]; var valStr = (v.value instanceof Date) ? formatDate(v.value) : String(v.value != null ? v.value : ''); addFilterValueRow(entries, valStr, v.type || 'value'); } } else { addFilterValueRow(entries, '', 'value'); } } function addFilterValueRow(container, value, type) { var row = document.createElement('div'); row.className = 'filter-value-row'; var fieldDiv = document.createElement('div'); fieldDiv.className = 'filter-value-field'; var input = document.createElement('input'); input.type = 'text'; input.className = 'filter-value-input'; input.value = value; input.placeholder = '필터 값...'; var removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'filter-value-remove'; removeBtn.textContent = 'X'; removeBtn.title = '값 제거'; removeBtn.onclick = function () { row.remove(); }; fieldDiv.appendChild(createLabel('값:')); fieldDiv.appendChild(input); fieldDiv.appendChild(removeBtn); var metaDiv = document.createElement('div'); metaDiv.className = 'filter-value-meta'; var typeSelect = document.createElement('select'); typeSelect.className = 'filter-value-type'; var typeOptions = [ { value: 'value', text: '값' }, { value: 'date', text: '날짜' }, { value: 'blank', text: '공백' } ]; for (var i = 0; i < typeOptions.length; i++) { var opt = document.createElement('option'); opt.value = typeOptions[i].value; opt.textContent = typeOptions[i].text; if (typeOptions[i].value === type) opt.selected = true; typeSelect.appendChild(opt); } metaDiv.appendChild(createLabel('유형:')); metaDiv.appendChild(typeSelect); var valueSpacer = document.createElement('span'); valueSpacer.className = 'filter-meta-spacer'; metaDiv.appendChild(valueSpacer); row.appendChild(fieldDiv); row.appendChild(metaDiv); container.appendChild(row); } // --- Condition panel --- var _conditionPanelId = 0; function buildConditionPanel(panel, conditionData) { var sectionLabel = document.createElement('div'); sectionLabel.className = 'filter-section-label'; sectionLabel.textContent = '조건으로 필터링:'; panel.appendChild(sectionLabel); var isRelation = false; var group1Data = null, group2Data = null; var relationType = 'and'; if (conditionData) { var cf = GC.Spread.Sheets.ConditionalFormatting; var conType = conditionData.conType ? conditionData.conType() : undefined; if (conType === cf.ConditionType.relationCondition) { isRelation = true; group1Data = conditionData.item1(); group2Data = conditionData.item2(); relationType = (conditionData.compareType() === cf.LogicalOperators.or) ? 'or' : 'and'; } else { group1Data = conditionData; } } var group1 = createConditionGroup('1', group1Data); panel.appendChild(group1); var panelId = ++_conditionPanelId; var radioName = 'conditionRelation_' + panelId; var relationRow = document.createElement('div'); relationRow.className = 'condition-relation'; var andRadio = document.createElement('input'); andRadio.type = 'radio'; andRadio.name = radioName; andRadio.value = 'and'; andRadio.checked = (relationType === 'and'); var andLabel = document.createElement('label'); andLabel.textContent = 'AND'; var orRadio = document.createElement('input'); orRadio.type = 'radio'; orRadio.value = 'or'; orRadio.name = radioName; orRadio.checked = (relationType === 'or'); var orLabel = document.createElement('label'); orLabel.textContent = 'OR'; relationRow.appendChild(andRadio); relationRow.appendChild(andLabel); relationRow.appendChild(orRadio); relationRow.appendChild(orLabel); panel.appendChild(relationRow); var group2 = createConditionGroup('2', group2Data); group2.style.display = isRelation ? 'block' : 'none'; panel.appendChild(group2); var addGroupBtn = document.createElement('button'); addGroupBtn.type = 'button'; addGroupBtn.className = 'condition-add-group'; addGroupBtn.textContent = '+ 조건 추가'; addGroupBtn.style.display = isRelation ? 'none' : 'inline-block'; addGroupBtn.onclick = function () { group2.style.display = 'block'; relationRow.style.display = 'flex'; addGroupBtn.style.display = 'none'; }; panel.appendChild(addGroupBtn); if (!isRelation) { relationRow.style.display = 'none'; } } function createConditionGroup(groupNum, conditionData) { var group = document.createElement('div'); group.className = 'condition-group'; group.setAttribute('data-group', groupNum); var groupLabel = document.createElement('div'); groupLabel.className = 'condition-group-label'; groupLabel.textContent = '조건 ' + groupNum + ':'; group.appendChild(groupLabel); var initialType = 'numberCondition'; var initialCompare = 'equalsTo'; var initialValue = ''; var isPercent = false; if (conditionData) { var cf = GC.Spread.Sheets.ConditionalFormatting; var conType = conditionData.conType ? conditionData.conType() : undefined; if (conType === cf.ConditionType.numberCondition) { initialType = 'numberCondition'; initialCompare = enumKeyFromValue(cf.GeneralComparisonOperators, conditionData.compareType()); initialValue = conditionData.expected() != null ? String(conditionData.expected()) : ''; } else if (conType === cf.ConditionType.textCondition) { initialType = 'textCondition'; initialCompare = enumKeyFromValue(cf.TextCompareType, conditionData.compareType()); initialValue = conditionData.expected() != null ? String(conditionData.expected()) : ''; } else if (conType === cf.ConditionType.dateCondition) { initialType = 'dateCondition'; initialCompare = enumKeyFromValue(cf.DateCompareType, conditionData.compareType()); var exp = conditionData.expected(); initialValue = exp != null ? (exp instanceof Date ? formatDate(exp) : String(exp)) : ''; } else if (conType === cf.ConditionType.top10Condition) { initialType = 'top10Condition'; initialCompare = conditionData.type() === 0 ? 'top' : 'bottom'; initialValue = conditionData.expected() != null ? String(conditionData.expected()) : ''; isPercent = !!conditionData.isPercent(); } else if (conType === cf.ConditionType.averageCondition) { initialType = 'averageCondition'; initialCompare = conditionData.compareType() === 0 ? 'above' : 'below'; } } var typeSelect = document.createElement('select'); typeSelect.className = 'condition-type'; var condTypes = ['numberCondition', 'textCondition', 'dateCondition', 'top10Condition', 'averageCondition']; for (var i = 0; i < condTypes.length; i++) { var opt = document.createElement('option'); opt.value = condTypes[i]; opt.textContent = { numberCondition: '숫자', textCondition: '텍스트', dateCondition: '날짜', top10Condition: '상위/하위 10', averageCondition: '평균' }[condTypes[i]]; if (condTypes[i] === initialType) opt.selected = true; typeSelect.appendChild(opt); } var compareSelect = document.createElement('select'); compareSelect.className = 'condition-compare'; populateCompareTypes(compareSelect, initialType, initialCompare); var valueInput = document.createElement('input'); valueInput.type = 'text'; valueInput.className = 'condition-value'; valueInput.value = initialValue; // Type row var typeRow = document.createElement('div'); typeRow.className = 'condition-field-row'; typeRow.appendChild(createLabel('유형:')); typeRow.appendChild(typeSelect); group.appendChild(typeRow); // Compare row var compareRow = document.createElement('div'); compareRow.className = 'condition-field-row'; compareRow.appendChild(createLabel('비교:')); compareRow.appendChild(compareSelect); group.appendChild(compareRow); // Value row var valueRow = document.createElement('div'); valueRow.className = 'condition-field-row'; valueRow.appendChild(createLabel('값:')); valueRow.appendChild(valueInput); group.appendChild(valueRow); var top10Row = document.createElement('div'); top10Row.className = 'top10-options' + (initialType === 'top10Condition' ? ' active' : ''); var percentCheck = document.createElement('input'); percentCheck.type = 'checkbox'; percentCheck.className = 'condition-is-percent'; percentCheck.checked = isPercent; var percentLabel = document.createElement('label'); percentLabel.textContent = '백분율'; top10Row.appendChild(percentCheck); top10Row.appendChild(percentLabel); group.appendChild(top10Row); if (groupNum === '2') { var removeGroupBtn = document.createElement('button'); removeGroupBtn.type = 'button'; removeGroupBtn.className = 'condition-remove-group'; removeGroupBtn.textContent = '- 조건 제거'; removeGroupBtn.onclick = function () { group.style.display = 'none'; valueInput.value = ''; var addBtn = group.parentElement.querySelector('.condition-add-group'); if (addBtn) addBtn.style.display = 'inline-block'; }; group.appendChild(removeGroupBtn); } typeSelect.addEventListener('change', function () { populateCompareTypes(compareSelect, typeSelect.value, null); top10Row.classList.toggle('active', typeSelect.value === 'top10Condition'); }); return group; } function populateCompareTypes(selectEl, conditionType, selectedValue) { selectEl.innerHTML = ''; var items = conditionCompareTypes[conditionType] || []; for (var i = 0; i < items.length; i++) { var opt = document.createElement('option'); opt.value = items[i].value; opt.textContent = items[i].text; if (items[i].value === selectedValue) opt.selected = true; selectEl.appendChild(opt); } } function enumKeyFromValue(enumObj, value) { for (var key in enumObj) { if (enumObj[key] === value) return key; } var keys = Object.keys(enumObj); return keys.length > 0 ? keys[0] : 'equalsTo'; } // --- Paths panel --- function buildPathsPanel(panel, pathsData) { var sectionLabel = document.createElement('div'); sectionLabel.className = 'filter-section-label'; sectionLabel.textContent = '경로로 필터링(쉼표로 구분):'; panel.appendChild(sectionLabel); var entries = document.createElement('div'); entries.className = 'filter-path-entries'; panel.appendChild(entries); var addBtn = document.createElement('button'); addBtn.type = 'button'; addBtn.className = 'filter-path-add'; addBtn.textContent = '+ 경로 추가'; addBtn.onclick = function () { addFilterPathRow(entries, '', 'value'); }; panel.appendChild(addBtn); if (pathsData && pathsData.length > 0) { for (var i = 0; i < pathsData.length; i++) { var p = pathsData[i]; var pathStr = p.path ? p.path.join(', ') : ''; addFilterPathRow(entries, pathStr, p.type || 'value'); } } else { addFilterPathRow(entries, '', 'value'); } } function addFilterPathRow(container, pathValue, type) { var row = document.createElement('div'); row.className = 'filter-path-row'; var fieldDiv = document.createElement('div'); fieldDiv.className = 'filter-path-field'; var input = document.createElement('input'); input.type = 'text'; input.className = 'filter-path-input'; input.value = pathValue; input.placeholder = '세그먼트1, 세그먼트2, ...'; var removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'filter-path-remove'; removeBtn.textContent = 'X'; removeBtn.title = '경로 제거'; removeBtn.onclick = function () { row.remove(); }; fieldDiv.appendChild(createLabel('경로:')); fieldDiv.appendChild(input); fieldDiv.appendChild(removeBtn); var metaDiv = document.createElement('div'); metaDiv.className = 'filter-path-meta'; var typeSelect = document.createElement('select'); typeSelect.className = 'filter-path-type'; var typeOptions = [ { value: 'value', text: '값' }, { value: 'date', text: '날짜' }, { value: 'blank', text: '공백' } ]; for (var i = 0; i < typeOptions.length; i++) { var opt = document.createElement('option'); opt.value = typeOptions[i].value; opt.textContent = typeOptions[i].text; if (typeOptions[i].value === type) opt.selected = true; typeSelect.appendChild(opt); } metaDiv.appendChild(createLabel('유형:')); metaDiv.appendChild(typeSelect); var pathSpacer = document.createElement('span'); pathSpacer.className = 'filter-meta-spacer'; metaDiv.appendChild(pathSpacer); row.appendChild(fieldDiv); row.appendChild(metaDiv); container.appendChild(row); } // --- Build filter infos from UI --- function getFilterEntriesFromUI() { var rows = getElementById('filterEntries').getElementsByClassName('filter-entry-row'); var filterInfos = []; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var field = row.querySelector('.filter-field').value; var type = row.querySelector('.filter-type').value; var info = { field: field }; if (type === 'values') { var valueRows = row.querySelectorAll('.filter-value-row'); var values = []; for (var v = 0; v < valueRows.length; v++) { var valInput = valueRows[v].querySelector('.filter-value-input').value.trim(); var valType = valueRows[v].querySelector('.filter-value-type').value; if (valType === 'blank') { values.push({ value: null, type: 'blank' }); } else if (valInput !== '') { if (valType === 'date') { values.push({ value: new Date(valInput), type: 'date' }); } else if (!isNaN(valInput)) { values.push({ value: parseFloat(valInput), type: valType }); } else { values.push({ value: valInput, type: valType }); } } } if (values.length > 0) { info.values = values; filterInfos.push(info); } } else if (type === 'condition') { var cond = buildConditionFromPanel(row.querySelector('.filter-condition-panel')); if (cond) { info.condition = cond; filterInfos.push(info); } } else if (type === 'paths') { var pathRows = row.querySelectorAll('.filter-path-row'); var paths = []; for (var p = 0; p < pathRows.length; p++) { var pathInput = pathRows[p].querySelector('.filter-path-input').value.trim(); var pathType = pathRows[p].querySelector('.filter-path-type').value; if (pathInput !== '') { var pathSegments = pathInput.split(','); var trimmed = []; for (var s = 0; s < pathSegments.length; s++) { var seg = pathSegments[s].trim(); if (seg !== '') trimmed.push(seg); } if (trimmed.length > 0) { var pathObj = { path: trimmed }; if (pathType !== 'value') { pathObj.type = pathType; } paths.push(pathObj); } } } if (paths.length > 0) { info.paths = paths; filterInfos.push(info); } } } return filterInfos; } // --- Build Condition objects --- function buildConditionFromPanel(panel) { var groups = panel.querySelectorAll('.condition-group'); var group1 = groups[0]; var group2 = groups.length > 1 ? groups[1] : null; var cond1 = buildSingleCondition(group1); if (!cond1) return null; if (group2 && group2.style.display !== 'none') { var cond2 = buildSingleCondition(group2); if (cond2) { var cf = GC.Spread.Sheets.ConditionalFormatting; var relationRadios = panel.querySelectorAll('.condition-relation input[type=radio]'); var isOr = false; for (var r = 0; r < relationRadios.length; r++) { if (relationRadios[r].checked && relationRadios[r].value === 'or') { isOr = true; } } return new cf.Condition(cf.ConditionType.relationCondition, { compareType: isOr ? cf.LogicalOperators.or : cf.LogicalOperators.and, item1: cond1, item2: cond2 }); } } return cond1; } function buildSingleCondition(group) { var typeSelect = group.querySelector('.condition-type'); var compareSelect = group.querySelector('.condition-compare'); var valueInput = group.querySelector('.condition-value'); var isPercentCheck = group.querySelector('.condition-is-percent'); var condTypeStr = typeSelect.value; var compareStr = compareSelect.value; var valueStr = valueInput.value.trim(); var isPercent = isPercentCheck ? isPercentCheck.checked : false; var cf = GC.Spread.Sheets.ConditionalFormatting; var expected = valueStr; if (condTypeStr !== 'averageCondition' && valueStr !== '' && !isNaN(valueStr)) { expected = parseFloat(valueStr); } switch (condTypeStr) { case 'numberCondition': return new cf.Condition(cf.ConditionType.numberCondition, { compareType: cf.GeneralComparisonOperators[compareStr], expected: expected }); case 'textCondition': return new cf.Condition(cf.ConditionType.textCondition, { compareType: cf.TextCompareType[compareStr], expected: expected }); case 'dateCondition': var dateVal = expected instanceof Date ? expected : new Date(expected); return new cf.Condition(cf.ConditionType.dateCondition, { compareType: cf.DateCompareType[compareStr], expected: dateVal }); case 'top10Condition': return new cf.Condition(cf.ConditionType.top10Condition, { type: compareStr === 'top' ? 0 : 1, expected: expected, isPercent: isPercent }); case 'averageCondition': return new cf.Condition(cf.ConditionType.averageCondition, { compareType: compareStr === 'above' ? 0 : 1 }); default: return null; } } // --- API handlers --- function getActiveTableSheet() { var activeSheet = spread.getActiveSheetTab(); if (activeSheet && activeSheet.filter) { return activeSheet; } return sheet; } function currentSheetHasHierarchy() { var activeSheet = getActiveTableSheet(); if (!activeSheet) return false; try { var view = activeSheet.getDataView(); if (!view) return false; var table = typeof view.getTable === 'function' ? view.getTable() : null; if (table) { var schema = table.schema || (table.options && table.options.schema); if (schema && schema.hierarchy) return true; } } catch (e) {} return false; } function getFilterFieldsFromSheet() { var activeSheet = getActiveTableSheet(); if (!activeSheet) return { fields: filterFields, captions: filterFieldCaptions }; try { var view = activeSheet.getDataView(); if (!view) return { fields: filterFields, captions: filterFieldCaptions }; var cols = view.getColumn(); if (cols && cols.length) { var fields = []; var captions = {}; for (var i = 0; i < cols.length; i++) { if (cols[i] && cols[i].value) { fields.push(cols[i].value); captions[cols[i].value] = filterFieldCaptions[cols[i].value] || cols[i].caption || cols[i].value; } } if (fields.length > 0) return { fields: fields, captions: captions }; } } catch (e) {} return { fields: filterFields, captions: filterFieldCaptions }; } function handleApplyFilters() { var filterInfos = getFilterEntriesFromUI(); if (filterInfos.length === 0) return; var activeSheet = getActiveTableSheet(); if (activeSheet && activeSheet.filter) { activeSheet.filter(filterInfos); } } function handleReadFilters() { var activeSheet = getActiveTableSheet(); if (activeSheet && activeSheet.filter) { var filterInfos = activeSheet.filter(); if (filterInfos && filterInfos.length > 0) { populateFilterEntries(filterInfos); } else { clearFilterEntries(); } } } function handleClearFilters() { var activeSheet = getActiveTableSheet(); if (activeSheet && activeSheet.removeFilter) { activeSheet.removeFilter(); } clearFilterEntries(); } function populateFilterEntries(filterInfos) { clearFilterEntries(); var uniqueFilterInfos = getUniqueFilterInfos(filterInfos); for (var i = 0; i < uniqueFilterInfos.length; i++) { var info = uniqueFilterInfos[i]; var type = detectFilterType(info); var configData = {}; if (type === 'values') configData.values = info.values; else if (type === 'condition') configData.condition = info.condition; else if (type === 'paths') configData.paths = info.paths; addFilterEntryRow(info.field, type, configData); } updateFilterFieldOptions(); } // --- Utility --- function createLabel(text) { var label = document.createElement('label'); label.textContent = text; return label; } function formatDate(date) { if (!(date instanceof Date)) return String(date); var y = date.getFullYear(); var m = ('0' + (date.getMonth() + 1)).slice(-2); var d = ('0' + date.getDate()).slice(-2); return y + '-' + m + '-' + d; } function getProperty(domId, prop) { return getElementById(domId)[prop]; } function getElementById (domId) { return document.getElementById(domId); } function setProperty(domId, prop, value) { getElementById(domId)[prop] = value; } function getBaseApiUrl() { return window.location.href.match(/http.+spreadjs\/learn-spreadjs\//)[0] + 'server/api'; }
<!doctype html> <html style="height:100%;font-size:14px;"> <head> <meta charset="utf-8" /> <meta name="spreadjs culture" content="ko-kr" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <!-- Promise Polyfill for IE, https://www.npmjs.com/package/promise-polyfill --> <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-tablesheet/dist/gc.spread.sheets.tablesheet.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-resources-ko/dist/gc.spread.sheets.resources.ko.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/data/departments.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <div class="sample-tutorial"> <div id="ss" class="sample-spreadsheets"></div> <div id="options-container" class="options-container"> <div class="options-tabs"> <button class="options-tab-link active" onclick="openOptionsTab(event, 'options-tab')">옵션</button> <button class="options-tab-link" onclick="openOptionsTab(event, 'filterapi-tab')">필터 API</button> </div> <div class="options-tab-panels"> <div id="options-tab" class="tab-content active"> <fieldset> <legend>필터 인덱스 만들기</legend> <input type="checkbox" id="createIdIndexes" checked/> <label for="createIdIndexes">ID 인덱스 만들기</label> <br> <input type="checkbox" id="createBirthIndexes" checked/> <label for="createBirthIndexes">생일 인덱스 만들기</label> <br> </fieldset> <fieldset> <legend>ID 열 필터 대화 상자 옵션</legend> <input type="checkbox" id="sortByValue_id" checked/> <label for="sortByValue_id">정렬 허용</label> <br> <input type="checkbox" id="filterByValue_id" checked/> <label for="filterByValue_id">값으로 필터링 허용</label> <br> <input type="checkbox" id="listFilterArea_id" checked/> <label for="listFilterArea_id">목록으로 필터링 허용</label> <br> </fieldset> <fieldset> <legend>생일 열 필터 대화 상자 옵션</legend> <input type="checkbox" id="sortByValue_birthday" checked/> <label for="sortByValue_birthday">정렬 허용</label> <br> <input type="checkbox" id="filterByValue_birthday" checked/> <label for="filterByValue_birthday">값으로 필터링 허용</label> <br> <input type="checkbox" id="listFilterArea_birthday" checked/> <label for="listFilterArea_birthday">목록으로 필터링 허용</label> <br> </fieldset> <fieldset class="data-source-controls"> <legend>데이터 원본</legend> <label for="dataRows">행 수: </label> <select id="dataRows"> <option value="1000" selected="selected">1000</option> <option value="3000">3000</option> <option value="10000">10000</option> <option value="30000">30000</option> <option value="100000">100000</option> <option value="300000">300000</option> <option value="1000000">1000000</option> </select> <input type="button" id="setDataSource" value="데이터 원본 설정"/> </fieldset> <fieldset style="height: 130px;"> <legend>성능</legend> <textarea id="showEventArgs" style="width: 224px;height: 105px;" cols="64" rows="15"></textarea> </fieldset> </div> <div id="filterapi-tab" class="tab-content"> <fieldset> <legend>필터 정보</legend> <div class="filter-actions-row"> <input type="button" id="addFilterEntry" value="+ 필터 정보 추가"/> </div> <div id="filterEntries"></div> <div class="filter-actions-row"> <input type="button" id="applyFilters" value="필터 적용"/> <input type="button" id="clearFilters" value="모든 필터 지우기"/> </div> </fieldset> </div> </div> </div> </div> </html>
body { position: absolute; top: 0; bottom: 0; left: 0; right: 0; } fieldset { padding: 6px; margin: 0; margin-top: 10px; } .sample-tutorial { position: relative; height: 100%; overflow: hidden; } .sample-spreadsheets { width: calc(100% - 280px); height: 100%; overflow: hidden; float: left; } .options-container { float: right; width: 280px; padding: 12px; height: 100%; box-sizing: border-box; background: #fbfbfb; overflow: auto; } fieldset span, fieldset input, fieldset select { display: inline-block; text-align: left; } fieldset span { width: 50px; } fieldset input[type=text] { width: calc(100% - 58px); } fieldset input[type=button] { width: 100%; text-align: center; } .data-source-controls label, .data-source-controls select, .data-source-controls input[type=button] { display: block; width: 100%; box-sizing: border-box; } .data-source-controls select, .data-source-controls input[type=button] { margin-top: 4px; } fieldset select { width: calc(100% - 50px); } .field-line { margin-top: 4px; } /* Tab styles */ .options-tabs { display: flex; border-bottom: 1px solid #ccc; margin-bottom: 8px; } .options-tab-link { padding: 6px 12px; background-color: #f1f1f1; border: none; cursor: pointer; font-size: 13px; flex: 1; text-align: center; transition: background-color 0.3s; } .options-tab-link:hover { background-color: #ddd; } .options-tab-link.active { background-color: #ccc; font-weight: bold; } .tab-content { display: none; } .tab-content.active { display: block; } /* Filter entry rows */ #filterEntries { max-height: 400px; overflow-y: auto; margin-bottom: 4px; } .filter-actions-row { display: flex; justify-content: flex-end; gap: 6px; margin-bottom: 4px; } .filter-actions-row input[type=button] { width: auto; padding: 2px 10px; font-size: 13px; } /* Filter entry card */ .filter-entry-row { border: 1px solid #ddd; border-radius: 3px; padding: 8px; margin-bottom: 10px; background: #fff; } /* Header - two rows stacked vertically */ .filter-entry-header { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; } .filter-header-row { display: flex; align-items: center; gap: 4px; } .filter-header-row label { font-size: 12px; color: #444; white-space: nowrap; min-width: 75px; } .filter-header-spacer { display: inline-block; width: 22px; } .filter-meta-spacer { display: inline-block; width: 18px; } .filter-header-row select { flex: 1; font-size: 12px; padding: 2px; min-width: 0; } .filter-entry-remove { width: 22px; height: 22px; padding: 0; font-size: 12px; line-height: 22px; text-align: center; border: none; background: transparent; cursor: pointer; color: #999; } /* Config panels */ .filter-config-panel { margin-top: 6px; border-top: 1px solid #eee; padding-top: 6px; } .filter-values-panel, .filter-condition-panel, .filter-paths-panel { display: none; } .filter-values-panel.active, .filter-condition-panel.active, .filter-paths-panel.active { display: block; } .filter-section-label { font-size: 11px; color: #555; margin-bottom: 6px; font-weight: bold; } /* Value rows - two-line layout */ .filter-value-row { margin-bottom: 6px; padding: 4px; background: #fafafa; border-radius: 2px; border: 1px solid #f0f0f0; } .filter-value-field { display: flex; align-items: center; gap: 4px; margin-bottom: 3px; } .filter-value-field input[type=text] { flex: 1; font-size: 12px; padding: 2px 4px; min-width: 0; } .filter-value-meta { display: flex; align-items: center; gap: 4px; } .filter-value-meta select { flex: 1; font-size: 12px; padding: 1px 2px; min-width: 0; } .filter-value-remove { width: 18px; height: 18px; padding: 0; font-size: 11px; line-height: 18px; text-align: center; border: none; background: transparent; cursor: pointer; color: #999; } /* Condition groups - vertical field layout */ .condition-group { border: 1px solid #e0e0e0; padding: 6px; margin-bottom: 6px; border-radius: 3px; background: #fafafa; } .condition-group-label { font-size: 11px; color: #555; margin-bottom: 4px; font-weight: bold; } .condition-field-row { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; } .condition-field-row label { font-size: 11px; color: #555; min-width: 55px; white-space: nowrap; } .condition-field-row select, .condition-field-row input[type=text] { flex: 1; font-size: 12px; padding: 2px 4px; min-width: 0; box-sizing: border-box; } .top10-options { display: none; align-items: center; gap: 4px; margin-left: 59px; margin-bottom: 2px; } .top10-options.active { display: flex; } .top10-options label { font-size: 11px; color: #555; } .condition-relation { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; font-size: 12px; } /* Path rows - two-line layout */ .filter-path-row { margin-bottom: 6px; padding: 4px; background: #fafafa; border-radius: 2px; border: 1px solid #f0f0f0; } .filter-path-field { display: flex; align-items: center; gap: 4px; margin-bottom: 3px; } .filter-path-field input[type=text] { flex: 1; font-size: 12px; padding: 2px 4px; min-width: 0; } .filter-path-meta { display: flex; align-items: center; gap: 4px; } .filter-path-meta select { flex: 1; font-size: 12px; padding: 1px 2px; min-width: 0; } .filter-path-remove { width: 18px; height: 18px; padding: 0; font-size: 11px; line-height: 18px; text-align: center; border: none; background: transparent; cursor: pointer; color: #999; } /* Small buttons */ .filter-value-add, .filter-path-add, .condition-add-group, .condition-remove-group { font-size: 12px; padding: 2px 8px; border: 1px solid #ccc; background: #f9f9f9; cursor: pointer; margin-top: 2px; } /* Consistent heights for all filter selects and inputs */ .filter-header-row select, .condition-field-row select, .condition-field-row input[type=text], .filter-value-field input[type=text], .filter-value-meta select, .filter-path-field input[type=text], .filter-path-meta select { height: 24px; box-sizing: border-box; } /* Labels in value/path field rows */ .filter-value-field label, .filter-value-meta label, .filter-path-field label, .filter-path-meta label { font-size: 12px; color: #444; white-space: nowrap; min-width: 40px; }