<gh_stars>0
"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

exports.__esModule = true;
exports.registerRefreshPromise = registerRefreshPromise;
exports.Markmap = exports.loadCSS = exports.loadJS = void 0;

var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));

var d3 = _interopRequireWildcard(require("d3"));

var _d3Flextree = require("d3-flextree");

var _util = require("./util");

exports.loadJS = _util.loadJS;
exports.loadCSS = _util.loadCSS;

var _hook = require("./util/hook");

function linkWidth(nodeData) {
  const data = nodeData.data;
  return Math.max(6 - 2 * data.d, 1.5);
}

function adjustSpacing(tree, spacing) {
  (0, _util.walkTree)(tree, (d, next) => {
    d.ySizeInner = d.ySize - spacing;
    d.y += spacing;
    next();
  }, 'children');
}

function createViewHooks() {
  return {
    transformHtml: new _hook.Hook()
  };
}

const refreshPromises = [];

function registerRefreshPromise(promise) {
  refreshPromises.push(promise);
}

function onPromiseResolve(promises, callback) {
  let scheduled;

  const callbackSoon = () => {
    if (!scheduled) {
      scheduled = Promise.resolve().then(() => {
        scheduled = undefined;
        callback();
      }).catch(_util.noop);
    }
  };

  promises.forEach(promise => {
    promise.then(callbackSoon, _util.noop).then(() => {
      // No need to wait for the promise in the future
      const i = promises.indexOf(promise);
      if (i >= 0) promises.splice(i, 1);
    });
  });
}

class Markmap {
  constructor(svg, opts) {
    this.options = void 0;
    this.state = void 0;
    this.svg = void 0;
    this.styleNode = void 0;
    this.g = void 0;
    this.zoom = void 0;
    this.viewHooks = void 0;
    ['handleZoom', 'handleClick'].forEach(key => {
      this[key] = this[key].bind(this);
    });
    this.viewHooks = createViewHooks();
    this.svg = svg.datum ? svg : d3.select(svg);
    this.styleNode = this.svg.append('style');
    this.zoom = d3.zoom().on('zoom', this.handleZoom);
    this.options = (0, _extends2.default)({
      duration: 500,
      nodeFont: '300 16px/20px sans-serif',
      nodeMinHeight: 16,
      spacingVertical: 5,
      spacingHorizontal: 80,
      autoFit: false,
      fitRatio: 0.95,
      color: (colorFn => node => colorFn(node.p.i))(d3.scaleOrdinal(d3.schemeCategory10)),
      paddingX: 8
    }, opts);
    this.state = {
      id: this.options.id || (0, _util.getId)()
    };
    this.g = this.svg.append('g').attr('class', `${this.state.id}-g`);
    this.updateStyle();
    this.svg.call(this.zoom);
  }

  getStyleContent() {
    const {
      style,
      nodeFont
    } = this.options;
    const {
      id
    } = this.state;
    const extraStyle = typeof style === 'function' ? style(id) : '';
    const styleText = `\
.${id} a { color: #0097e6; }
.${id} a:hover { color: #00a8ff; }
.${id}-g > path { fill: none; }
.${id}-fo > div { display: inline-block; font: ${nodeFont}; white-space: nowrap; }
.${id}-fo code { padding: .2em .4em; font-size: calc(1em - 2px); color: #555; background-color: #f0f0f0; border-radius: 2px; }
.${id}-fo del { text-decoration: line-through; }
.${id}-fo em { font-style: italic; }
.${id}-fo strong { font-weight: bolder; }
.${id}-fo pre { margin: 0; }
.${id}-fo pre[class*=language-] { padding: 0; }
.${id}-g > g { cursor: pointer; }
${extraStyle}
`;
    return styleText;
  }

  updateStyle() {
    this.svg.attr('class', (0, _util.addClass)(this.svg.attr('class'), this.state.id));
    this.styleNode.text(this.getStyleContent());
  }

  handleZoom() {
    const {
      transform
    } = d3.event;
    this.g.attr('transform', transform);
  }

  handleClick(d) {
    var _data$p;

    const {
      data
    } = d;
    data.p = (0, _extends2.default)({}, data.p, {
      f: !((_data$p = data.p) == null ? void 0 : _data$p.f)
    });
    this.renderData(d.data);
  }

  initializeData(node) {
    let i = 0;
    const {
      nodeFont,
      color,
      nodeMinHeight
    } = this.options;
    const {
      id
    } = this.state;
    const container = document.createElement('div');
    const containerClass = `${id}-container`;
    container.className = (0, _util.addClass)(container.className, `${id}-fo`, containerClass);
    const style = document.createElement('style');
    style.textContent = `
${this.getStyleContent()}
.${containerClass} {
  position: absolute;
  width: 0;
  height: 0;
  top: -100px;
  left: -100px;
  overflow: hidden;
  font: ${nodeFont};
}
.${containerClass} > div {
  display: inline-block;
}
`;
    document.body.append(style, container);
    (0, _util.walkTree)(node, (item, next) => {
      var _item$c;

      item.c = (_item$c = item.c) == null ? void 0 : _item$c.map(child => (0, _extends2.default)({}, child));
      i += 1;
      const el = document.createElement('div');
      el.innerHTML = item.v;
      container.append(el);
      item.p = (0, _extends2.default)({}, item.p, {
        // unique ID
        i,
        el
      });
      color(item); // preload colors

      next();
    });
    const nodes = (0, _util.arrayFrom)(container.childNodes);
    this.viewHooks.transformHtml.call(this, nodes);
    (0, _util.walkTree)(node, (item, next, parent) => {
      var _parent$p;

      const rect = item.p.el.getBoundingClientRect();
      item.v = item.p.el.innerHTML;
      item.p.s = [Math.ceil(rect.width), Math.max(Math.ceil(rect.height), nodeMinHeight)]; // TODO keep keys for unchanged objects
      // unique key, should be based on content

      item.p.k = `${(parent == null ? void 0 : (_parent$p = parent.p) == null ? void 0 : _parent$p.i) || ''}.${item.p.i}:${item.v}`;
      next();
    });
    container.remove();
    style.remove();
  }

  setOptions(opts) {
    Object.assign(this.options, opts);
  }

  setData(data, opts) {
    if (!data) data = (0, _extends2.default)({}, this.state.data);
    this.state.data = data;
    this.initializeData(data);
    if (opts) this.setOptions(opts);
    this.renderData();
  }

  renderData(originData) {
    var _origin$data$p$x, _origin$data$p$y;

    if (!this.state.data) return;
    const {
      spacingHorizontal,
      paddingX,
      spacingVertical,
      autoFit,
      color
    } = this.options;
    const {
      id
    } = this.state;
    const layout = (0, _d3Flextree.flextree)().children(d => {
      var _d$p;

      return !((_d$p = d.p) == null ? void 0 : _d$p.f) && d.c;
    }).nodeSize(d => {
      const [width, height] = d.data.p.s;
      return [height, width + (width ? paddingX * 2 : 0) + spacingHorizontal];
    }).spacing((a, b) => {
      return a.parent === b.parent ? spacingVertical : spacingVertical * 2;
    });
    const tree = layout.hierarchy(this.state.data);
    layout(tree);
    adjustSpacing(tree, spacingHorizontal);
    const descendants = tree.descendants().reverse();
    const links = tree.links();
    const linkShape = d3.linkHorizontal();
    const minX = d3.min(descendants, d => d.x - d.xSize / 2);
    const maxX = d3.max(descendants, d => d.x + d.xSize / 2);
    const minY = d3.min(descendants, d => d.y);
    const maxY = d3.max(descendants, d => d.y + d.ySizeInner);
    Object.assign(this.state, {
      minX,
      maxX,
      minY,
      maxY
    });
    if (autoFit) this.fit();
    const origin = originData && descendants.find(item => item.data === originData) || tree;
    const x0 = (_origin$data$p$x = origin.data.p.x0) != null ? _origin$data$p$x : origin.x;
    const y0 = (_origin$data$p$y = origin.data.p.y0) != null ? _origin$data$p$y : origin.y; // Update the nodes

    const node = this.g.selectAll((0, _util.childSelector)('g')).data(descendants, d => d.data.p.k);
    const nodeEnter = node.enter().append('g').attr('transform', d => `translate(${y0 + origin.ySizeInner - d.ySizeInner},${x0 + origin.xSize / 2 - d.xSize})`).on('click', this.handleClick);
    const nodeExit = this.transition(node.exit());
    nodeExit.select('rect').attr('width', 0).attr('x', d => d.ySizeInner);
    nodeExit.select('foreignObject').style('opacity', 0);
    nodeExit.attr('transform', d => `translate(${origin.y + origin.ySizeInner - d.ySizeInner},${origin.x + origin.xSize / 2 - d.xSize})`).remove();
    const nodeMerge = node.merge(nodeEnter);
    this.transition(nodeMerge).attr('transform', d => `translate(${d.y},${d.x - d.xSize / 2})`);
    const rect = nodeMerge.selectAll((0, _util.childSelector)('rect')).data(d => [d], d => d.data.p.k).join(enter => {
      return enter.append('rect').attr('x', d => d.ySizeInner).attr('y', d => d.xSize - linkWidth(d) / 2).attr('width', 0).attr('height', linkWidth);
    }, update => update, exit => exit.remove());
    this.transition(rect).attr('x', -1).attr('width', d => d.ySizeInner + 2).attr('fill', d => color(d.data));
    const circle = nodeMerge.selectAll((0, _util.childSelector)('circle')).data(d => d.data.c ? [d] : [], d => d.data.p.k).join(enter => {
      return enter.append('circle').attr('stroke-width', '1.5').attr('cx', d => d.ySizeInner).attr('cy', d => d.xSize).attr('r', 0);
    }, update => update, exit => exit.remove());
    this.transition(circle).attr('r', 6).attr('stroke', d => color(d.data)).attr('fill', d => {
      var _d$data$p;

      return ((_d$data$p = d.data.p) == null ? void 0 : _d$data$p.f) && d.data.c ? color(d.data) : '#fff';
    });
    const foreignObject = nodeMerge.selectAll((0, _util.childSelector)('foreignObject')).data(d => [d], d => d.data.p.k).join(enter => {
      const fo = enter.append('foreignObject').attr('class', `${id}-fo`).attr('x', paddingX).attr('y', 0).style('opacity', 0).attr('height', d => d.xSize);
      fo.append('xhtml:div').select(function select(d) {
        const node = d.data.p.el.cloneNode(true);
        this.replaceWith(node);
        return node;
      }).attr('xmlns', 'http://www.w3.org/1999/xhtml');
      return fo;
    }, update => update, exit => exit.remove()).attr('width', d => Math.max(0, d.ySizeInner - paddingX * 2));
    this.transition(foreignObject).style('opacity', 1); // Update the links

    const path = this.g.selectAll((0, _util.childSelector)('path')).data(links, d => d.target.data.p.k).join(enter => {
      const source = [y0 + origin.ySizeInner, x0 + origin.xSize / 2];
      return enter.insert('path', 'g').attr('d', linkShape({
        source,
        target: source
      }));
    }, update => update, exit => {
      const source = [origin.y + origin.ySizeInner, origin.x + origin.xSize / 2];
      return this.transition(exit).attr('d', linkShape({
        source,
        target: source
      })).remove();
    });
    this.transition(path).attr('stroke', d => color(d.target.data)).attr('stroke-width', d => linkWidth(d.target)).attr('d', d => {
      const source = [d.source.y + d.source.ySizeInner, d.source.x + d.source.xSize / 2];
      const target = [d.target.y, d.target.x + d.target.xSize / 2];
      return linkShape({
        source,
        target
      });
    });
    descendants.forEach(d => {
      d.data.p.x0 = d.x;
      d.data.p.y0 = d.y;
    });
  }

  transition(sel) {
    const {
      duration
    } = this.options;
    return sel.transition().duration(duration);
  }

  fit() {
    const svgNode = this.svg.node();
    const {
      width: offsetWidth,
      height: offsetHeight
    } = svgNode.getBoundingClientRect();
    const {
      fitRatio
    } = this.options;
    const {
      minX,
      maxX,
      minY,
      maxY
    } = this.state;
    const naturalWidth = maxY - minY;
    const naturalHeight = maxX - minX;
    const scale = Math.min(offsetWidth / naturalWidth * fitRatio, offsetHeight / naturalHeight * fitRatio, 2);
    const initialZoom = d3.zoomIdentity.translate((offsetWidth - naturalWidth * scale) / 2 - minY * scale, (offsetHeight - naturalHeight * scale) / 2 - minX * scale).scale(scale);
    return this.transition(this.svg).call(this.zoom.transform, initialZoom).end().catch(_util.noop);
  }

  rescale(scale) {
    const svgNode = this.svg.node();
    const {
      width: offsetWidth,
      height: offsetHeight
    } = svgNode.getBoundingClientRect();
    const halfWidth = offsetWidth / 2;
    const halfHeight = offsetHeight / 2;
    const transform = d3.zoomTransform(svgNode);
    const newTransform = transform.translate((halfWidth - transform.x) * (1 - scale) / transform.k, (halfHeight - transform.y) * (1 - scale) / transform.k).scale(scale);
    return this.transition(this.svg).call(this.zoom.transform, newTransform).end().catch(_util.noop);
  }

  static create(svg, opts, data) {
    const mm = new Markmap(svg, opts);

    if (data) {
      mm.setData(data);
      mm.fit(); // always fit for the first render
    }

    onPromiseResolve(refreshPromises, () => {
      mm.setData();
    });
    return mm;
  }

}

exports.Markmap = Markmap;