class TfMultiCheckbox extends LegacyElementMixin()

in tensorboard/components/tf_dashboard_common/tf-multi-checkbox.ts [34:344]


class TfMultiCheckbox extends LegacyElementMixin(PolymerElement) {
  static readonly template = html`
    <style include="scrollbar-style"></style>
    <style include="run-color-style"></style>

    <paper-input
      id="names-regex"
      no-label-float=""
      label="Write a regex to filter runs"
      value="[[regex]]"
      on-bind-value-changed="_debouncedRegexChange"
    ></paper-input>
    <div id="outer-container" class="scrollbar">
      <template
        is="dom-repeat"
        items="[[namesMatchingRegex]]"
        on-dom-change="synchronizeColors"
      >
        <div class="name-row">
          <div
            class="icon-container checkbox-container vertical-align-container"
          >
            <paper-checkbox
              class="checkbox vertical-align-center"
              id$="checkbox-[[item]]"
              name="[[item]]"
              checked$="[[_isChecked(item, selectionState.*)]]"
              on-change="_checkboxChange"
            ></paper-checkbox>
          </div>
          <div
            class="icon-container isolator-container vertical-align-container"
          >
            <paper-icon-button
              icon="radio-button-unchecked"
              class="isolator vertical-align-center"
              on-tap="_isolateName"
              name="[[item]]"
            ></paper-icon-button>
          </div>
          <div class="item-label-container">
            <span>[[item]]</span>
          </div>
        </div>
      </template>
    </div>
    <style>
      paper-input {
        --paper-input-container-focus-color: var(--tb-orange-strong);
        --paper-input-container-input: {
          font-size: 14px;
        }
        --paper-input-container-label: {
          font-size: 14px;
        }
      }
      :host {
        display: flex;
        flex-direction: column;
        height: 100%;
        overflow: hidden;
      }
      #outer-container {
        contain: content;
        flex-grow: 1;
        flex-shrink: 1;
        overflow-x: hidden;
        overflow-y: auto;
        width: 100%;
        will-change: transform;
        word-wrap: break-word;
      }
      .name-row {
        contain: content;
        padding-top: 5px;
        padding-bottom: 5px;
        display: flex;
        flex-direction: row;
        font-size: 13px;
        word-break: break-all; /* makes wrapping of hyperparam strings better */
      }
      .icon-container {
        flex-grow: 0;
        flex-shrink: 0;
        padding-left: 2px;
      }
      .checkbox {
        padding-left: 2px;
        width: 18px;
        height: 18px;
      }
      .isolator {
        width: 18px;
        height: 18px;
        padding: 0px;
      }
      .isolator-container {
        padding-left: 6px;
        padding-right: 3px;
      }
      .checkbox-container {
        padding-left: 2px;
      }
      .item-label-container {
        padding-left: 5px;
        flex-grow: 1;
        flex-shrink: 1;
        width: 0px; /* hack to get the flex-grow to work properly */
      }
      .tooltip-value-container {
        display: flex;
        justify-content: center;
        flex-grow: 0;
        flex-shrink: 0;
        text-align: right;
        padding-left: 2px;
      }
      .vertical-align-container {
        display: flex;
        justify-content: center;
      }
      .vertical-align-container .vertical-align-center {
        align-self: center;
      }
      .vertical-align-container .vertical-align-top {
        align-self: start;
      }
    </style>
  `;

  @property({
    type: Array,
  })
  // All the values of the checkbox.
  names: string[] = [];

  @property({
    type: Object,
  })
  coloring: {getColor: (name: string) => string} = {
    getColor: () => '',
  };

  @property({
    type: String,
    notify: true,
  })
  // Regex for filtering the names.
  regex: string = '';

  @property({
    type: Array,
    computed: 'computeNamesMatchingRegex(names.*, _regex)',
  })
  // Runs that match the regex.
  namesMatchingRegex: string[];

  @property({
    // If a name is explicitly enabled by user gesture, True, if explicitly
    // disabled, False. If undefined, default value (enable for first k
    // names, disable after).
    type: Object,
    notify: true,
  })
  selectionState: Record<string, boolean> = {};

  @property({
    type: Array,
    notify: true,
    computed: 'computeOutSelected(namesMatchingRegex.*, selectionState.*)',
  })
  // Allows state to persist across regex filtering.
  outSelected: string[];

  @property({
    // When TB first loads, if it has k or fewer names, they are all enabled
    // by default. If there are more, then they are all disabled.
    type: Number,
  })
  maxNamesToEnableByDefault: number = 40;

  _debouncedRegexChangeImpl() {
    var debounced = _.debounce(
      (r) => {
        this.regex = r;
      },
      150,
      {leading: false}
    );
    return function () {
      var r = this.$$('#names-regex').value;
      if (r == '') {
        // If the user cleared the field, they may be done typing, so
        // update more quickly.
        this.async(() => {
          this.regex = r;
        }, 30);
      } else {
        debounced(r);
      }
    };
  }

  // Updating the regex can be slow, because it involves updating styles
  // on a large number of Polymer paper-checkboxes. We don't want to do
  // this while the user is typing, as it may make a bad, laggy UI.
  // So we debounce the updates that come from user typing.
  @property({type: Object})
  _debouncedRegexChange = this._debouncedRegexChangeImpl();

  @computed('regex')
  get _regex(): RegExp | null {
    var regexString = this.regex;
    try {
      return new RegExp(regexString);
    } catch (e) {
      return null;
    }
  }

  @observe('selectionState', 'names')
  _setIsolatorIcon() {
    var selectionMap = this.selectionState;
    var numChecked = _.filter(_.values(selectionMap)).length;
    var buttons = Array.prototype.slice.call(
      this.root.querySelectorAll('.isolator')
    );
    buttons.forEach(function (b) {
      if (numChecked === 1 && selectionMap[b.name]) {
        b.icon = 'radio-button-checked';
      } else {
        b.icon = 'radio-button-unchecked';
      }
    });
  }

  computeNamesMatchingRegex(__, ___) {
    const regex = this._regex;
    return regex ? this.names.filter((n) => regex.test(n)) : this.names;
  }

  computeOutSelected(__, ___) {
    var selectionState = this.selectionState;
    var num = this.maxNamesToEnableByDefault;
    var allEnabled = this.namesMatchingRegex.length <= num;
    return this.namesMatchingRegex.filter((n) => {
      return selectionState[n] == null ? allEnabled : selectionState[n];
    });
  }

  synchronizeColors(e) {
    this._setIsolatorIcon();
    const checkboxes = this.root.querySelectorAll('paper-checkbox');
    checkboxes.forEach((p: any) => {
      const color = this.coloring.getColor(p.name);
      p.updateStyles({
        '--paper-checkbox-checked-color': color,
        '--paper-checkbox-checked-ink-color': color,
        '--paper-checkbox-unchecked-color': color,
        '--paper-checkbox-unchecked-ink-color': color,
      });
    });
    const buttons = this.root.querySelectorAll('.isolator');
    buttons.forEach((p: any) => {
      const color = this.coloring.getColor(p.name);
      p.style['color'] = color;
    });
    // The updateStyles call fails silently if the browser doesn't have focus,
    // e.g. if TensorBoard was opened into a new tab that isn't visible.
    // So we wait for requestAnimationFrame.
    window.requestAnimationFrame(() => {
      this.updateStyles();
    });
  }

  _isolateName(e) {
    // If user clicks on the label for one name, enable it and disable all other
    // names.
    var name = (e.target as any).name;
    var selectionState = {};
    this.names.forEach(function (n) {
      selectionState[n] = n == name;
    });
    this.selectionState = selectionState;
  }

  _checkboxChange(e) {
    var target = e.target as any;
    const newSelectionState = _.clone(this.selectionState);
    newSelectionState[(target as any).name] = (target as any).checked;
    // n.b. notifyPath won't work because names may have periods.
    this.selectionState = newSelectionState;
  }

  _isChecked(item: string, outSelectedChange): boolean {
    return this.outSelected.indexOf(item) != -1;
  }

  toggleAll() {
    // If any are toggled on, we turn everything off. Or, if none are toggled
    // on, we turn everything on.
    const anyToggledOn = this.namesMatchingRegex.some((name) =>
      this.outSelected.includes(name)
    );
    const newSelectionState = {};
    this.names.forEach((n) => {
      newSelectionState[n] = !anyToggledOn;
    });
    this.selectionState = newSelectionState;
  }
}