Gapminder/vizabi

View on GitHub
src/components/dialogs/find/find.js

Summary

Maintainability
C
1 day
Test Coverage
import * as utils from "base/utils";
import Dialog from "components/dialogs/_dialog";

import show from "./show";
import singlehandleslider from "components/brushslider/singlehandleslider/singlehandleslider";
import indicatorpicker from "components/indicatorpicker/indicatorpicker";

/*!
 * VIZABI FIND CONTROL
 * Reusable find dialog
 */

const Find = Dialog.extend("find", {

  init(config, parent) {
    this.name = "find";
    const _this = this;

    this.components = [{
      component: show,
      placeholder: ".vzb-dialog-panel-show",
      model: ["state", "locale"]
    }, {
      component: singlehandleslider,
      placeholder: ".vzb-dialog-bubbleopacity",
      model: ["state.marker", "locale"],
      arg: "opacitySelectDim"
    }];

    this.enableSelectShowSwitch = ((config.ui.dialogs.dialog || {}).find || {}).enableSelectShowSwitch || false;
    this.panelMode = ((config.ui.dialogs.dialog || {}).find || {}).panelMode || "find";
    this.enablePicker = ((config.ui.dialogs.dialog || {}).find || {}).enablePicker;
    if (this.enablePicker) {
      this.components.push({
        component: indicatorpicker,
        placeholder: ".vzb-find-filter-selector",
        model: ["state.time", "state.marker", "locale"]
      });
    }

    this.model_binds = {
      "change:state.marker.select": function(evt, path) {
        if (path.indexOf("select.labelOffset") !== -1) return;

        _this.items.order();
        _this.selectDataPoints();
        _this.showHideButtons();
      },
      "change:state.time.playing": function(evt) {
        if (!_this.model.state.time.playing) {
          _this.time = _this.model.state.time.value;

          _this.model.state.marker.getFrame(_this.time, (values, time) => {
            if (!values || (_this.time - time)) return;
            _this.redrawDataPoints(values);
          });
        }
      },
      "change:state.time.value": function(evt) {
        // hide changes if the dialog is not visible
        if (!_this.placeholderEl.classed("vzb-active") && !_this.placeholderEl.classed("vzb-sidebar")) return;

        _this.time = _this.model.state.time.value;

        _this.model.state.marker.getFrame(_this.time, values => {
          if (!values) return;
          _this.redrawDataPoints(values);
        });
      },
      "change:ui.dialogs.dialog.find.enableSelectShowSwitch": function(evt) {
        if (!_this._readyOnce) return;
        _this.enableSelectShowSwitch = ((config.ui.dialogs.dialog || {}).find || {}).enableSelectShowSwitch || false;
        _this.element.select(".vzb-dialog-title-switch .vzb-switch-slider").classed("vzb-hidden", !_this.enableSelectShowSwitch);
        _this.element.select(".vzb-dialog-title-switch").style("pointer-events", _this.enableSelectShowSwitch ? "auto" : "none");
      },
      "translate:locale": function() {
        if (!_this._readyOnce) return;
        _this.input_search.attr("placeholder", _this.translator("placeholder/search") + "...");
      }
    };

    this._super(config, parent);
  },

  /**
   * Grab the list div
   */
  readyOnce() {
    this._super();

    this.panelComps = { find: this, show: this.findChildByName("show") };

    this.titleSwitch = this.element.select(".vzb-dialog-title-switch input");
    this.panelsEl = this.contentEl.selectAll(".vzb-dialog-content");
    this.list = this.element.select(".vzb-find-list");
    this.input_search = this.element.select(".vzb-find-search");
    this.deselect_all = this.element.select(".vzb-find-deselect");
    this.opacity_nonselected = this.element.select(".vzb-dialog-bubbleopacity");

    this.element.select(".vzb-dialog-title-switch .vzb-switch-slider").classed("vzb-hidden", !this.enableSelectShowSwitch);
    this.element.select(".vzb-dialog-title-switch").style("pointer-events", this.enableSelectShowSwitch ? "auto" : "none");
    this.element.select(".vzb-find-filter-selector").classed("vzb-hidden", !this.enablePicker);
    this.element.select(".vzb-dialog-title").classed("vzb-title-two-rows", this.enablePicker);

    this.KEY = this.model.state.entities.getDimension();

    const _this = this;

    this.titleSwitch.on("change", () => {
      _this.panelMode = _this.titleSwitch.property("checked") ? "show" : "find";
      _this.panelsEl.classed("vzb-active", false);
      _this.contentEl.select(".vzb-dialog-panel-" + _this.panelMode).classed("vzb-active", true);
      _this.panelComps[_this.panelMode].showHideSearch();
      _this._buttonAdjust();
      _this.panelComps[_this.panelMode].showHideButtons();
    });
    this.titleSwitch.property("checked", this.panelMode !== "find");
    this.titleSwitch.dispatch("change");

    this.input_search.on("keyup", () => {
      const event = d3.event;
      if (event.keyCode == 13 && _this.input_search.node().value == "select all") {
        _this.input_search.node().value = "";
        //clear highlight so it doesn't get in the way when selecting an entity
        if (!utils.isTouchDevice()) _this.model.state.marker.clearHighlighted();
        _this.model.state.marker.selectAll();
        utils.defer(() => _this.panelComps[_this.panelMode].showHideSearch());
      }
    });

    this.input_search.on("input", () => {
      _this.panelComps[_this.panelMode].showHideSearch();
    });

    d3.select(this.input_search.node().parentNode)
      .on("reset", () => {
        utils.defer(() => _this.panelComps[_this.panelMode].showHideSearch());
      })
      .on("submit", () => {
        d3.event.preventDefault();
        return false;
      });

    this.deselect_all.on("click", () => {
      _this.deselectMarkers();
    });

    const closeButton = this.buttonsEl.select(".vzb-dialog-button[data-click='closeDialog']");
    closeButton.on("click.panel", () => _this.panelComps[_this.panelMode].closeClick());

    this.translator = this.model.locale.getTFunction();
    this.input_search.attr("placeholder", this.translator("placeholder/search") + "...");

    //make sure it refreshes when all is reloaded
    this.root.on("ready", () => {
      _this.ready();
    });

  },

  getPanelMode() {
    return this.panelMode;
  },

  _buttonAdjust() {
    this.buttonsEl.selectAll(".vzb-dialog-buttons > :not([data-dialogtype])").classed("vzb-hidden", true);
    this.buttonsEl.selectAll(`[data-panel=${this.panelMode}]`).classed("vzb-hidden", false);
  },

  open() {
    const _this = this;
    this._super();

    this.input_search.node().value = "";
    this.showHideSearch();

    this.time = this.model.state.time.value;

    this.model.state.marker.getFrame(this.time, values => {
      if (!values) return;
      _this.redrawDataPoints(values);
    });
  },

  /**
   * Build the list everytime it updates
   */
  //TODO: split update in render and update methods
  ready() {
    this._super();

    const _this = this;
    const KEYS = this.KEYS = utils.unique(this.model.state.marker._getAllDimensions({ exceptType: "time" }));

    this.importantHooks = _this.model.state.marker.getImportantHooks();

    this.time = this.model.state.time.value;
    this.model.state.marker.getFrame(this.time, values => {
      if (!values) return;

      const data = _this.model.state.marker.getKeys().map(d => {
        d.brokenData = false;
        d.name = _this.model.state.marker.getCompoundLabelText(d, values);
        return d;
      });

      //sort data alphabetically
      data.sort((a, b) => (a.name < b.name) ? -1 : 1);

      _this.list.html("");

      _this.items = _this.list.selectAll(".vzb-find-item")
        .data(data)
        .enter()
        .append("div")
        .attr("class", "vzb-find-item vzb-dialog-checkbox");

      _this.items.append("input")
        .attr("type", "checkbox")
        .attr("class", "vzb-find-item")
        .attr("id", (d, i) => "-find-" + i + "-" + _this._id)
        .on("change", d => {
          //clear highlight so it doesn't get in the way when selecting an entity
          if (!utils.isTouchDevice()) _this.model.state.marker.clearHighlighted();
          _this.model.state.marker.selectMarker(d);
          //return to highlighted state
          if (!utils.isTouchDevice() && !d.brokenData) _this.model.state.marker.highlightMarker(d);
        });

      _this.items.append("label")
        .attr("for", (d, i) => "-find-" + i + "-" + _this._id)
        .text(d => d.name)
        .on("mouseover", d => {
          if (!utils.isTouchDevice() && !d.brokenData) _this.model.state.marker.highlightMarker(d);
        })
        .on("mouseout", d => {
          if (!utils.isTouchDevice()) _this.model.state.marker.clearHighlighted();
        });

      _this.items.each(function(d) {
        d.nameIfEllipsis = this.offsetWidth < this.scrollWidth ? d.name : "";
      });

      utils.preventAncestorScrolling(_this.element.select(".vzb-dialog-scrollable"));

      _this.redrawDataPoints(values);
      _this.selectDataPoints();
      _this.showHideSearch();
      _this.showHideButtons();

    });
  },

  redrawDataPoints(values) {
    const _this = this;
    const KEYS = this.KEYS;

    _this.items.each(function(d) {
      const view = d3.select(this).select("label");
      d.brokenData = false;

      utils.forEach(_this.importantHooks, name => {
        if (_this.model.state.marker[name].use == "constant") return;
        const hook = values[name];
        if (!hook) return;
        const value = hook[utils.getKey(d, KEYS)];
        if (!value && value !== 0) {
          d.brokenData = true;
          return false;
        }
      });

      view
        .classed("vzb-find-item-brokendata", d.brokenData)
        .attr("title", d.nameIfEllipsis + (d.brokenData ? (d.nameIfEllipsis ? " | " : "") + _this.model.state.time.formatDate(_this.time) + ": " + _this.translator("hints/nodata") : ""));
    });
  },

  selectDataPoints() {
    const _this = this;
    const KEY = this.KEY;
    //    const selected = this.model.state.marker.getSelected(KEY);
    const selected = this.model.state.marker;
    this.items.selectAll("input")
    //      .property("checked", d => (selected.indexOf(d[KEY]) !== -1));
      .property("checked", function(d) {
        const isSelected = selected.isSelected(d);
        d3.select(this.parentNode).classed("vzb-checked", isSelected);
        return isSelected;
      });
    const lastCheckedNode = this.list.selectAll(".vzb-checked")
      .classed("vzb-separator", false)
      .lower()
      .nodes()[0];
    d3.select(lastCheckedNode).classed("vzb-separator", true);
    this.contentEl.node().scrollTop = 0;
  },

  showHideSearch() {
    if (this.getPanelMode() !== "find") return;

    let search = this.input_search.node().value || "";
    search = search.toLowerCase();

    this.list.selectAll(".vzb-find-item")
      .classed("vzb-hidden", d => {
        const lower = (d.name || "").toString().toLowerCase();
        return (lower.indexOf(search) === -1);
      });
  },

  showHideButtons() {
    if (this.getPanelMode() !== "find") return;

    const someSelected = !!this.model.state.marker.select.length;
    this.deselect_all.classed("vzb-hidden", !someSelected);
    this.opacity_nonselected.classed("vzb-hidden", !someSelected);
    if (someSelected) {
      this.findChildByName("singlehandleslider").trigger("resize");
    }
  },

  deselectMarkers() {
    this.model.state.marker.clearSelected();
  },

  transitionEnd(event) {
    this._super(event);

    if (!utils.isTouchDevice()) this.input_search.node().focus();
  },

  closeClick() {

  }

});

export default Find;