Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

skava / @skava/ui   js

Repository URL to install this package:

Version: 4.2.0-a11y.0 

/ src / state / SelectionState.tsx

/**
 * @todo find an optimized understandable way to mixin...
 * also split this file probably
 */
import { isArray, isFunction, EMPTY_OBJ, EMPTY_ARRAY } from 'exotic'
import { action, observable, decorate } from 'xmobx/mobx'
import { commonState } from './common'

// === actions ===
export interface SelectableItemAction {
  (item: SelectableItem): void
  isMobxAction?: boolean
}
export interface SelectableListAction {
  (list: SelectableList): void
  isMobxAction?: boolean
}

// === list ===
export type SelectableList = Array<SelectableItem>

export interface SelectableListProps {
  list: SelectableList
  // should be required... but in Select it is not
  // and we warn if it's not passed in
  onChange?: SelectableItemOnClick
  readonly ref?: any
}
export interface DecorateItem {
  (item: SelectableItem, index?: number): void
}
export interface SelectableState {
  // @todo <SelectableItem>...
  list: SelectableItem[]
  isVisible: boolean

  selectItem: SelectableItemAction
  unselectItem: SelectableItemAction
  unselectList: SelectableListAction
  decorateItem: DecorateItem
  handleToggleVisibility(): void
}

// === item ===
export interface SelectableItemOnClick {
  (event: Event, item: SelectableItem, state: SelectableState): void

  // added if it's an action
  isMobxAction?: boolean
  // added when we decorate, so we don't decorate 2x
  isDecorated?: boolean
}
// @todo CommonState for `.select` etc
export interface SelectableItem {
  isSelected?: boolean
  isDisabled?: boolean
  identifier?: string
  label?: string

  // this can definitely be anything
  value?: any

  onClick?: SelectableItemOnClick
}

/**
 * @todo belongs in deps...
 */
export const isSatisfiedByIsSelected = item => item.isSelected

/**
 * props | props.list | props.options | []
 */
export const toList = (props: SelectableListProps): SelectableList =>
  isArray(props)
    ? props
    : isArray(props.list)
      ? props.list
      : isArray(props.options)
        ? props.options
        : EMPTY_ARRAY
/**
 * optimizations...
 */
export const isDecorated = (item: SelectableItem): boolean =>
  isFunction(item.onClick) && item.onClick.isDecorated === true

export const isEveryItemInListDecoratedWithOnClick = (
  props: SelectableListProps
): boolean => toList(props).every(isDecorated)

/**
 * This way we can provide the selction state in a list
 */
class SelectionState implements SelectableState {
  @observable.ref
  list: SelectableList = []

  @observable
  isVisible = false

  props: SelectableListProps = EMPTY_OBJ

  /**
   * @todo @name toggleIsVisible
   */
  @action.bound
  handleToggleVisibility(): void {
    this.isVisible = !this.isVisible
  }
  @action.bound
  setIsVisible(isVisible: boolean): void {
    this.isVisible = isVisible
  }

  /**
   * This function selects the particular item
   */
  @action
  selectItem(item: SelectableItem): void {
    item.isSelected = true
  }
  /**
   * This function deSelects the item in the array list
   *
   * .bound because @see this.unselectList
   */
  @action.bound
  unselectItem(item: SelectableItem): void {
    console.info('[SelectionState] unselecting item: ', item.label)
    item.isSelected = false
  }

  /**
   * This function loops the items in the array list
   */
  @action
  unselectList(list: SelectableList): void {
    console.info('[SelectionState] unselecting list: ', list.length)

    list.forEach(this.unselectItem)
  }

  /**
   * here we set properties on each item so we can handle each click
   */
  @action.bound
  decorateItem(item: SelectableItem, index?: number): void {
    console.debug('[SelectionState] decorateItem: ' + item.label)

    if (isFunction(item.onClick)) {
      /**
       * we put .isDecorated on this so that we can call it multiple times
       * & not double d
       */
      if (item.onClick.isDecorated === true) {
        console.debug(
          `[SelectionState] item is already decorated: ${item.label}`
        )
      } else if (isFunction(this.props.onChange)) {
        /**
         * onChange - (select list level)
         * onClick - item level
         */
        console.warn(`[SelectionState] already had onClick ${item.label}`)
        console.info(
          `[SelectionState] - adding a wrapper since we passed in onChange`
        )

        // ref
        const itemOnClick = item.onClick

        // wrap
        const selectionStateItemOnClickWrappingExistingOnClick = (
          event: Event
        ): void => {
          console.info(
            '[SelectionState] calling onChange + itemOnClick: ',
            item.label
          )
          itemOnClick(event, item, this)
          this.props.onChange(event, item, this)
        }

        item.onClick = action(selectionStateItemOnClickWrappingExistingOnClick)
      } else {
        console.warn(
          `[SelectionState] already had onClick ${
            item.label
          } and no "onChange" was passed in`
        )
      }
    } else {
      const selectionStateItemOnClick = (event: Event): void => {
        console.info('[SelectionState] selecting item: ', item.label)

        this.unselectList(this.list)
        this.selectItem(item)

        if (isFunction(this.props.onChange)) {
          console.info('[SelectionState] calling onChange: ', item.label)
          // @todo there is no real event, need to finish standardizing
          this.props.onChange(event, item, this)
        }
      }

      // for enforced actions
      item.onClick = action(selectionStateItemOnClick)
    }

    // added either way
    item.onClick.isDecorated = true
  }

  get selectedItem() {
    // @todo Default Select Label || this.list[0]
    return this.list.find(isSatisfiedByIsSelected)
  }

  /**
   * was named unSelectedListWithSelectedItem
   */
  @action
  decorateWithProps(props: SelectableListProps): void {
    this.props = props
    // @note - compat for skreact
    this.list = toList(this.props)
    // @note - we can see this without logs using mobx devtools
    console.debug('[decorateWithProps]')
    console.dir(this.list)
    this.list.forEach(this.decorateItem)
  }

  // yagni
  // @action setList(list: SelectableList) { this.list = list }
  // @action setProps(props: SelectableProps) { this.props = props }
}

function toSelectionState(props: SelectableListProps) {
  const state = new SelectionState()
  state.decorateWithProps(props)
  return state
}

export { SelectionState, toSelectionState }
export default SelectionState