in packages/attribute-slicer/src/AttributeSlicer.ts [54:1159]
const naturalSort: (
a: unknown,
b: unknown,
) => number = require("javascript-natural-sort");
/**
* Represents an advanced slicer to help slice through data
*/
export class AttributeSlicer {
/**
* The selection manager to use
*/
private selectionManager: JQuerySelectionManager<IItemReference>;
/**
* The slicer element
*/
private slicerEle: BaseSelection;
/**
* The actual list element
*/
private listEle: BaseSelection;
/**
* The clearAll element
*/
private clearAllEle: BaseSelection;
/**
* The check all button
*/
private checkAllEle: BaseSelection;
/**
* Our container element
*/
private element: BaseSelection;
/**
* The data contained in this slicer
*/
private internalData: ISlicerItem[] = [];
private internalEventEmitter: EventEmitter = new EventEmitter();
/**
* Container for the selections
*/
private selectionsEle: BaseSelection;
/**
* Stores the currently loading promise
*/
private loadPromise: PromiseLike<undefined | ISlicerItem[] | void> & {
cancel?: boolean;
};
/**
* Whether or not we are loading the search box
*/
private loadingSearch: boolean = false;
private virtualList: VirtualList;
/**
* The virtual list element
*/
private virtualListEle: BaseSelection;
/**
* Whether or not we are currently loading a state
*/
private loadingState: boolean = false;
/**
* The number of milliseconds before running the search, after a user stops typing
*/
private searchDebounce: number = SEARCH_DEBOUNCE;
/**
* Whether this component has been destroyed
*/
private destroyed: boolean = false;
private internalItemTextColor: string = "#000";
private internalLeftAlignText: boolean = false;
private internalDisplayValueLables: boolean = false;
private internalOverflowValueLabels: boolean = false;
private internalShowOptions: boolean = true;
private internalShowSearchBox: boolean = true;
private internalServerSideSearch: boolean = true;
private internalCaseInsentitive: boolean = true;
private internalRenderHorizontal: boolean = false;
private internalDimensions?: IDimensions;
private internalValueWidthPercentage: number = DEFAULT_VALUE_WIDTH;
private internalShowValues: boolean = false;
private internalShowSelections: boolean = true;
private internalFontSize: number = DEFAULT_TEXT_SIZE;
private internalSearchString: string = "";
private internalLoadingMoreData: boolean = false; // don't use this directly
/**
* Updates the list height
*/
private updateListHeight: () => void = lodashDebounce(() => {
if (!this.destroyed && this.dimensions) {
const optionsHeight = (<HTMLElement>(
this.element.select(".slicer-options").node()
)).getBoundingClientRect().height;
const height: number = this.dimensions.height - optionsHeight - 2;
this.listEle.style("width", "100%");
this.listEle.style("height", `${height}px`);
// .attr("dir", dir);
this.virtualList.setHeight(height);
this.virtualList.setDir(this.renderHorizontal);
}
}, 50);
private toggleLoadingClass: Function = lodashDebounce(() => {
this.element.classed("loading", this.loadingMoreData);
}, 100);
/**
* Whether or not to left align item text
*/
public get leftAlignText(): boolean {
return this.internalLeftAlignText;
}
/**
* Sets wheter or not to left align item text
*/
public set leftAlignText(value: boolean) {
if (value !== this.internalLeftAlignText) {
this.internalLeftAlignText = value;
if (this.virtualList) {
this.virtualList.rerender();
}
}
}
/**
* Whether or not to display values
*/
public get displayValueLabels(): boolean {
return this.internalDisplayValueLables;
}
/**
* Sets wheter or not to display values
*/
public set displayValueLabels(value: boolean) {
if (value !== this.internalDisplayValueLables) {
this.internalDisplayValueLables = value;
if (this.virtualList) {
this.virtualList.rerender();
}
}
}
/**
* Wheter or not to set value text overflow to visible
*/
public get overflowValueLabels(): boolean {
return this.internalOverflowValueLabels;
}
/**
* Sets wheter or not to use allow value text to overflow
*/
public set overflowValueLabels(shouldOverflow: boolean) {
if (shouldOverflow !== this.internalOverflowValueLabels) {
this.internalOverflowValueLabels = shouldOverflow;
if (this.virtualList) {
this.virtualList.rerender();
}
}
}
/**
* Font color used to display item text
*/
public get itemTextColor(): string {
return this.internalItemTextColor;
}
/**
* Sets the font color used to display item text
*/
public set itemTextColor(color: string) {
if (color !== this.internalItemTextColor) {
this.internalItemTextColor = color;
if (this.virtualList) {
this.virtualList.rerender();
}
}
}
/**
* Constructor for the advanced slicer
*/
constructor(
element: BaseSelection,
config?: { searchDebounce?: number },
vlist?: VirtualList,
) {
this.showSelections = true;
element.append(
() =>
html`
${require("raw-loader!./AttributeSlicer.tmpl.html").default}
`,
);
this.element = element.select(".advanced-slicer");
this.listEle = this.element.select(".list");
this.searchDebounce = lodashGet(config, "searchDebounce", SEARCH_DEBOUNCE);
this.virtualList =
vlist ||
new VirtualList({
itemHeight: this.fontSize * 2,
afterRender: () => {
return this.selectionManager.refresh();
},
generatorFn: (i: number): HTMLElement => {
const item: ISlicerItem = <ISlicerItem>this.virtualList.items[i];
const ele = slicerItemTemplate(
item,
this.calcColumnSizes(),
this.leftAlignText,
this.displayValueLabels,
this.itemTextColor,
this.overflowValueLabels,
);
ele
.style("height", `${this.virtualList.itemHeight - 4}px`)
.style("padding-bottom", "2.5px")
.style("padding-top", "2px")
.datum(item);
return ele.node();
},
});
this.selectionManager = new JQuerySelectionManager<IItemReference>(
(items: IItemReference[]): void => {
this.syncSelectionTokens(items);
this.raiseSelectionChanged(items);
},
);
this.element.classed("show-selections", this.showSelections);
// We should just pass this info into the constructor
this.selectionManager.bindTo(
this.listEle,
"item",
(ele: BaseSelection): IItemReference => ele.datum(),
(i: IItemReference) =>
<BaseSelection>this.listEle.selectAll(".item").filter(function() {
const ele = this;
return (<IItemReference>select(ele).datum()).id === i.id;
}),
);
this.fontSize = this.fontSize;
this.virtualListEle = this.virtualList.container;
const emitScrollEvent: Function = lodashDebounce(
(position: [number, number]) => {
this.events.raiseEvent("scroll", position);
},
500,
);
this.virtualListEle.on("scroll.attribute-slicer", () => {
const event = d3Selection.event;
const position: [number, number] = [
event.target.scrollTop,
event.target.scrollLeft,
];
emitScrollEvent(position);
this.checkLoadMoreData();
});
this.listEle.append(() => this.virtualListEle.node());
this.selectionsEle = element.select(".selections");
this.checkAllEle = <BaseSelection>(
element
.select(".check-all")
.on("click.attribute-slicer", () => this.toggleSelectAll())
);
this.clearAllEle = <BaseSelection>(
element.select(".clear-all").on("click.attribute-slicer", () => {
this.search("");
this.clearSelection();
})
);
this.attachEvents();
this.brushSelectionMode = false;
// these two are here because the devtools call init more than once
this.loadingMoreData = true;
}
public get scrollPosition(): [number, number] {
const element: BaseSelection = this.virtualListEle;
if (element) {
return [element.node().scrollTop, element.node().scrollLeft];
}
return [0, 0];
}
public set scrollPosition(value: [number, number]) {
const element: BaseSelection = this.virtualListEle;
if (element) {
element.node().scrollTop = value[0];
element.node().scrollLeft = value[1];
}
}
/**
* Builds the current state
*/
public get state(): IAttributeSlicerState {
return {
selectedItems: this.selectedItems.map(
(n: ISlicerItem) => <ISlicerItem>lodashClonedeep(n),
),
searchText: this.searchString || "",
labelDisplayUnits: 0,
labelPrecision: 0,
horizontal: this.renderHorizontal,
valueColumnWidth: this.valueWidthPercentage,
showSelections: this.showSelections,
singleSelect: this.singleSelect,
brushMode: this.brushSelectionMode,
textSize: this.fontSize,
leftAlignText: this.leftAlignText,
showOptions: this.showOptions,
showSearch: this.showSearchBox,
searchSupported: this.showSearchBox,
showValues: this.showValues,
scrollPosition: this.scrollPosition,
displayValueLabels: this.displayValueLabels,
itemTextColor: this.itemTextColor,
overflowValueLabels: this.overflowValueLabels,
};
}
/**
* Loads our state from the given state
*/
public set state(state: IAttributeSlicerState) {
this.loadingState = true;
state = lodashMerge({}, lodashClonedeep(DEFAULT_STATE), state);
this.singleSelect = state.singleSelect;
this.brushSelectionMode = state.brushMode;
this.showSelections = state.showSelections;
this.showOptions = state.showOptions;
this.showSearchBox = state.showSearch && state.searchSupported;
this.showValues = state.showValues;
const newSearchString: string = state.searchText;
if (newSearchString !== this.searchString) {
this.searchString = newSearchString;
}
this.fontSize = state.textSize;
this.selectedItems = (state.selectedItems || []).map(
(n: IItemReference) => {
return lodashMerge({}, n);
},
);
this.renderHorizontal = state.horizontal;
this.valueWidthPercentage = state.valueColumnWidth;
this.scrollPosition = state.scrollPosition;
this.leftAlignText = state.leftAlignText;
this.displayValueLabels = state.displayValueLabels;
this.itemTextColor = state.itemTextColor;
this.overflowValueLabels = state.overflowValueLabels;
this.loadingState = false;
}
/**
* Setter for whether or not the slicer options should be shown
*/
public set showOptions(value: boolean) {
this.internalShowOptions = value;
this.syncUIVisibility();
}
/**
* Getter for showOptions
*/
public get showOptions(): boolean {
return this.internalShowOptions;
}
/**
* Setter for whether or not the slicer search box should be shown
*/
public set showSearchBox(value: boolean) {
this.internalShowSearchBox = value;
this.syncUIVisibility();
}
/**
* Getter for showSearchBox
*/
public get showSearchBox(): boolean {
return this.internalShowSearchBox;
}
/**
* Setter for server side search
*/
public set serverSideSearch(value: boolean) {
this.internalServerSideSearch = value;
}
/**
* Getter for server side search
*/
public get serverSideSearch(): boolean {
return this.internalServerSideSearch;
}
/**
* Setter for if the attribute slicer should be single select
*/
public set singleSelect(value: boolean) {
if (value !== this.selectionManager.singleSelect) {
this.selectionManager.singleSelect = value;
}
}
/**
* Getter for single select
*/
public get singleSelect(): boolean {
return this.selectionManager.singleSelect;
}
/**
* Setter for if the attribute slicer should use brush selection mode
*/
public set brushSelectionMode(value: boolean) {
if (value !== this.selectionManager.brushMode) {
this.selectionManager.brushMode = value;
this.element.classed("brush-mode", value);
}
}
/**
* Getter for should use brush selection mode
*/
public get brushSelectionMode(): boolean {
return this.selectionManager.brushMode;
}
/**
* Gets whether or not the search is case insensitive
*/
public get caseInsensitive(): boolean {
return this.internalCaseInsentitive;
}
/**
* Setter for case insensitive
*/
public set caseInsensitive(value: boolean) {
this.internalCaseInsentitive = value;
this.syncItemVisiblity();
}
/**
* Gets our event emitter
*/
public get events(): EventEmitter {
return this.internalEventEmitter;
}
/**
* Whether or not to render horizontal
*/
public get renderHorizontal(): boolean {
return this.internalRenderHorizontal;
}
/**
* Sets whether or not to render horizontal
*/
public set renderHorizontal(value: boolean) {
if (value !== this.internalRenderHorizontal) {
this.internalRenderHorizontal = value;
this.element.classed("render-horizontal", value);
this.updateListHeight();
}
}
/**
* The actual dimensions
*/
public get dimensions(): IDimensions | undefined {
return this.internalDimensions;
}
/**
* Sets the dimension of the slicer
*/
public set dimensions(dims: IDimensions) {
this.internalDimensions = dims;
this.updateListHeight();
}
/**
* Getter for the percentage width of the value column (10 - 100)
*/
public get valueWidthPercentage(): number {
return this.internalValueWidthPercentage;
}
/**
* Setter for the percentage width of the value column (10 - 100)
*/
public set valueWidthPercentage(value: number) {
value = value ? Math.max(Math.min(value, 100), 10) : DEFAULT_VALUE_WIDTH;
if (value !== this.internalValueWidthPercentage) {
this.internalValueWidthPercentage = value;
this.resizeColumns();
}
}
/**
* Setter for showing the values column
*/
public set showValues(show: boolean) {
if (show !== this.internalShowValues) {
this.internalShowValues = show;
this.element.classed("has-values", show);
}
}
/**
* Getter for show values
*/
public get showValues(): boolean {
return this.internalShowValues;
}
/**
* Controls whether or not to show the selection tokens
*/
public get showSelections(): boolean {
return this.internalShowSelections;
}
/**
* Setter for showing the selections area
*/
public set showSelections(show: boolean) {
if (show !== this.internalShowSelections) {
this.internalShowSelections = show;
this.element.classed("show-selections", show);
this.syncItemVisiblity();
}
}
/**
* Gets whether or not we are showing the highlights
*/
public get showHighlight(): boolean {
return this.element.classed("show-highlight");
}
/**
* Toggles whether or not to show highlights
*/
public set showHighlight(highlight: boolean) {
this.element.classed("show-highlight", !!highlight);
}
/**
* Gets the data behind the slicer
*/
public get data(): ISlicerItem[] {
return this.internalData;
}
/**
* Sets the slicer data
*/
public set data(newData: ISlicerItem[]) {
// If the user is straight up just setting new data, then clear the selected item
// Otherwise, we are appending from a search/page action, it doesn't make sense to clear it.
if (!this.loadingMoreData) {
this.selectedItems = [];
}
if (newData && newData.length) {
// This forces a visibility change for the items (if necessary)
this.search(this.searchString);
}
this.internalData = newData;
this.selectionManager.items = newData;
// Not necessary as performed in syncItemVisibility
// this.virtualList.setItems(newData);
this.syncItemVisiblity(true);
this.updateSelectAllButtonState();
// If this is just setting data, we are not currently in a load cycle
// Justification: When you change case insensitive in PBI, it reloads the data, filtering it
// and passing it to us. But that sometimes is not enough data ie start with 100 items,
// after a filter you have 2, well we need more data to fill the screen, this accounts for that
if (!this.loadingMoreData) {
setTimeout(() => this.checkLoadMoreData(), 10);
}
// if some one sets the data, then clearly we are no longer loading data
this.loadingMoreData = false;
}
/**
* Controls the size of the font
*/
public get fontSize(): number {
return this.internalFontSize;
}
/**
* Setter for fontSize
*/
public set fontSize(value: number) {
value = value || DEFAULT_TEXT_SIZE;
if (value !== this.internalFontSize) {
this.internalFontSize = value;
this.element.style("font-size", `${this.internalFontSize}px`);
if (this.virtualList) {
this.virtualList.setItemHeight(this.internalFontSize * 2);
}
}
}
/**
* The list of selected items
*/
public get selectedItems(): IItemReference[] {
return this.selectionManager.selection;
}
/**
* Sets the set of selected items
*/
public set selectedItems(value: IItemReference[]) {
this.selectionManager.selection = value;
this.syncSelectionTokens(value);
}
/**
* Gets the current serch value
*/
public get searchString(): string | undefined {
return this.internalSearchString;
}
/**
* Gets the current serch value
*/
public set searchString(value: string) {
value = value || "";
if (value !== this.internalSearchString) {
this.internalSearchString = value || "";
this.loadingSearch = true;
this.element.select(".searchbox").property("value", value);
this.loadingSearch = false;
this.syncUIVisibility(false);
}
}
/**
* A boolean indicating whether or not the list is loading more data
*/
protected get loadingMoreData(): boolean {
return this.internalLoadingMoreData;
}
/**
* Setter for loadingMoreData
*/
protected set loadingMoreData(value: boolean) {
this.internalLoadingMoreData = value;
// Little janky, but what this does is ensures that if we are loading,
// to set the loading flag immediately. We also want to remove the load
// flag slowly, in case we are loading stuff a bunch (ie, scroll load)
if (value) {
this.element.classed("loading", true);
}
this.toggleLoadingClass();
}
/**
* Determines if the given slice item matches the given string value
*/
public static IS_MATCH(
item: ISlicerItem,
matchValue: string,
caseInsensitive: boolean,
): boolean {
const searchStr: string = pretty(matchValue);
const flags: string = caseInsensitive ? "i" : "";
const regex: RegExp = new RegExp(
AttributeSlicer.escapeRegExp(searchStr),
flags,
);
return regex.test(pretty(item.text));
}
/**
* Escapes RegExp
*/
private static escapeRegExp(str: string): string {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
/**
* Calculates the column sizes for both the value and category columns
*/
public calcColumnSizes(): { value: number; category: number } {
const remaining: number = 100 - this.valueWidthPercentage;
return {
value: this.showValues ? this.valueWidthPercentage : 0,
category: this.showValues ? remaining : 100,
};
}
/**
* Destroys this attribute slicer
*/
public destroy(): void {
this.destroyed = true;
if (this.selectionManager) {
this.selectionManager.destroy();
this.selectionManager = undefined;
}
if (this.virtualList) {
this.virtualList.destroy();
this.virtualList = undefined;
}
}
/**
* Performs a search
*/
public search(searchStr: string): void {
// If the search string has not changed we don't need to query for more data
if (this.searchString !== searchStr) {
this.searchString = searchStr;
if (this.serverSideSearch) {
const prevLoadState: boolean = this.loadingMoreData;
this.loadingMoreData = true;
setTimeout(() => {
if (!this.checkLoadMoreDataBasedOnSearch()) {
this.loadingMoreData = prevLoadState;
}
}, 10);
}
this.raiseSearchPerformed(searchStr);
}
// this is required because when the list is done searching it
// adds back in cached elements with selected flags
this.syncItemVisiblity();
this.element.classed("has-search", !!this.searchString);
}
/**
* Refreshes the display when data changes
*/
public refresh(): void {
if (this.virtualList) {
this.virtualList.rerender();
}
}
/**j
* Sorts the slicer
*/
public sort(sortProp: string, desc?: boolean): void {
this.data.sort((a: ISlicerItem, b: ISlicerItem) => {
const sortVal: number = naturalSort(a[sortProp], b[sortProp]);
return desc ? -1 * sortVal : sortVal;
});
}
/**
* Listener for the list scrolling
*/
protected checkLoadMoreData(): void {
const scrollElement: HTMLElement = this.virtualListEle.node();
const sizeProp: string = this.renderHorizontal ? "Width" : "Height";
const posProp: string = this.renderHorizontal ? "Left" : "Top";
const scrollSize: number = scrollElement[`scroll${sizeProp}`];
const scrollPos: number = scrollElement[`scroll${posProp}`];
const shouldScrollLoad: boolean =
scrollSize - (scrollPos + scrollElement[`client${sizeProp}`]) < 200;
if (
shouldScrollLoad &&
!this.loadingMoreData &&
this.raiseCanLoadMoreData()
) {
this.raiseLoadMoreData(false);
}
}
/**
* Raises the search performed event
*/
protected raiseSearchPerformed(searchText: string): void {
this.events.raiseEvent("searchPerformed", searchText);
}
/**
* Raises the event to load more data
*/
protected raiseLoadMoreData(isNewSearch: boolean): void {
const item: { result?: PromiseLike<ISlicerItem[]> } = {};
this.events.raiseEvent(
"loadMoreData",
item,
isNewSearch,
this.searchString,
);
if (item.result) {
this.loadingMoreData = true;
const promise: PromiseLike<void | ISlicerItem[]> & {
cancel?: boolean;
} = (this.loadPromise = item.result.then(
(items: ISlicerItem[]) => {
// if this promise hasn't been cancelled
if (!promise || !promise.cancel) {
this.loadPromise = undefined;
if (isNewSearch) {
this.data = items;
} else {
this.data = this.data.concat(items);
}
// make sure we don't need to load more after this,
// in case it doesn't all fit on the screen
setTimeout(() => {
this.checkLoadMoreData();
if (!this.loadPromise) {
this.loadingMoreData = false;
}
}, 10);
return items;
}
},
() => {
// If we are rejected, we don't need to clear the data,
// this just means the retrieval for more data failed, leave the data
// this.data = [];
this.loadingMoreData = false;
},
));
}
this.loadingMoreData = false;
}
/**
* Raises the event 'can
* '
*/
protected raiseCanLoadMoreData(isSearch: boolean = false): boolean {
const item: { result: boolean } = {
result: false,
};
this.events.raiseEvent("canLoadMoreData", item, isSearch);
return item.result;
}
/**
* Raises the selectionChanged event
*/
protected raiseSelectionChanged(newItems: IItemReference[]): void {
this.events.raiseEvent("selectionChanged", newItems);
}
/**
* Resizes all of the visible columns
*/
private resizeColumns(): void {
const sizes: {
value: number;
category: number;
} = this.calcColumnSizes();
this.element
.select(".value-container")
.style("max-width", `${sizes.value}%`);
this.element
.select(".category-container")
.style("max-width", `${sizes.category}%`);
}
/**
* Syncs the tokens in the UI with the actual selection
*/
private syncSelectionTokens(items: IItemReference[]): void {
// Important that these are always in sync, in case showSelections gets set to true
if (items) {
this.selectionsEle.selectAll(".token").remove();
items
.map((v: IItemReference) => this.createSelectionToken(v))
.forEach((n: BaseSelection) => {
this.selectionsEle.append(() => {
return n.node();
});
});
}
// We don't need to do any of this if show selections is off
if (this.showSelections) {
this.syncItemVisiblity();
}
this.syncUIVisibility();
}
/**
* Syncs the item elements state with the current set of selected items and the search
*/
private syncItemVisiblity(forceLoad: boolean = false): void {
let filteredData: ISlicerItem[] = [];
if (this.data && this.data.length) {
filteredData = this.data.filter((n: ISlicerItem, i: number) => {
const item: ISlicerItem = this.data[i];
let isVisible: boolean =
!this.showSelections ||
!(
!!this.selectedItems &&
this.selectedItems.filter((b: ISlicerItem) => b.id === item.id)
.length > 0
);
// update the search
if (isVisible && !this.serverSideSearch && this.searchString) {
isVisible = AttributeSlicer.IS_MATCH(
item,
this.searchString,
this.caseInsensitive,
);
}
return isVisible;
});
}
if (
this.virtualList &&
(forceLoad ||
filteredData.length !== (this.virtualList.items || []).length)
) {
this.virtualList.setItems(filteredData);
// this.virtualList.rerender();
}
}
/**
* Syncs the UIs Visibility
*/
private syncUIVisibility(calcList: boolean = true): void {
const hasSelection: boolean = !!(
this.selectedItems && this.selectedItems.length
);
toggleElement(this.element.select(".slicer-options"), this.showOptions);
toggleElement(this.element.select(".searchbox"), this.showSearchBox);
toggleElement(
this.element.select(".slicer-options"),
this.showOptions && (this.showSearchBox || hasSelection),
);
toggleElement(this.clearAllEle, hasSelection || !!this.searchString);
// If we are no longer showing the search box, hide the search string
if (!this.showSearchBox) {
this.searchString = "";
}
if (calcList) {
this.updateListHeight();
}
}
/**
* Toggle the select all state
*/
private toggleSelectAll(): void {
const checked: boolean = this.checkAllEle.property("checked");
if (!!checked) {
this.selectedItems = this.internalData.slice(0);
} else {
this.selectedItems = [];
}
}
/**
* Creates a new selection token element
*/
private createSelectionToken(v: IItemReference): BaseSelection {
const newEle: BaseSelection = create("div");
const text: string = pretty(v.text);
newEle
.classed("token", true)
.attr("title", text)
.datum(v)
.on("click.attribute-slicer", () => {
newEle.remove();
const item: IItemReference = this.selectedItems.filter(
(n: IItemReference) => n.id === v.id,
)[0];
const newSel: IItemReference[] = this.selectedItems.slice(0);
newSel.splice(newSel.indexOf(item), 1);
this.selectedItems = newSel;
})
.text(text);
return newEle;
}
/**
* Clears the selection
*/
private clearSelection(): void {
this.selectedItems = [];
}
/**
* Updates the select all button state to match the data
*/
private updateSelectAllButtonState(): void {
this.checkAllEle.property(
"indeterminate",
this.selectedItems.length > 0 &&
this.internalData.length !== this.selectedItems.length,
);
this.checkAllEle.property("checked", this.selectedItems.length > 0);
}
/**
* Attaches all the necessary events
*/
private attachEvents(): void {
const searchDebounced: Function = lodashDebounce(
() => this.search(this.getSearchStringFromElement()),
this.searchDebounce,
);
this.element.select(".searchbox").on("input.attribute-slicer", () => {
if (!this.loadingSearch) {
searchDebounced();
}
});
this.listEle.on("click.attribute-slicer", () => {
const evt = d3Selection.event;
evt.stopImmediatePropagation();
evt.stopPropagation();
});
}
/**
* Gets the search string from the search box
*/
private getSearchStringFromElement(): string {
return this.element.select(".searchbox").property("value") || "";
}
/**
* Loads more data based on search
* @param force Force the loading of new data, if it can
*/
private checkLoadMoreDataBasedOnSearch(): boolean {
// only need to load if:
// 1. There is more data. 2. There is not too much stuff on the screen (not causing a scroll)
if (this.raiseCanLoadMoreData(true)) {
if (this.loadPromise) {
this.loadPromise.cancel = true;
}
// we're not currently loading data, cause we cancelled
this.loadingMoreData = false;
this.raiseLoadMoreData(true);
return true;
}
}
}