text: import {
    inject,
    bindable,
    bindingMode,
    BindingEngine,
    DOM,
  } from "aurelia-framework";
  
  @inject(BindingEngine, Element)
  export class AureliaTableCustomAttribute {
    @bindable dataSource = "local"; // local, ajax and server;
  
    @bindable data;
    @bindable({ defaultBindingMode: bindingMode.twoWay }) displayData;
  
    @bindable filters;
  
    @bindable({ defaultBindingMode: bindingMode.twoWay }) currentPage;
    @bindable pageSize;
    @bindable({ defaultBindingMode: bindingMode.twoWay }) totalItems;
  
    @bindable({ defaultBindingMode: bindingMode.twoWay }) api;
  
    isAttached = false;
  
    sortKey;
    sortOrder;
    sortChangedListeners = [];
  
    @bindable({ defaultBindingMode: bindingMode.twoWay }) beforePagination = [];
  
    dataObserver;
    filterObservers = [];
  
    constructor(bindingEngine, element) {
      this.bindingEngine = bindingEngine;
      this.element = element;
    }
  
    bind() {
      if (Array.isArray(this.data)) {
        this.dataObserver = this.bindingEngine
          .collectionObserver(this.data)
          .subscribe(() => this.applyPlugins());
      }
  
      this.filtersChanged();
  
      this.api = {
        revealItem: (item) => this.revealItem(item),
      };
    }
  
    attached() {
      this.isAttached = true;
      this.applyPlugins();
    }
  
    detached() {
      if (this.dataObserver) {
        this.dataObserver.dispose();
      }
  
      for (let observer of this.filterObservers) {
        observer.dispose();
      }
    }
  
    filtersChanged() {
      if (Array.isArray(this.filters)) {
        for (let observer of this.filterObservers) {
          observer.dispose();
        }
  
        for (let filter of this.filters) {
          let observer = this.bindingEngine
            .propertyObserver(filter, "value")
            .subscribe(() => this.filterChanged());
          this.filterObservers.push(observer);
        }
      }
    }
  
    filterChanged() {
      if (this.dataSource === "server") {
        return;
      }
  
      if (this.hasPagination()) {
        this.currentPage = 1;
      }
      this.applyPlugins();
    }
  
    currentPageChanged() {
      this.applyPlugins();
    }
  
    pageSizeChanged() {
      this.applyPlugins();
    }
  
    /**
     * Copies the data into the display data
     */
    getDataCopy() {
      return [].concat(this.data);
    }
  
    getPageData(localData) {
      if (this.hasPagination()) {
        this.beforePagination = [].concat(localData);
        return this.doPaginate(localData);
      }
  
      return localData;
    }
  
    /**
     * Applies all the plugins to the display data
     */
    applyPlugins() {
      if (!this.isAttached || !this.data || this.dataSource === "server") {
        return;
      }
  
      let localData = this.getDataCopy();
  
      if (this.hasFilter()) {
        localData = this.doFilter(localData);
      }
  
      if ((this.sortKey || this.customSort) && this.sortOrder !== 0) {
        this.doSort(localData);
      }
  
      this.totalItems = localData.length;
  
      this.displayData = this.getPageData(localData);
    }
  
    doFilter(toFilter) {
      let filteredData = [];
  
      for (let item of toFilter) {
        let passed = true;
  
        for (let filter of this.filters) {
          if (!this.passFilter(item, filter)) {
            passed = false;
            break;
          }
        }
  
        if (passed) {
          filteredData.push(item);
        }
      }
  
      return filteredData;
    }
  
    passFilter(item, filter) {
      if (
        typeof filter.custom === "function" &&
        !filter.custom(filter.value, item)
      ) {
        return false;
      }
  
      if (
        filter.value === null ||
        filter.value === undefined ||
        filter.value === "" ||
        !Array.isArray(filter.keys)
      ) {
        return true;
      }
  
      for (let key of filter.keys) {
        let value = this.getPropertyValue(item, key);
        let strippedStringLeaveOnlyNumbers = "";
  
        if (value !== null && value !== undefined) {
          if (typeof value === "boolean") {
            return value.toString() === filter.value.toString();
          }
  
          // Is the directly supplied value a number?
          const isNumber = typeof value === "number";
  
          // Value supplied is not a number
          // perform a string replacement to leave only numbers
          if (!isNumber) {
            strippedStringLeaveOnlyNumbers = value.replace(/\D/g, "");
          }
  
          // If the stripped string has any numbers, this will evaluate to a number
          const parsedValueIsNumber = parseInt(strippedStringLeaveOnlyNumbers);
  
          // If the passed value is not a number
          // and the parsed number is also not a number
          // we can safely typecast to a string
          if (!isNumber && isNaN(parsedValueIsNumber)) {
            value = value.toString().toLowerCase();
          }
  
          // We are dealing with a numeric value
          if (
            isNumber ||
            (typeof parsedValueIsNumber === "number" && !isNaN(value))
          ) {
            if (!isNumber && typeof parsedValueIsNumber === "number") {
              value = parsedValueIsNumber;
            }
  
            return value == parseInt(filter.value);
          } else {
            if (value.indexOf(filter.value.toString().toLowerCase()) > -1) {
              return true;
            }
          }
        }
      }
  
      return false;
    }
  
    doSort(toSort) {
      toSort.sort((a, b) => {
        if (typeof this.customSort === "function") {
          return this.customSort(a, b, this.sortOrder);
        }
  
        let val1;
        let val2;
  
        if (typeof this.sortKey === "function") {
          val1 = this.sortKey(a, this.sortOrder);
          val2 = this.sortKey(b, this.sortOrder);
        } else {
          val1 = this.getPropertyValue(a, this.sortKey);
          val2 = this.getPropertyValue(b, this.sortKey);
        }
  
        if (val1 === null) {
          val1 = "";
        }
        if (val2 === null) {
          val2 = "";
        }
  
        if (this.isNumeric(val1) && this.isNumeric(val2)) {
          return (val1 - val2) * this.sortOrder;
        }
  
        let str1 = val1.toString();
        let str2 = val2.toString();
  
        return str1.localeCompare(str2) * this.sortOrder;
      });
    }
  
    /**
     * Retrieves the value in the object specified by the key path
     * @param object the object
     * @param keyPath the path
     * @returns {*} the value
     */
    getPropertyValue(object, keyPath) {
      keyPath = keyPath.replace(/\[(\w+)\]/g, ".$1"); // convert indexes to properties
      keyPath = keyPath.replace(/^\./, ""); // strip a leading dot
      let a = keyPath.split(".");
      for (let i = 0, n = a.length; i < n; ++i) {
        let k = a[i];
        if (k in object) {
          object = object[k];
        } else {
          return;
        }
      }
      return object;
    }
  
    isNumeric(toCheck) {
      return !isNaN(parseFloat(toCheck)) && isFinite(toCheck);
    }
  
    doPaginate(toPaginate) {
      if (toPaginate.length <= this.pageSize) {
        return toPaginate;
      }
  
      let start = (this.currentPage - 1) * this.pageSize;
  
      let end = start + this.pageSize;
  
      return toPaginate.slice(start, end);
    }
  
    hasFilter() {
      return Array.isArray(this.filters) && this.filters.length > 0;
    }
  
    hasPagination() {
      return this.currentPage > 0;
    }
  
    dataChanged() {
      if (this.dataObserver) {
        this.dataObserver.dispose();
      }
  
      this.dataObserver = this.bindingEngine
        .collectionObserver(this.data)
        .subscribe(() => this.applyPlugins());
  
      this.applyPlugins();
    }
  
    sortChanged(key, custom, order) {
      this.sortKey = key;
      this.customSort = custom;
      this.sortOrder = order;
      this.applyPlugins();
      this.emitSortChanged();
    }
  
    addSortChangedListener(callback) {
      this.sortChangedListeners.push(callback);
    }
  
    removeSortChangedListener(callback) {
      this.removeListener(callback, this.sortChangedListeners);
    }
  
    emitSortChanged() {
      for (let listener of this.sortChangedListeners) {
        listener();
      }
    }
  
    removeListener(callback, listeners) {
      let index = listeners.indexOf(callback);
  
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
  
    revealItem(item) {
      if (!this.hasPagination()) {
        return true;
      }
  
      let index = this.beforePagination.indexOf(item);
  
      if (index === -1) {
        return false;
      }
  
      this.currentPage = Math.ceil((index + 1) / this.pageSize);
  
      return true;
    }
  }
  