diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts index e6d326310..528d20b48 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts @@ -12,6 +12,7 @@ import { GridOption, LongTextEditorOption, SlickNamespace, + SliderRangeOption, SortComparers, // utilities @@ -172,10 +173,17 @@ export class Example14 { formatter: Formatters.dollar, }, { - id: 'percentComplete', name: '% Complete', field: 'percentComplete', minWidth: 100, + id: 'percentComplete', name: '% Complete', field: 'percentComplete', minWidth: 150, type: FieldType.number, sortable: true, filterable: true, columnGroup: 'Analysis', - filter: { model: Filters.compoundSlider, operator: '>=' }, + filter: { + model: Filters.sliderRange, + operator: '>=', + filterOptions: { + enableSliderTrackColoring: true, + hideSliderNumbers: false, + } as SliderRangeOption, + }, editor: { model: Editors.slider, minValue: 0, maxValue: 100, diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 599612327..b1be7bf53 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -78,6 +78,9 @@ export class Constants { TREE_LEVEL_PROP: '__treeLevel', PARENT_PROP: '__parentId', }; + static readonly SLIDER_DEFAULT_MIN_VALUE = 0; + static readonly SLIDER_DEFAULT_MAX_VALUE = 100; + static readonly SLIDER_DEFAULT_STEP = 1; static readonly VALIDATION_REQUIRED_FIELD = 'Field is required'; static readonly VALIDATION_EDITOR_VALID_NUMBER = 'Please enter a valid number'; static readonly VALIDATION_EDITOR_VALID_INTEGER = 'Please enter a valid integer number'; diff --git a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts new file mode 100644 index 000000000..2a31d4c55 --- /dev/null +++ b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts @@ -0,0 +1,343 @@ +import { Filters } from '../filters.index'; +import { Column, FilterArguments, GridOption, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { SliderRangeFilter } from '../sliderRangeFilter'; + +const containerId = 'demo-container'; +declare const Slick: SlickNamespace; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), + onHeaderMouseLeave: new Slick.Event(), +} as unknown as SlickGrid; + +describe('SliderRangeFilter', () => { + let consoleSpy: any; + let divContainer: HTMLDivElement; + let filter: SliderRangeFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + + beforeEach(() => { + consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { id: 'duration', field: 'duration', filterable: true, filter: { model: Filters.sliderRange } }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn(), + filterContainerElm: gridStub.getHeaderRowColumn(mockColumn.id) + }; + + filter = new SliderRangeFilter(); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should throw an error when trying to call init without any arguments', () => { + expect(() => filter.init(null as any)).toThrowError('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); + }); + + it('should initialize the filter', () => { + filter.init(filterArguments); + const filterCount = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + it('should be able to retrieve default slider options through the Getter', () => { + filter.init(filterArguments); + + expect(filter.sliderRangeOptions).toEqual({ + maxValue: 100, + minValue: 0, + step: 1, + }); + }); + + it('should be able to retrieve slider options defined through the Getter when passing different filterOptions', () => { + mockColumn.filter = { + minValue: 4, + maxValue: 69, + valueStep: 5, + }; + filter.init(filterArguments); + + expect(filter.sliderRangeOptions).toEqual({ + maxValue: 69, + minValue: 4, + step: 5, + }); + }); + + it('should call "setValues" and expect that value to be in the callback when triggered', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues(['2..80']); + const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); + filterElms[0].dispatchEvent(new CustomEvent('change')); + + expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [2, 80], shouldTriggerQuery: true }); + }); + + it('should call "setValues" and expect that value to be in the callback when triggered', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues([3, 84]); + const sliderInputs = divContainer.querySelectorAll('.slider-filter-input'); + const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); + filterElms[0].dispatchEvent(new CustomEvent('change')); + + expect(sliderInputs[0].style.zIndex).toBe('0'); + expect(sliderInputs[1].style.zIndex).toBe('1'); + expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [3, 84], shouldTriggerQuery: true }); + }); + + it('should change z-index on left handle when it is by 20px near right handle so it shows over the right handle not below', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues([50, 63]); + const sliderInputs = divContainer.querySelectorAll('.slider-filter-input'); + const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); + filterElms[0].dispatchEvent(new CustomEvent('change')); + + expect(sliderInputs[0].style.zIndex).toBe('1'); + expect(sliderInputs[1].style.zIndex).toBe('0'); + expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [50, 63], shouldTriggerQuery: true }); + }); + + it('should change minValue to a lower value when it is to close to maxValue and "stopGapBetweenSliderHandles" is enabled so it will auto-change minValue to a lower value plus gap', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const minVal = 56; + const maxVal = 58; + + mockColumn.filter = { + filterOptions: { stopGapBetweenSliderHandles: 5 } + }; + filter.init(filterArguments); + filter.setValues([minVal, maxVal]); + const sliderInputs = divContainer.querySelectorAll('.slider-filter-input'); + const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); + filterElms[0].dispatchEvent(new CustomEvent('change')); + + expect(sliderInputs[0].value).toBe(`${minVal - 5}`); + expect(sliderInputs[1].value).toBe('58'); + expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [51, 58], shouldTriggerQuery: true }); + }); + + it('should change maxValue to a lower value when it is to close to minValue and "stopGapBetweenSliderHandles" is enabled so it will auto-change maxValue to a lower value plus gap', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const minVal = 56; + const maxVal = 58; + + mockColumn.filter = { + filterOptions: { stopGapBetweenSliderHandles: 5 } + }; + filter.init(filterArguments); + filter.setValues([minVal, maxVal]); + const sliderInputs = divContainer.querySelectorAll('.slider-filter-input'); + const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); + filterElms[1].dispatchEvent(new CustomEvent('change')); + + expect(sliderInputs[0].value).toBe('56'); + expect(sliderInputs[1].value).toBe(`${minVal + 5}`); + expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [56, 61], shouldTriggerQuery: true }); + }); + + it('should be able to call "setValues" and set empty values and the input to not have the "filled" css class', () => { + filter.init(filterArguments); + filter.setValues([3, 80]); + let filledInputElm = divContainer.querySelector('.search-filter.slider-container.filter-duration.filled') as HTMLInputElement; + + expect(filledInputElm).toBeTruthy(); + + filter.setValues(''); + filledInputElm = divContainer.querySelector('.search-filter.slider-container.filter-duration.filled') as HTMLInputElement; + expect(filledInputElm).toBeFalsy(); + }); + + it('should create the input filter with default search terms range when passed as a filter argument', () => { + filterArguments.searchTerms = [3, 80]; + + filter.init(filterArguments); + + const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; + const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; + + expect(filterLowestElm.textContent).toBe('3'); + expect(filterHighestElm.textContent).toBe('80'); + expect(filter.currentValues).toEqual([3, 80]); + }); + + it('should create the input filter with min/max slider values being set by filter "minValue" and "maxValue"', () => { + mockColumn.filter = { + minValue: 4, + maxValue: 69, + }; + + filter.init(filterArguments); + + const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; + const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; + + expect(filterLowestElm.textContent).toBe('4'); + expect(filterHighestElm.textContent).toBe('69'); + expect(filter.currentValues).toEqual([4, 69]); + }); + + it('should create the input filter with min/max slider values being set by filter "sliderStartValue" and "sliderEndValue" through the filterOptions', () => { + mockColumn.filter = { + filterOptions: { + sliderStartValue: 4, + sliderEndValue: 69, + } + }; + + filter.init(filterArguments); + + const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; + const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; + + expect(filterLowestElm.textContent).toBe('4'); + expect(filterHighestElm.textContent).toBe('69'); + expect(filter.currentValues).toEqual([4, 69]); + }); + + it('should create the input filter with min/max slider values defined in params and expect deprecated console warning', () => { + mockColumn.filter = { + params: { + sliderStartValue: 4, + sliderEndValue: 69, + } + }; + + filter.init(filterArguments); + + const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; + const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; + + expect(consoleSpy).toHaveBeenCalledWith('[Slickgrid-Universal] All filter.params were moved, and deprecated, to "filterOptions" as SliderRangeOption for better typing support.'); + expect(filterLowestElm.textContent).toBe('4'); + expect(filterHighestElm.textContent).toBe('69'); + expect(filter.currentValues).toEqual([4, 69]); + }); + + it('should create the input filter with default search terms range but without showing side numbers when "hideSliderNumbers" is set in filterOptions', () => { + filterArguments.searchTerms = [3, 80]; + mockColumn.filter!.filterOptions = { hideSliderNumbers: true }; + + filter.init(filterArguments); + + const filterLowestElms = divContainer.querySelectorAll('.lowest-range-duration'); + const filterHighestElms = divContainer.querySelectorAll('.highest-range-duration'); + + expect(filterLowestElms.length).toBe(0); + expect(filterHighestElms.length).toBe(0); + expect(filter.currentValues).toEqual([3, 80]); + }); + + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { + filterArguments.searchTerms = [3, 80]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(); + + expect(filter.currentValues).toEqual([0, 100]); + expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + }); + + it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { + filterArguments.searchTerms = [3, 80]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(false); + + expect(filter.currentValues).toEqual([0, 100]); + expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + }); + + it('should trigger a callback with the clear filter set when calling the "clear" method and expect min/max slider values being with values of "sliderStartValue" and "sliderEndValue" when defined through the filterOptions', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter = { + filterOptions: { + sliderStartValue: 4, + sliderEndValue: 69, + } + }; + + filter.init(filterArguments); + filter.clear(false); + + expect(filter.currentValues).toEqual([4, 69]); + expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + }); + + it('should enableSliderTrackColoring and trigger a change event and expect slider track to have background color', () => { + mockColumn.filter = { filterOptions: { enableSliderTrackColoring: true } }; + filter.init(filterArguments); + filter.setValues(['2..80']); + const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); + filterElms[0].dispatchEvent(new CustomEvent('change')); + const sliderTrackElm = divContainer.querySelector('.slider-track') as HTMLDivElement; + + // expect(sliderTrackElm.style.background).toBe('linear-gradient(to right, #eee 2%, var(--slick-slider-filter-thumb-color, #86bff8) 2%, var(--slick-slider-filter-thumb-color, #86bff8) 80%, #eee 80%)'); + expect(filter.sliderRangeOptions?.sliderTrackBackground).toBe('linear-gradient(to right, #eee 2%, var(--slick-slider-filter-thumb-color, #86bff8) 2%, var(--slick-slider-filter-thumb-color, #86bff8) 80%, #eee 80%)'); + }); + + it('should click on the slider track and expect left handle to move to the new position when calculated percent is below 50%', () => { + filter.init(filterArguments); + const sliderInputs = divContainer.querySelectorAll('.slider-filter-input'); + const sliderTrackElm = divContainer.querySelector('.slider-track') as HTMLDivElement; + + const sliderOneChangeSpy = jest.spyOn(sliderInputs[0], 'dispatchEvent'); + const sliderTwoChangeSpy = jest.spyOn(sliderInputs[1], 'dispatchEvent'); + + const clickEvent = new Event('click'); + Object.defineProperty(clickEvent, 'offsetX', { writable: true, configurable: true, value: 22 }); + Object.defineProperty(sliderTrackElm, 'offsetWidth', { writable: true, configurable: true, value: 85 }); + sliderTrackElm.dispatchEvent(clickEvent); + + expect(sliderOneChangeSpy).toHaveBeenCalled(); + expect(sliderTwoChangeSpy).not.toHaveBeenCalled(); + }); + + it('should click on the slider track and expect right handle to move to the new position when calculated percent is above 50%', () => { + filter.init(filterArguments); + const sliderInputs = divContainer.querySelectorAll('.slider-filter-input'); + const sliderTrackElm = divContainer.querySelector('.slider-track') as HTMLDivElement; + + const sliderOneChangeSpy = jest.spyOn(sliderInputs[0], 'dispatchEvent'); + const sliderTwoChangeSpy = jest.spyOn(sliderInputs[1], 'dispatchEvent'); + + const clickEvent = new Event('click'); + Object.defineProperty(clickEvent, 'offsetX', { writable: true, configurable: true, value: 56 }); + Object.defineProperty(sliderTrackElm, 'offsetWidth', { writable: true, configurable: true, value: 75 }); + sliderTrackElm.dispatchEvent(clickEvent); + + expect(sliderOneChangeSpy).not.toHaveBeenCalled(); + expect(sliderTwoChangeSpy).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/packages/common/src/filters/filters.index.ts b/packages/common/src/filters/filters.index.ts index c98595f91..1be909ffc 100644 --- a/packages/common/src/filters/filters.index.ts +++ b/packages/common/src/filters/filters.index.ts @@ -13,6 +13,7 @@ import { NativeSelectFilter } from './nativeSelectFilter'; import { DateRangeFilter } from './dateRangeFilter'; import { SingleSelectFilter } from './singleSelectFilter'; import { SliderFilter } from './sliderFilter'; +import { SliderRangeFilter } from './sliderRangeFilter'; export const Filters = { /** AutoComplete Filter (using https://github.com/kraaden/autocomplete) */ @@ -66,6 +67,9 @@ export const Filters = { /** Single Select filter, which uses 3rd party lib "multiple-select.js" */ singleSelect: SingleSelectFilter, - /** Slider Filter (only 1 value) */ + /** Slider Filter (single value) */ slider: SliderFilter, + + /** Slider Range Filter (dual values, lowest/highest filter range) */ + sliderRange: SliderRangeFilter, }; diff --git a/packages/common/src/filters/sliderRangeFilter.ts b/packages/common/src/filters/sliderRangeFilter.ts new file mode 100644 index 000000000..08576b80c --- /dev/null +++ b/packages/common/src/filters/sliderRangeFilter.ts @@ -0,0 +1,421 @@ +import { toSentenceCase } from '@slickgrid-universal/utils'; + +import { Constants } from '../constants'; +import { OperatorString, OperatorType, SearchTerm, } from '../enums/index'; +import { + Column, + ColumnFilter, + Filter, + FilterArguments, + FilterCallback, + GridOption, + SlickGrid, + SliderRangeOption, +} from '../interfaces/index'; +import { BindingEventService } from '../services/bindingEvent.service'; +import { createDomElement, emptyElement } from '../services/domUtilities'; + +interface CurrentSliderOption { + minValue: number; + maxValue: number; + step: number; + sliderTrackBackground?: string; +} + +const GAP_BETWEEN_SLIDER_HANDLES = 0; +const Z_INDEX_MIN_GAP = 20; // gap in Px before we change z-index so that lowest/highest handle doesn't block each other + +/** A Slider Range Filter written in pure JS, this is only meant to be used as a range filter (with 2 handles lowest & highest values) */ +export class SliderRangeFilter implements Filter { + protected _bindEventService: BindingEventService; + protected _clearFilterTriggered = false; + protected _currentValues?: number[]; + protected _shouldTriggerQuery = true; + protected _sliderOptions!: CurrentSliderOption; + protected filterElm!: HTMLDivElement; + protected _argFilterContainerElm!: HTMLDivElement; + protected _divContainerFilterElm!: HTMLDivElement; + protected _filterContainerElm!: HTMLDivElement; + protected _lowestSliderNumberElm?: HTMLSpanElement; + protected _highestSliderNumberElm?: HTMLSpanElement; + protected _sliderRangeContainElm!: HTMLDivElement; + protected _sliderTrackElm!: HTMLDivElement; + protected _sliderOneElm!: HTMLInputElement; + protected _sliderTwoElm!: HTMLInputElement; + grid!: SlickGrid; + searchTerms: SearchTerm[] = []; + columnDef!: Column; + callback!: FilterCallback; + + constructor() { + this._bindEventService = new BindingEventService(); + } + + /** @deprecated Getter for the Filter Generic Params */ + protected get filterParams(): any { + return this.columnDef?.filter?.params ?? {}; + } + + /** Getter for the Filter Options */ + get filterOptions(): SliderRangeOption | undefined { + return this.columnFilter.filterOptions; + } + + + /** Getter for the `filter` properties */ + protected get filterProperties(): ColumnFilter { + return this.columnDef && this.columnDef.filter || {}; + } + + /** Getter for the Column Filter */ + get columnFilter(): ColumnFilter { + return this.columnDef && this.columnDef.filter || {}; + } + + /** Getter for the Current Slider Values */ + get currentValues(): number[] | undefined { + return this._currentValues; + } + + /** Getter to know what would be the default operator when none is specified */ + get defaultOperator(): OperatorType | OperatorString { + return this.gridOptions.defaultFilterRangeOperator || OperatorType.rangeInclusive; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + get gridOptions(): GridOption { + return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + } + + /** Getter for the current Slider Options */ + get sliderRangeOptions(): CurrentSliderOption | undefined { + return this._sliderOptions; + } + + /** Getter of the Operator to use when doing the filter comparing */ + get operator(): OperatorType | OperatorString { + return this.columnFilter?.operator ?? this.defaultOperator; + } + + /** Setter for the filter operator */ + set operator(operator: OperatorType | OperatorString) { + if (this.columnFilter) { + this.columnFilter.operator = operator; + } + } + + /** + * Initialize the Filter + */ + init(args: FilterArguments) { + if (!args) { + throw new Error('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); + } + this.grid = args.grid; + this.callback = args.callback; + this.columnDef = args.columnDef; + this.searchTerms = args?.searchTerms ?? []; + this._argFilterContainerElm = args.filterContainerElm; + + // step 1, create the DOM Element of the filter & initialize it if searchTerm is filled + this.filterElm = this.createDomFilterElement(this.searchTerms); + } + + /** + * Clear the filter value + */ + clear(shouldTriggerQuery = true) { + if (this.filterElm) { + this._clearFilterTriggered = true; + this._shouldTriggerQuery = shouldTriggerQuery; + this.searchTerms = []; + const lowestValue = (this.getFilterOptionByName('sliderStartValue') ?? Constants.SLIDER_DEFAULT_MIN_VALUE) as number; + const highestValue = (this.getFilterOptionByName('sliderEndValue') ?? Constants.SLIDER_DEFAULT_MAX_VALUE) as number; + this._currentValues = [lowestValue, highestValue]; + this._sliderOneElm.value = `${lowestValue}`; + this._sliderTwoElm.value = `${highestValue}`; + this.dispatchBothEvents(); + + if (!this.getFilterOptionByName('hideSliderNumbers')) { + this.renderSliderValues(lowestValue, highestValue); + } + this._divContainerFilterElm.classList.remove('filled'); + this.filterElm.classList.remove('filled'); + this.callback(undefined, { columnDef: this.columnDef, clearFilterTriggered: true, shouldTriggerQuery }); + } + } + + /** + * destroy the filter + */ + destroy() { + this._bindEventService.unbindAll(); + } + + /** + * Get option from filter.params PR filter.filterOptions + * @deprecated this should be removed when slider filterParams are replaced by filterOptions + */ + getFilterOptionByName(optionName: string, defaultValue?: string | number | boolean): T { + let outValue: string | number | boolean | undefined; + if (this.filterOptions?.[optionName as keyof SliderRangeOption] !== undefined) { + outValue = this.filterOptions[optionName as keyof SliderRangeOption]; + } else if (this.filterParams?.[optionName] !== undefined) { + console.warn('[Slickgrid-Universal] All filter.params were moved, and deprecated, to "filterOptions" as SliderRangeOption for better typing support.'); + outValue = this.filterParams?.[optionName]; + } + return outValue as T ?? defaultValue ?? undefined; + } + + /** + * Render both slider values (low/high) on screen + * @param lowestValue number + * @param highestValue number + */ + renderSliderValues(lowestValue: number | string, highestValue: number | string) { + if (this._lowestSliderNumberElm?.textContent) { + this._lowestSliderNumberElm.textContent = lowestValue.toString(); + } + if (this._highestSliderNumberElm?.textContent) { + this._highestSliderNumberElm.textContent = highestValue.toString(); + } + } + + getValues() { + return this._currentValues; + } + + /** + * Set value(s) on the DOM element + * @params searchTerms + */ + setValues(searchTerms: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) { + if (searchTerms) { + let sliderValues: number[] | string[] = []; + + // get the slider values, if it's a string with the "..", we'll do the split else we'll use the array of search terms + if (typeof searchTerms === 'string' || (Array.isArray(searchTerms) && typeof searchTerms[0] === 'string') && (searchTerms[0] as string).indexOf('..') > 0) { + sliderValues = (typeof searchTerms === 'string') ? [(searchTerms as string)] : (searchTerms[0] as string).split('..'); + } else if (Array.isArray(searchTerms)) { + sliderValues = searchTerms as string[]; + } + + if (Array.isArray(sliderValues) && sliderValues.length === 2) { + if (!this.getFilterOptionByName('hideSliderNumbers')) { + const [lowestValue, highestValue] = sliderValues; + this._sliderOneElm.value = String(lowestValue ?? Constants.SLIDER_DEFAULT_MIN_VALUE); + this._sliderTwoElm.value = String(highestValue ?? Constants.SLIDER_DEFAULT_MAX_VALUE); + this.renderSliderValues(sliderValues[0], sliderValues[1]); + } + } + } + + (searchTerms && (this.getValues?.() ?? []).length > 0) + ? this.filterElm.classList.add('filled') + : this.filterElm.classList.remove('filled'); + + // set the operator when defined + this.operator = operator || this.defaultOperator; + } + + /** + * Create the Filter DOM element + * Follows article with few modifications (without tooltip & neither slider track color) + * https://codingartistweb.com/2021/06/double-range-slider-html-css-javascript/ + * @param searchTerm optional preset search terms + */ + protected createDomFilterElement(searchTerms?: SearchTerm | SearchTerm[]) { + const columnId = this.columnDef?.id ?? ''; + const minValue = +(this.filterProperties?.minValue ?? Constants.SLIDER_DEFAULT_MIN_VALUE); + const maxValue = +(this.filterProperties?.maxValue ?? Constants.SLIDER_DEFAULT_MAX_VALUE); + const step = +(this.filterProperties?.valueStep ?? Constants.SLIDER_DEFAULT_STEP); + emptyElement(this._argFilterContainerElm); + + let defaultStartValue: number = Constants.SLIDER_DEFAULT_MIN_VALUE; + let defaultEndValue: number = Constants.SLIDER_DEFAULT_MAX_VALUE; + if (Array.isArray(searchTerms) && searchTerms.length > 1) { + defaultStartValue = +searchTerms[0]; + defaultEndValue = +searchTerms[1]; + } else { + defaultStartValue = +(this.getFilterOptionByName('sliderStartValue') ?? minValue); + defaultEndValue = +(this.getFilterOptionByName('sliderEndValue') ?? maxValue); + } + + this._sliderRangeContainElm = createDomElement('div', { className: `filter-input filter-${columnId} slider-range-container slider-values` }); + this._sliderRangeContainElm.title = `${defaultStartValue} - ${defaultEndValue}`; + + this._sliderTrackElm = createDomElement('div', { className: 'slider-track' }); + this._sliderOneElm = createDomElement('input', { + type: 'range', + className: `slider-filter-input`, + ariaLabel: this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`, + defaultValue: `${defaultStartValue}`, value: `${defaultStartValue}`, + min: `${minValue}`, max: `${maxValue}`, step: `${step}`, + }); + this._sliderTwoElm = createDomElement('input', { + type: 'range', + className: `slider-filter-input`, + ariaLabel: this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`, + defaultValue: `${defaultEndValue}`, value: `${defaultEndValue}`, + min: `${minValue}`, max: `${maxValue}`, step: `${step}`, + }); + + this._bindEventService.bind(this._sliderTrackElm, 'click', this.sliderTrackClicked.bind(this) as EventListener); + this._bindEventService.bind(this._sliderOneElm, ['input', 'change'], this.slideOneInputChanged.bind(this)); + this._bindEventService.bind(this._sliderTwoElm, ['input', 'change'], this.slideTwoInputChanged.bind(this)); + this._bindEventService.bind(this._sliderOneElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); + this._bindEventService.bind(this._sliderTwoElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); + + // create the DOM element + const sliderNumberClass = this.getFilterOptionByName('hideSliderNumbers') ? '' : 'input-group'; + this._divContainerFilterElm = createDomElement('div', { className: `${sliderNumberClass} search-filter slider-container slider-values filter-${columnId}`.trim() }); + + this._sliderRangeContainElm.append(this._sliderTrackElm); + this._sliderRangeContainElm.append(this._sliderOneElm); + this._sliderRangeContainElm.append(this._sliderTwoElm); + + if (this.getFilterOptionByName('hideSliderNumbers')) { + this._divContainerFilterElm.append(this._sliderRangeContainElm); + } else { + const lowestSliderContainerDivElm = createDomElement('div', { className: `input-group-addon input-group-prepend slider-range-value` }); + this._lowestSliderNumberElm = createDomElement('span', { className: `input-group-text lowest-range-${columnId}`, textContent: `${defaultStartValue}` }); + lowestSliderContainerDivElm.append(this._lowestSliderNumberElm); + + const highestSliderContainerDivElm = createDomElement('div', { className: `input-group-addon input-group-append slider-range-value` }); + this._highestSliderNumberElm = createDomElement('span', { className: `input-group-text highest-range-${columnId}`, textContent: `${defaultEndValue}` }); + highestSliderContainerDivElm.append(this._highestSliderNumberElm); + + this._divContainerFilterElm.append(lowestSliderContainerDivElm); + this._divContainerFilterElm.append(this._sliderRangeContainElm); + this._divContainerFilterElm.append(highestSliderContainerDivElm); + } + + // if we are preloading searchTerms, we'll keep them for reference + this._currentValues = [defaultStartValue, defaultEndValue]; + + // merge options with optional user's custom options + this._sliderOptions = { minValue, maxValue, step }; + + // if there's a search term, we will add the "filled" class for styling purposes + if (Array.isArray(searchTerms) && searchTerms.length > 0 && searchTerms[0] !== '') { + this._divContainerFilterElm.classList.add('filled'); + } + + // append the new DOM element to the header row + this._argFilterContainerElm.append(this._divContainerFilterElm); + this.updateTrackFilledColor(); + + return this._divContainerFilterElm; + } + + protected dispatchBothEvents() { + this._sliderOneElm.dispatchEvent(new Event('change')); + this._sliderTwoElm.dispatchEvent(new Event('change')); + } + + /** handle value change event triggered, trigger filter callback & update "filled" class name */ + protected onValueChanged(e: Event) { + const sliderOneVal = parseInt(this._sliderOneElm.value, 10); + const sliderTwoVal = parseInt(this._sliderTwoElm.value, 10); + const values = [sliderOneVal, sliderTwoVal]; + const value = values.join('..'); + + if (this._clearFilterTriggered) { + this.filterElm.classList.remove('filled'); + this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); + } else { + value === '' ? this.filterElm.classList.remove('filled') : this.filterElm.classList.add('filled'); + this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: values, shouldTriggerQuery: this._shouldTriggerQuery }); + } + // reset both flags for next use + this._clearFilterTriggered = false; + this._shouldTriggerQuery = true; + this.changeBothSliderFocuses(false); + + // trigger leave event to avoid having previous value still being displayed with custom tooltip feat + this.grid?.onHeaderMouseLeave.notify({ column: this.columnDef, grid: this.grid }); + } + + protected changeBothSliderFocuses(isAddingFocus: boolean) { + const addRemoveCmd = isAddingFocus ? 'add' : 'remove'; + this._sliderOneElm.classList[addRemoveCmd]('focus'); + this._sliderTwoElm.classList[addRemoveCmd]('focus'); + } + + protected slideOneInputChanged() { + const sliderOneVal = parseInt(this._sliderOneElm.value, 10); + const sliderTwoVal = parseInt(this._sliderTwoElm.value, 10); + + if (sliderTwoVal - sliderOneVal <= this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)) { + this._sliderOneElm.value = String(sliderOneVal - this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)); + } + + this._sliderRangeContainElm.title = `${sliderOneVal} - ${sliderTwoVal}`; + + // change which handle has higher z-index to make them still usable, + // ie when left handle reaches the end, it has to have higher z-index or else it will be stuck below + // and we cannot move right because it cannot go below min value + if (+this._sliderOneElm.value >= +this._sliderTwoElm.value - Z_INDEX_MIN_GAP) { + this._sliderOneElm.style.zIndex = '1'; + this._sliderTwoElm.style.zIndex = '0'; + } else { + this._sliderOneElm.style.zIndex = '0'; + this._sliderTwoElm.style.zIndex = '1'; + } + + this.updateTrackFilledColor(); + this.changeBothSliderFocuses(true); + if (!this.getFilterOptionByName('hideSliderNumbers') && this._lowestSliderNumberElm?.textContent) { + this._lowestSliderNumberElm.textContent = this._sliderOneElm.value; + } + + } + + protected slideTwoInputChanged() { + const sliderOneVal = parseInt(this._sliderOneElm.value, 10); + const sliderTwoVal = parseInt(this._sliderTwoElm.value, 10); + + if (sliderTwoVal - sliderOneVal <= this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)) { + this._sliderTwoElm.value = String(sliderOneVal + this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)); + } + + this.updateTrackFilledColor(); + this.changeBothSliderFocuses(true); + this._sliderRangeContainElm.title = `${sliderOneVal} - ${sliderTwoVal}`; + + if (!this.getFilterOptionByName('hideSliderNumbers') && this._highestSliderNumberElm?.textContent) { + this._highestSliderNumberElm.textContent = this._sliderTwoElm.value; + } + } + + protected sliderTrackClicked(e: MouseEvent) { + e.preventDefault(); + const sliderTrackX = e.offsetX; + const sliderTrackWidth = this._sliderTrackElm.offsetWidth; + const trackPercentPosition = (sliderTrackX + 0) * 100 / sliderTrackWidth; + + // when tracker position is below 50% we'll auto-place the left slider thumb or else auto-place right slider thumb + if (trackPercentPosition <= 50) { + this._sliderOneElm.value = `${trackPercentPosition}`; + this._sliderOneElm.dispatchEvent(new Event('change')); + } else { + this._sliderTwoElm.value = `${trackPercentPosition}`; + this._sliderTwoElm.dispatchEvent(new Event('change')); + } + } + + protected updateTrackFilledColor() { + if (this.getFilterOptionByName('enableSliderTrackColoring')) { + const percent1 = ((+this._sliderOneElm.value - +this._sliderOneElm.min) / (this.sliderRangeOptions?.maxValue ?? 0 - +this._sliderOneElm.min)) * 100; + const percent2 = ((+this._sliderTwoElm.value - +this._sliderTwoElm.min) / (this.sliderRangeOptions?.maxValue ?? 0 - +this._sliderTwoElm.min)) * 100; + const bg = 'linear-gradient(to right, %b %p1, %c %p1, %c %p2, %b %p2)' + .replace(/%b/g, '#eee') + .replace(/%c/g, (this.getFilterOptionByName('sliderTrackFilledColor') ?? 'var(--slick-slider-filter-thumb-color, #86bff8)') as string) + .replace(/%p1/g, `${percent1}%`) + .replace(/%p2/g, `${percent2}%`); + + this._sliderTrackElm.style.background = bg; + this._sliderOptions.sliderTrackBackground = bg; + } + } +} diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 71b796561..a18fe475a 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -146,6 +146,7 @@ export * from './slickRange.interface'; export * from './slickRemoteModel.interface'; export * from './slickResizer.interface'; export * from './slickRowDetailView.interface'; +export * from './sliderOption.interface'; export * from './sorter.interface'; export * from './textExportOption.interface'; export * from './treeDataOption.interface'; diff --git a/packages/common/src/interfaces/sliderOption.interface.ts b/packages/common/src/interfaces/sliderOption.interface.ts new file mode 100644 index 000000000..c8013befc --- /dev/null +++ b/packages/common/src/interfaces/sliderOption.interface.ts @@ -0,0 +1,24 @@ +export interface SliderOption { + /** Defaults to true, hide the slider number shown on the right side */ + hideSliderNumber?: boolean; + + /** Slider max end value */ + sliderEndValue?: number; + + /** Slider min start value */ + sliderStartValue?: number; +} + +export interface SliderRangeOption extends Omit { + /** Defaults to false, do we want to show slider track coloring? */ + enableSliderTrackColoring?: boolean; + + /** Defaults to false, hide the slider numbers shown on the left/right side */ + hideSliderNumbers?: boolean; + + /** Defaults to 0, minimum value gap before reaching the maximum end value */ + stopGapBetweenSliderHandles?: number; + + /** Defaults to "#3C97DD", what will be the color to use to represent slider range */ + sliderTrackFilledColor?: string; +} \ No newline at end of file diff --git a/packages/common/src/styles/_variables-theme-salesforce.scss b/packages/common/src/styles/_variables-theme-salesforce.scss index e21eff9b6..4ad3dcfbf 100644 --- a/packages/common/src/styles/_variables-theme-salesforce.scss +++ b/packages/common/src/styles/_variables-theme-salesforce.scss @@ -178,7 +178,7 @@ $slick-editor-modal-title-font-weight: var(--lwc-fontWeig $slick-editor-modal-title-line-height: var(--lwc-lineHeightHeading, 1.25) !default; $slick-editor-modal-title-text-align: center !default; $slick-large-editor-button-border-radius: 3px !default; -$slick-slider-filter-thumb-color: #3C97DD !default; +$slick-slider-filter-thumb-color: #6cb6ff !default; $slick-slider-filter-runnable-track-bgcolor: #ECEBEA !default; $slick-row-selected-color: #ECEBEA !default; $slick-row-highlight-background-color: lighten($slick-highlight-color, 50%) !default; diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 76a66601c..0ddf5edeb 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -659,37 +659,27 @@ $slick-draggable-group-column-icon-margin-left: 4px !default; $slick-slider-filter-border: 1px solid #ccc !default; $slick-slider-filter-bgcolor: #eee !default; $slick-slider-filter-runnable-track-bgcolor: #ddd !default; -$slick-slider-filter-runnable-track-height: 4px !default; +$slick-slider-filter-runnable-track-cursor: pointer !default; +$slick-slider-filter-runnable-track-height: 5px !default; $slick-slider-filter-runnable-track-padding: 0 6px !default; $slick-slider-filter-fill-lower-color: #ddd !default; /* ms only */ $slick-slider-filter-fill-focus-lower-color: #aaa !default; /* ms only */ $slick-slider-filter-height: $slick-header-input-height !default; $slick-slider-filter-thumb-border-radius: 50% !default; -$slick-slider-filter-thumb-cursor: pointer !default; +$slick-slider-filter-thumb-cursor: grab !default; $slick-slider-filter-thumb-color: rgb(201, 219, 203) !default; +$slick-slider-filter-thumb-active-bg-color: #fff !default; $slick-slider-filter-thumb-size: 14px !default; -$slick-slider-filter-thumb-height: calc(#{$slick-slider-filter-thumb-size} - 2px) !default; +$slick-slider-filter-thumb-height: calc(#{$slick-slider-filter-thumb-size} - 4px) !default; $slick-slider-filter-thumb-width: $slick-slider-filter-thumb-height !default; -$slick-slider-filter-thumb-border: 1px solid darken($slick-slider-filter-thumb-color, 15%) !default; +$slick-slider-filter-thumb-border: 2px solid darken($slick-slider-filter-thumb-color, 15%) !default; $slick-slider-filter-number-padding: 4px 8px !default; $slick-slider-filter-number-font-size: calc(#{$slick-font-size-base-value} - 1px) !default; -/* Input Range Slider Filter (with jQuery UI) */ -$slick-slider-range-filter-height: $slick-slider-filter-height !default; -$slick-slider-range-filter-border: $slick-slider-filter-border !default; -$slick-slider-range-filter-thumb-color: $slick-slider-filter-thumb-color !default; -$slick-slider-range-filter-thumb-border: $slick-slider-filter-thumb-border !default; -$slick-slider-range-filter-thumb-border-radius: $slick-slider-filter-thumb-border-radius !default; -$slick-slider-range-filter-thumb-cursor: $slick-slider-filter-thumb-cursor !default; -$slick-slider-range-filter-thumb-size: $slick-slider-filter-thumb-size !default; -$slick-slider-range-filter-thumb-top: -5px !default; -$slick-slider-range-filter-runnable-track-top: 45% !default; -$slick-slider-range-filter-runnable-track-height: $slick-slider-filter-runnable-track-height !default; -$slick-slider-range-filter-bgcolor: $slick-slider-filter-bgcolor !default; -$slick-slider-range-filter-padding: 0 12px !default; -$slick-slider-range-filter-values-slider-width: calc(98% - 16px) !default; -$slick-slider-range-filter-values-slider-top: 12px !default; -$slick-slider-range-filter-values-slider-margin: 0 10px !default; +/* Input Range Slider Filter */ +$slick-slider-range-filter-border-radius: 4px !default; +$slick-slider-range-focus-border-color: lighten($slick-primary-color, 10%) !default; +$slick-slider-range-focus-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px rgba(lighten($slick-primary-color, 3%), .3) !default; /* Multiple-Select Filter */ $slick-multiselect-input-filter-border: 1px solid #ccc !default; diff --git a/packages/common/src/styles/slick-filters.scss b/packages/common/src/styles/slick-filters.scss index 050875b9c..5cb6a2ebf 100644 --- a/packages/common/src/styles/slick-filters.scss +++ b/packages/common/src/styles/slick-filters.scss @@ -9,26 +9,27 @@ $slick-filled-filter-font-weight: 400 !default; .slick-headerrow { input.search-filter.filled, - .search-filter.filled input, - .search-filter.filled input.flatpickr-input, - .search-filter.filled .input-group-addon.slider-value, - .search-filter.filled .input-group-addon select { - color: var(--slick-filled-filter-color, $slick-filled-filter-color); - font-weight: var(--slick-filled-filter-font-weight, $slick-filled-filter-font-weight); - border: var(--slick-filled-filter-border, $slick-filled-filter-border); + .search-filter.filled input, + .search-filter.filled input.flatpickr-input, + .search-filter.filled .input-group-addon.slider-value, + .search-filter.filled .input-group-addon.slider-range-value, + .search-filter.filled .input-group-addon select { + color: var(--slick-filled-filter-color, $slick-filled-filter-color); + font-weight: var(--slick-filled-filter-font-weight, $slick-filled-filter-font-weight); + border: var(--slick-filled-filter-border, $slick-filled-filter-border); + box-shadow: var(--slick-filled-filter-box-shadow, $slick-filled-filter-box-shadow); + } + .search-filter.filled .input-group-addon select { + border-right: 0px; + } + .search-filter.filled { + .ms-choice { box-shadow: var(--slick-filled-filter-box-shadow, $slick-filled-filter-box-shadow); - } - .search-filter.filled .input-group-addon select { - border-right: 0px; - } - .search-filter.filled { - .ms-choice { - box-shadow: var(--slick-filled-filter-box-shadow, $slick-filled-filter-box-shadow); - border: var(--slick-filled-filter-border, $slick-filled-filter-border); - span { - font-weight: var(--slick-filled-filter-font-weight, $slick-filled-filter-font-weight); - color: var(--slick-filled-filter-color, $slick-filled-filter-color); - } + border: var(--slick-filled-filter-border, $slick-filled-filter-border); + span { + font-weight: var(--slick-filled-filter-font-weight, $slick-filled-filter-font-weight); + color: var(--slick-filled-filter-color, $slick-filled-filter-color); } } + } } \ No newline at end of file diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss index b60da4ce4..bc07acfe1 100644 --- a/packages/common/src/styles/slick-plugins.scss +++ b/packages/common/src/styles/slick-plugins.scss @@ -965,47 +965,51 @@ input.flatpickr.form-control { // ---------------------------------------------- // Input Slider Filter (with vanilla html) // ---------------------------------------------- -.ui-widget.ui-widget-content { - border: 0; +.slider-single { + input[type="range"] { + &::-webkit-slider-runnable-track, &:focus::-webkit-slider-runnable-track { + background: var(--slick-slider-filter-bgcolor, $slick-slider-filter-bgcolor); + } + &::-moz-range-track, &:focus::-moz-range-track { + background: var(--slick-slider-filter-bgcolor, $slick-slider-filter-bgcolor); + } + &::-ms-fill-lower { + background: var(--slick-slider-filter-fill-lower-color, $slick-slider-filter-fill-lower-color); + &:focus { + background: var(--slick-slider-filter-fill-focus-lower-color, $slick-slider-filter-fill-focus-lower-color); + } + } + &::-ms-fill-upper, &:focus::-ms-fill-upper { + background: var(--slick-slider-filter-bgcolor, $slick-slider-filter-bgcolor); + } + + &::-webkit-slider-runnable-track, &:focus::-webkit-slider-runnable-track { + background: var(--slick-slider-filter-bgcolor, $slick-slider-filter-bgcolor); + } + } } input.slider-editor-input[type=range], input.slider-filter-input[type=range] { /*removes default webkit styles*/ - -webkit-appearance: none; - height: var(--slick-slider-filter-height, $slick-slider-filter-height); + appearance: none; flex: 1; - + height: var(--slick-slider-filter-height, $slick-slider-filter-height); padding: var(--slick-slider-filter-runnable-track-padding, $slick-slider-filter-runnable-track-padding); /* change runnable track color while in focus on all browsers */ &:focus { outline: none; - - &::-webkit-slider-runnable-track { - background: var(--slick-slider-filter-runnable-track-bgcolor, $slick-slider-filter-runnable-track-bgcolor); - } - &::-moz-range-track { - background: var(--slick-slider-filter-runnable-track-bgcolor, $slick-slider-filter-runnable-track-bgcolor); - } - &::-ms-fill-lower { - background: var(--slick-slider-filter-fill-focus-lower-color, $slick-slider-filter-fill-focus-lower-color); - } - &::-ms-fill-upper { - background: var(--slick-slider-filter-runnable-track-bgcolor, $slick-slider-filter-runnable-track-bgcolor); - } } /* WebKit specific (Opera/Chrome/Safari) */ &::-webkit-slider-runnable-track { height: var(--slick-slider-filter-runnable-track-height, $slick-slider-filter-runnable-track-height); - background: var(--slick-slider-filter-bgcolor, $slick-slider-filter-bgcolor); border: none; border-radius: 3px; } &::-webkit-slider-thumb { cursor: var(--slick-slider-filter-thumb-cursor, $slick-slider-filter-thumb-cursor); -webkit-appearance: none; - border: none; height: var(--slick-slider-filter-thumb-size, $slick-slider-filter-thumb-size); width: var(--slick-slider-filter-thumb-size, $slick-slider-filter-thumb-size); border-radius: var(--slick-slider-filter-thumb-border-radius, $slick-slider-filter-thumb-border-radius); @@ -1021,12 +1025,10 @@ input.slider-filter-input[type=range] { &::-moz-range-track { height: var(--slick-slider-filter-runnable-track-height, $slick-slider-filter-runnable-track-height); - background: var(--slick-slider-filter-bgcolor, $slick-slider-filter-bgcolor); border: none; border-radius: 3px; } &::-moz-range-thumb { - border: none; cursor: var(--slick-slider-filter-thumb-cursor, $slick-slider-filter-thumb-cursor); height: var(--slick-slider-filter-thumb-height, $slick-slider-filter-thumb-height); width: var(--slick-slider-filter-thumb-width, $slick-slider-filter-thumb-width); @@ -1064,7 +1066,6 @@ input.slider-filter-input[type=range] { border-radius: 10px; } &::-ms-thumb { - border: none; cursor: var(--slick-slider-filter-thumb-cursor, $slick-slider-filter-thumb-cursor); height: var(--slick-slider-filter-thumb-height, $slick-slider-filter-thumb-height); width: var(--slick-slider-filter-thumb-width, $slick-slider-filter-thumb-width); @@ -1076,6 +1077,14 @@ input.slider-filter-input[type=range] { &::-ms-tooltip { display: none; } + &:active::-webkit-slider-thumb { + background-color: var(--slick-slider-filter-thumb-active-bg-color, $slick-slider-filter-thumb-active-bg-color); + border: 2px solid var(--slick-slider-filter-thumb-color, $slick-slider-filter-thumb-color); + } + &:active::-moz-range-thumb { + background-color: var(--slick-slider-filter-thumb-active-bg-color, $slick-slider-filter-thumb-active-bg-color); + border: 2px solid var(--slick-slider-filter-thumb-color, $slick-slider-filter-thumb-color); + } } .search-filter { height: var(--slick-header-input-height, $slick-header-input-height); @@ -1117,67 +1126,81 @@ input.slider-editor-input[type=range] { } } + // ---------------------------------------------- -// Input Slider Range Filter (using jQuery UI) +// Input Slider Range Filter (with vanilla html) // ---------------------------------------------- -.slider-range-container { - height: var(--slick-slider-range-filter-height, $slick-slider-range-filter-height); - padding: var(--slick-slider-range-filter-padding, $slick-slider-range-filter-padding); - .ui-slider { - position: relative; +.slider-container { + .slider-range-value { + padding: 0; + height: 100%; + .input-group-text { + padding: var(--slick-slider-filter-number-padding, $slick-slider-filter-number-padding); + font-size: var(--slick-slider-filter-number-font-size, $slick-slider-filter-number-font-size); + } + } + &:not(.input-group) { + .slider-filter-input { + border-radius: var(--slick-slider-range-filter-border-radius, $slick-slider-range-filter-border-radius); + } + } +} +.slider-range-container { + position: relative; + width: 100%; - .ui-slider-handle { - position: absolute; - top: var(--slick-slider-range-filter-thumb-top, $slick-slider-range-filter-thumb-top); - border-radius: var(--slick-slider-range-filter-thumb-border-radius, $slick-slider-range-filter-thumb-border-radius); - cursor: var(--slick-slider-range-filter-thumb-cursor, $slick-slider-range-filter-thumb-cursor); - border: var(--slick-slider-range-filter-thumb-border, $slick-slider-range-filter-thumb-border); - height: var(--slick-slider-range-filter-thumb-size, $slick-slider-range-filter-thumb-size); - width: var(--slick-slider-range-filter-thumb-size, $slick-slider-range-filter-thumb-size); - background-color: var(--slick-slider-range-filter-thumb-color, $slick-slider-range-filter-thumb-color); + .slider-track { + cursor: var(--slick-slider-filter-runnable-track-cursor, $slick-slider-filter-runnable-track-cursor); + width: calc(100% - 16px); + height: var(--slick-slider-filter-runnable-track-height, $slick-slider-filter-runnable-track-height); + position: absolute; + margin: auto; + margin-left: 8px; + top: 0; + bottom: 0; + border-radius: 3px; + background: var(--slick-slider-filter-runnable-track-bgcolor, $slick-slider-filter-runnable-track-bgcolor); + } - &:focus { - outline: none; - } + input[type="range"] { + position: absolute; + background-color: transparent; + pointer-events: none; + width: 100%; + &.focus { + outline: 0; + border-color: var(--slick-slider-range-focus-border-color, $slick-slider-range-focus-border-color); + box-shadow: var(--slick-slider-range-focus-box-shadow, $slick-slider-range-focus-box-shadow); } } - .ui-slider-horizontal { - top: var(--slick-slider-range-filter-runnable-track-top, $slick-slider-range-filter-runnable-track-top); - height: var(--slick-slider-range-filter-runnable-track-height, $slick-slider-range-filter-runnable-track-height); - background-color: var(--slick-slider-range-filter-bgcolor, $slick-slider-range-filter-bgcolor); + input[type="range"]::-webkit-slider-runnable-track { + -webkit-appearance: none; } - .input-group-text { - border: 0; - + input[type="range"]::-moz-range-track { + -moz-appearance: none; } -} -.slider-range-container.slider-values { - padding: 0; - .ui-slider-horizontal { - flex: 1; - width: var(--slick-slider-range-filter-values-slider-width, $slick-slider-range-filter-values-slider-width); - top: var(--slick-slider-range-filter-values-slider-top, $slick-slider-range-filter-values-slider-top); - margin: var(--slick-slider-range-filter-values-slider-margin, $slick-slider-range-filter-values-slider-margin); + input[type="range"]::-ms-track { + appearance: none; } - .slider-range-value { - padding: 0; - border: 0; - height: 100%; - .input-group-text { - padding: var(--slick-slider-filter-number-padding, $slick-slider-filter-number-padding); - font-size: var(--slick-slider-filter-number-font-size, $slick-slider-filter-number-font-size); - } + input[type="range"]::-webkit-slider-thumb { + pointer-events: auto; } - .input-group-prepend.slider-range-value { - border-right: var(--slick-slider-range-filter-border, $slick-slider-range-filter-border); + input[type="range"]::-moz-range-thumb { + pointer-events: auto; } - .input-group-append.slider-range-value { - border-left: var(--slick-slider-range-filter-border, $slick-slider-range-filter-border); + input[type="range"]::-ms-thumb { + appearance: none; + pointer-events: auto; } } +.slider-range-container.slider-values { + display: flex; + padding: 0; +} + // --------------------------------------------------------- // Row Detail View Plugin // --------------------------------------------------------- diff --git a/packages/common/src/styles/slick-without-bootstrap-min-styling.scss b/packages/common/src/styles/slick-without-bootstrap-min-styling.scss index 61c7244cd..a0262adf5 100644 --- a/packages/common/src/styles/slick-without-bootstrap-min-styling.scss +++ b/packages/common/src/styles/slick-without-bootstrap-min-styling.scss @@ -94,6 +94,9 @@ $slick-form-control-focus-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px .input-group .form-control, .input-group-addon, .input-group-btn { display: table-cell; } + .input-group-addon:first-child { + border-right: 0; + } .input-group-addon:last-child { border-left: 0; } diff --git a/test/cypress/e2e/example14.cy.js b/test/cypress/e2e/example14.cy.js index c2980b75b..3ca2cfbba 100644 --- a/test/cypress/e2e/example14.cy.js +++ b/test/cypress/e2e/example14.cy.js @@ -19,7 +19,7 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 83); cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('equal', 98); cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('equal', 67); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 110); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 160); cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('equal', 106); cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('equal', 88); cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('equal', 68); @@ -35,7 +35,7 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 75); cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('equal', 98); cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('equal', 67); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 102); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 152); cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('equal', 98); cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('equal', 80); cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('equal', 68); @@ -51,7 +51,7 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.lt', 75); cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.lt', 95); cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.lt', 70); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.lt', 150); cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 100); cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.lt', 85); cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.lt', 70);