Home Reference Source Repository

src/es6/editor.es6

import {d3} from "nbpresent-deps";

import {PART} from "./parts";

import {RegionTree} from "./regiontree";
import {CellManager} from "./cells/notebook";

import {bbox} from "./d3.bbox";

class Editor{
  constructor(slide, selectedRegion) {
    this.slide = slide;
    this.selectedRegion = selectedRegion;
    this.regions = this.slide.select("regions");
    this.cellManager = new CellManager();

    // TODO: make these discrete to base unit
    this.x = d3.scale.linear();
    this.y = d3.scale.linear();


    this.initUI();
    this.sidebar = new RegionTree(this.slide, this.selectedRegion);

    this.initBehavior();

    this.update();

    this.slide.on("update", ()=> { this.killed || this.update(); });
  }

  destroy() {
    this.sidebar.destroy();
    this.$ui.transition()
      .style({opacity: 0})
      .remove();
    this.killed = true;
  }

  initBehavior(){
    this.bbox = bbox();
  }

  initUI(){
    this.$ui = d3.select("body")
      .append("div")
      .classed({nbpresent_editor: 1})
      .style({opacity: 0});

    this.$bg = this.$ui.append("div")
      .classed({slide_bg: 1});

    this.$svg = this.$bg.append("svg");

    this.$defs = this.$svg.append("defs");

    this.$ui.transition()
      .style({opacity: 1});
  }

  padding(){
    return 20;
  }

  aspectRatio(){
    // TODO: put in data
    return 16 / 9;
  }

  bbEnd(el, d){
    let $el = d3.select(el),
      scales = {
        x: this.x,
        y: this.y,
        width: this.x,
        height: this.y
      };

    let selected = this.selectedRegion.get();

    if(!selected){
      return;
    }

    this.slide.merge(["regions", selected.region, "attrs"],
      d3.entries(scales)
        .reduce((memo, attr) => {
          memo[attr.key] = attr.value.invert($el.attr(attr.key));
          return memo;
        }, {}));
  }

  hasContent(part){
    return (d) => (d.value.content || {}).part === part;
  }

  update(){
    let that = this,
      uibb = this.$ui.node().getBoundingClientRect(),
      width = uibb.width - (this.sidebar.width() + (2 * this.padding())),
      height = width / this.aspectRatio(),
      regions = d3.entries(this.regions.get()),
      {slide, region} = this.selectedRegion.get() || {},
      selected = this.selectedRegion.get();

    if(height > uibb.height + 2 * this.padding()){
      height = uibb.height - (2 * this.padding());
      width = height * this.aspectRatio();
    }

    this.x.range([0, width]);
    this.y.range([0, height]);
    let {x, y} = this;

    this.$bg.style({
      left: `${(((uibb.width + this.sidebar.width()) - width) / 2)}px`,
      top: `${(uibb.height - height) / 2}px`,
      width: `${width}px`,
      height: `${height}px`
    });

    this.$svg.attr({width, height});

    regions.sort((a, b) => {
      return (selected && selected.region == a.key) ? -1 :
        (selected && selected.region == b.key) ? 1 :
          (a.value.attrs.z || 0) - (b.value.attrs.z || 0)
    });

    let $region = this.$svg.selectAll(".region")
      .data(regions, (d) => d.key)
      .order();

    $region.exit().remove();

    let regionData = (region) => {
      return directions.map((dir) => {
        return {region, dir};
      })
    };

    $region.enter()
      .append("g")
      .classed({"region": 1})
      .call(($region) => {
        $region.append("rect")
          .classed({region_bg: 1})
          .each(function(d){
            that.bbox.infect(d3.select(this))
              .on("dragend", function(d){ that.bbEnd(this, d) })
              .on("resizeend", function(d){ that.bbEnd(this, d) });
          });
      })
      .on("mousedown", (d) => {
        this.selectedRegion.set({slide: this.slide.get("id"), region: d.key});
      });

    $region
      .classed({
        active: (d) => selected && (d.key == selected.region),
        content_source: this.hasContent(PART.source),
        content_outputs: this.hasContent(PART.outputs),
        content_widgets: this.hasContent(PART.widgets)
      })
    .select(".region_bg")
      .transition()
      .attr({
        width: (d) => x(d.value.attrs.width),
        height: (d) => y(d.value.attrs.height),
        x: (d) => x(d.value.attrs.x),
        y: (d) => y(d.value.attrs.y)
      });

    $region.filter(({value}) => !value.content)
      .select(".region_bg")
      .style({fill: null});


    $region.filter(({value}) => value.content)
      .each(function(d){
        let $region = d3.select(this);
        that.cellManager.thumbnail(d.value.content)
          .catch(function(err){
            console.warn("thumbnail error", err);
          })
          .then(function({uri, width, height}){
            let id = `${d.value.content.part}-${d.value.content.cell}`,
              bg = that.$defs.selectAll(`#${id}`).data([id]);

            bg.enter().append("pattern")
              .attr({
                patternUnits: "userSpaceOnUse",
                id
              })
            .append("image")
              .attr({
                x: 0,
                y: 0
              });

            bg.attr({width, height})
              .select("image")
              .attr({
                "xlink:href": uri,
                width,
                height
              });

            $region.select(".region_bg")
              .style({
                fill: `url(#${id})`
              });
          });
      });
  }
}

export {Editor};