TableSheet can support the multiple Excel-like filters including Text, Number and Date conditions.
In order to improve the performance of opening filter dialogs when there is a large amount of data, TableSheet provides an option to create a specific field filter indexes cache.
For some cases, users may not need the checklist or some parts of the filter dialog, so TableSheet provides several options in columns to control the visibility of each part in the TableSheet filter dialog.
If the allowSort, allowFilterByValue and allowFilterByList are all false, the filter button in column header is invisible.
APIs
Use tableSheet.filter(filterInfos?) to get or set the active filter definitions, and use tableSheet.removeFilter() to clear all active filters.
When filterInfos is passed, TableSheet clears the previous filter definitions and applies the supplied definitions in one operation. Calling tableSheet.filter() without arguments returns a cloned list of the active filter definitions, so changing the returned array does not update the TableSheet until it is passed back to tableSheet.filter(filterInfos).
A filter definition identifies a field and uses one of these shapes:
Value filters use values to match one or more values on the field:
Use type: "date" for date values that must round-trip through serialization and deserialization. Date values can be provided as strings, numbers, or Date instances. Use type: "blank" to filter null or undefined values. Other values use the default "value" type.
Hierarchy filters use paths to identify the hierarchy path that should be shown:
Condition filters use a GC.Spread.Sheets.ConditionalFormatting.Condition for more complex rules. Use a relation condition when multiple conditions need to be combined.
For hierarchy data, paths filters by the exact hierarchy path. Value filters and condition filters match values on the field without requiring a hierarchy path.
For generated columns, read the column information from the data view and use the column info value as the field name:
Commands
Use the TableSheet filter commands through the command manager. Each command supports undo and redo.
TableSheetFilterColumn filters one column.
Options: sheetName, col, and optional values, condition, or paths. If more than one filter option is supplied, TableSheet uses this priority: values > condition > paths.
TableSheetRemoveFilterColumn removes the filter from one column.
Options: sheetName and col.
Events
TableSheet raises filter events when filtering or clearing filters from the filter dialog or related TableSheet filter commands.
TableSheetFiltering occurs before a column is filtered. It is cancellable by setting args.cancel = true.
Payload fields: sheetName, col, values, condition, paths, and cancel.
TableSheetFiltered occurs after a column has been filtered.
Payload fields: sheetName, col, values, condition, and paths.
TableSheetFilterClearing occurs before filtering is cleared from a column. It is cancellable by setting args.cancel = true.
Payload fields: sheetName, col, and cancel.
TableSheetFilterCleared occurs after filtering has been cleared from a column.
Payload fields: sheetName and 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: 'First Name', lastName: 'Last Name', birth: 'Birthday',
state: 'State', dept: 'Department No', title: 'Title', salary: 'Salary'
};
var filterFields = ['id', 'firstName', 'lastName', 'birth', 'state', 'dept', 'title', 'salary'];
var conditionCompareTypes = {
numberCondition: [
{ value: 'equalsTo', text: 'EqualsTo' },
{ value: 'notEqualsTo', text: 'NotEqualsTo' },
{ value: 'greaterThan', text: 'GreaterThan' },
{ value: 'greaterThanOrEqualsTo', text: 'GreaterThanOrEqualsTo' },
{ value: 'lessThan', text: 'LessThan' },
{ value: 'lessThanOrEqualsTo', text: 'LessThanOrEqualsTo' }
],
textCondition: [
{ value: 'equalsTo', text: 'EqualsTo' },
{ value: 'notEqualsTo', text: 'NotEqualsTo' },
{ value: 'beginsWith', text: 'BeginsWith' },
{ value: 'doesNotBeginWith', text: 'DoesNotBeginWith' },
{ value: 'endsWith', text: 'EndsWith' },
{ value: 'doesNotEndWith', text: 'DoesNotEndWith' },
{ value: 'contains', text: 'Contains' },
{ value: 'doesNotContain', text: 'DoesNotContain' }
],
dateCondition: [
{ value: 'equalsTo', text: 'EqualsTo' },
{ value: 'notEqualsTo', text: 'NotEqualsTo' },
{ value: 'before', text: 'Before' },
{ value: 'beforeEqualsTo', text: 'BeforeEqualsTo' },
{ value: 'after', text: 'After' },
{ value: 'afterEqualsTo', text: 'AfterEqualsTo' }
],
top10Condition: [
{ value: 'top', text: 'Top' },
{ value: 'bottom', text: 'Bottom' }
],
averageCondition: [
{ value: 'above', text: 'Above' },
{ value: 'below', text: 'Below' }
]
};
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: 'First Name', width: 100},
{ value: 'lastName', caption: 'Last Name', width: 100},
{ value: 'birth', caption: 'Birthday', width: 100, style: formatStringStyle, allowSort: visibleInfo_birthday.sortByValue, allowFilterByValue: visibleInfo_birthday.filterByValue, allowFilterByList: visibleInfo_birthday.listFilterArea},
{ value: 'state', caption: 'State', width: 100},
{ value: 'dept', caption: 'Department No', width: 130},
{ value: 'title', caption: 'Title', width: 120},
{ value: 'salary', caption: 'Salary', 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 = ("Fetch data and paint - " + (timeGap) + " ms");
var filterApiStart = new Date();
applyInitialEmployeeFilters(sheet);
getElementById('showEventArgs').value = getElementById('showEventArgs').value + "\r\nFilter - " + (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", width: 130 },
{ value: "Term", width: 100 },
{ value: "Credit", width: 100 },
{ value: "Score", width: 100 },
{ value: "Teacher", 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: 'Department', width: 265, outlineColumn: true },
{ value: "budget", width: 100, caption: 'Budget' },
{ value: '=IF(LEVEL()=0,"",[@budget]/PARENT(1,"budget"))', width: 120, caption: 'Percentage', style: { formatter: '0.00%' } },
{ value: "location", width: 100, caption: 'Location' },
{ value: "phone", width: 150, caption: 'Phone' },
{ value: "country", width: 100, caption: 'Country' },
]);
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\nSort - " + (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 ? "Clear & Sort" : "Clear Sort";
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\nFilter - " + (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 ? "Clear & Filter" : "Clear Filter";
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: 'Values' },
{ value: 'condition', text: 'Condition' }
];
if (currentSheetHasHierarchy() || filterType === 'paths') {
types.push({ value: 'paths', text: 'Paths' });
}
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 = 'Remove Filter Info';
removeBtn.onclick = function () {
entry.remove();
updateFilterFieldOptions();
};
var row1 = document.createElement('div');
row1.className = 'filter-header-row';
row1.appendChild(createLabel('Field:'));
row1.appendChild(fieldSelect);
row1.appendChild(removeBtn);
var row2 = document.createElement('div');
row2.className = 'filter-header-row';
row2.appendChild(createLabel('Filter Type:'));
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 = 'Filter by values:';
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 = '+ Add Value';
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 = 'Filter value...';
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'filter-value-remove';
removeBtn.textContent = 'X';
removeBtn.title = 'Remove Value';
removeBtn.onclick = function () { row.remove(); };
fieldDiv.appendChild(createLabel('Value:'));
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' },
{ value: 'date', text: 'Date' },
{ value: 'blank', text: 'Blank' }
];
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('Type:'));
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 = 'Filter by condition:';
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 = '+ Add Condition';
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 = 'Condition ' + 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 = 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('Type:'));
typeRow.appendChild(typeSelect);
group.appendChild(typeRow);
// Compare row
var compareRow = document.createElement('div');
compareRow.className = 'condition-field-row';
compareRow.appendChild(createLabel('Compare:'));
compareRow.appendChild(compareSelect);
group.appendChild(compareRow);
// Value row
var valueRow = document.createElement('div');
valueRow.className = 'condition-field-row';
valueRow.appendChild(createLabel('Value:'));
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 = 'Is Percent';
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 = '- Remove Condition';
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 = 'Filter by paths (comma-separated):';
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 = '+ Add Path';
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 = 'segment1, segment2, ...';
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'filter-path-remove';
removeBtn.textContent = 'X';
removeBtn.title = 'Remove Path';
removeBtn.onclick = function () { row.remove(); };
fieldDiv.appendChild(createLabel('Path:'));
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' },
{ value: 'date', text: 'Date' },
{ value: 'blank', text: 'Blank' }
];
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('Type:'));
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] = 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\/demos\//)[0] + 'server/api';
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/en/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$/en/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-tablesheet/dist/gc.spread.sheets.tablesheet.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')">Options</button>
<button class="options-tab-link" onclick="openOptionsTab(event, 'filterapi-tab')">Filter API</button>
</div>
<div class="options-tab-panels">
<div id="options-tab" class="tab-content active">
<fieldset>
<legend>Create Filter Indexes</legend>
<input type="checkbox" id="createIdIndexes" checked/>
<label for="createIdIndexes">Create ID Indexes</label>
<br>
<input type="checkbox" id="createBirthIndexes" checked/>
<label for="createBirthIndexes">Create Birthday Indexes</label>
<br>
</fieldset>
<fieldset>
<legend>ID Column Filter Dialog Options</legend>
<input type="checkbox" id="sortByValue_id" checked/>
<label for="sortByValue_id">Allow Sort</label>
<br>
<input type="checkbox" id="filterByValue_id" checked/>
<label for="filterByValue_id">Allow Filter By Value</label>
<br>
<input type="checkbox" id="listFilterArea_id" checked/>
<label for="listFilterArea_id">Allow Filter By List</label>
<br>
</fieldset>
<fieldset>
<legend>Birthday Column Filter Dialog Options</legend>
<input type="checkbox" id="sortByValue_birthday" checked/>
<label for="sortByValue_birthday">Allow Sort</label>
<br>
<input type="checkbox" id="filterByValue_birthday" checked/>
<label for="filterByValue_birthday">Allow Filter By Value</label>
<br>
<input type="checkbox" id="listFilterArea_birthday" checked/>
<label for="listFilterArea_birthday">Allow Filter By List</label>
<br>
</fieldset>
<fieldset class="data-source-controls">
<legend>Data Source</legend>
<label for="dataRows">Row Count: </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="Set DataSource"/>
</fieldset>
<fieldset style="height: 130px;">
<legend>Performance</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>Filter Infos</legend>
<div class="filter-actions-row">
<input type="button" id="addFilterEntry" value="+ Add Filter Info"/>
</div>
<div id="filterEntries"></div>
<div class="filter-actions-row">
<input type="button" id="applyFilters" value="Apply Filters"/>
<input type="button" id="clearFilters" value="Clear All Filters"/>
</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;
}