lively.components_widgets_mode-selector.cp.js

import { Label, TilingLayout, ViewModel, part, component } from 'lively.morphic';
import { connect, signal } from 'lively.bindings';
import { Color } from 'lively.graphics';
import { pt } from 'lively.graphics/geometry-2d.js';

const ModeSelectorLabelUnselected = component({
  type: Label,
  nativeCursor: 'pointer',
  name: 'mode selector label',
  fontWeight: 'bold',
  fill: Color.transparent,
  fontColor: Color.black,
  padding: 5,
  borderRadius: 3,
  textString: 'a mode selector label'
});

const ModeSelectorLabelSelected = component(ModeSelectorLabelUnselected, {
  fill: Color.black.withA(0.4),
  fontColor: Color.white
});

export const ModeSelectorLabel = component(ModeSelectorLabelUnselected, {
  master: {
    states: {
      selected: ModeSelectorLabelSelected
    }
  }
});

const ModeSelectorLabelUnselectedDark = component(ModeSelectorLabelUnselected, {
  fill: Color.transparent,
  fontColor: Color.white
});

const ModeSelectorLabelSelectedDark = component(ModeSelectorLabelUnselected, {
  fill: Color.white.withA(0.8),
  fontColor: Color.black
});

export const ModeSelectorLabelDark = component(ModeSelectorLabelUnselectedDark, {
  master: {
    states: {
      selected: ModeSelectorLabelSelectedDark
    }
  }
});

/**
 * Allows to switch between different items by clicking on them. The selected Item can also be changed by calling the exposed `select` function.
 * A change in the selected item is signalled with the `selectionChanged` signal providing the newly selected item.
 *
 * The items need to be provided as an array of objects. The keys `name` and `text` need to be present. Optionally, a `tooltip` property is accepted.
 *
 * If no other `selectedItem` is provided, the first element of `items` will be selected by default. This initial selection does not trigger the above mentioned signal.
 * In the selected item or the item to be selected is specified by the name of the item as a string. This is also what the `selectionChanged` signal will provide.
 * The `ModeSelector` can be deactivated, graying out its UI elements and not accepting mouse inputs any longer.
 */
class ModeSelectorModel extends ViewModel {
  /* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  EXAMPLE:
  const example = part(ModeSelector, { // eslint-disable-line no-use-before-define
     viewModel: {
       items: [
         { text: 'demo1', name: 'demo one', tooltip: 'demo one' },
         { text: 'demo2', name: 'demo two', tooltip: 'demo two' },
         { text: 'demo3', name: 'demo three', tooltip: 'demo three' },
         { text: 'demo4', name: 'demo four', tooltip: 'demo four' }
       ]
     }
   }).openInWorld();
   connect(example, 'selectionChanged', $world, 'setStatusMessage');
  -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
  static get properties () {
    return {
      expose: {
        get () {
          return ['select', 'items', 'enabled', 'selectedItem'];
        }
      },
      items: { },
      selectedItem: {},
      enabled: {
        defaultValue: true
      },
      labelMaster: {
        defaultValue: ModeSelectorLabel
      }
    };
  }

  viewDidLoad () {
    this.createLabels();
    this.select(this.selectedItem || this.items[0].name);
    if (!this.enabled) this.disable();
  }

  onRefresh (prop) {
    if (prop === 'enabled') {
      if (this.enabled) this.enable();
      else this.disable();
    }
    if (prop === 'selectedItem') {
      this.select(this.selectedItem);
    }
  }

  enable () {
    this.ui.labels.forEach(l => l.nativeCursor = 'pointer');
    this.view.opacity = 1;
  }

  disable () {
    this.ui.labels.forEach(l => l.nativeCursor = 'not-allowed');
    this.view.opacity = 0.5;
  }

  createLabels () {
    this.view.submorphs = this.items.map((item) => {
      const label = part(this.labelMaster, {
        textString: item.text,
        name: item.name,
        tooltip: item.tooltip
      });
      connect(label, 'onMouseDown', this, 'select', {
        updater: `function ($upd) {
          if (!viewModel.enabled) return;
          $upd(label.name, true, true);
        }`,
        varMapping: { label, viewModel: this }
      });
      return label;
    });
  }

  async select (itemName, withAnimation = false, withSignal = false) {
    // Selected Item Changed by Clicking on the View
    if (withAnimation) {
      this.ui.labels.forEach(l => {
        if (l.name !== itemName) {
          l.withAnimationDo(() => {
            l.master.setState(null);
          }, { duration: 200 });
        }
      });
      const labelToSelect = this.ui.labels.find((label) => label.name === itemName);
      await labelToSelect.withAnimationDo(() => {
        labelToSelect.master.setState('selected');
      }, { duration: 200 });
    } else {
      // Selected Item changed programmatically
      this.ui.labels.forEach(l => {
        if (l.name !== itemName) {
          l.master.setState(null);
        }
      });
      this.ui.labels.find((label) => label.name === itemName).master.setState('selected');
    }

    this.selectedItem = itemName;
    if (withSignal) signal(this.view, 'selectionChanged', this.selectedItem);
  }
}

const ModeSelector = component({
  defaultViewModel: ModeSelectorModel,
  name: 'mode selector',
  extent: pt(234.7, 36.7),
  nativeCursor: 'pointer',
  height: 30,
  fill: Color.transparent,
  layout: new TilingLayout({
    align: 'center',
    axisAlign: 'center',
    orderByIndex: true,
    spacing: 5
  }),
  submorphs: [
    part(ModeSelectorLabel, { name: 'mode1', textString: 'demo' }),
    part(ModeSelectorLabel, { name: 'mode2', textString: 'demo2' })
  ]
});

const ModeSelectorDark = component(ModeSelector, {
  name: 'mode selector/dark',
  viewModelClass: ModeSelectorModel,
  viewModel: {
    labelMaster: ModeSelectorLabelDark
  }
});

export { ModeSelector, ModeSelectorDark };