<commit_before>/*
 @author Zakai Hamilton
 @component AppVisualize
 */

screens.app.visualize = function AppVisualize(me) {
    me.init = async function () {
        await me.ui.transform.attach(me);
    };
    me.ready = async function () {
        await me.ui.content.attach(me);
    };
    me.launch = function (args) {
        if (me.core.property.get(me.singleton, "ui.node.parent")) {
            me.core.property.set(me.singleton, "widget.window.close");
        }
        if (!args) {
            args = [""];
        }
        var json = me.json;
        var params = args[0];
        if (params && params.fullscreen) {
            json["widget.window.fullscreen"] = null;
        }
        var window = me.singleton = me.ui.element.create(json, "workspace", "self", params);
        if (window.var.terms) {
            window.var.terms.focus();
        }
        if (typeof args[0] === "string") {
            me.content.import(window, args[0], args[1]);
        }
        else {
            me.core.property.set(window, "ui.property.after", {
                "app.visualize.sortDown": null,
                "timeout": 250
            });
        }
        return window;
    };
    me.initOptions = {
        set: function (object) {
            var window = me.widget.window.get(object);
            var options = me.transform.options();
            me.ui.options.load(me, window, Object.assign({}, options.load));
            me.ui.options.toggleSet(me, null, Object.assign({}, options.toggle));
            me.ui.options.choiceSet(me, null, Object.assign({
                "fontSize": (object, value) => {
                    me.core.property.set(window.var.terms, "ui.style.fontSize", value);
                    me.core.property.notify(window, "update");
                }
            }, options.choice));
            me.core.property.set(window, "app", me);
        }
    };
    me.importData = function (object, text, title, options) {
        var window = me.widget.window.get(object);
        me.clear(window);
        me.core.property.set(window, "widget.window.name", title);
        var data = JSON.parse(text);
        for (var item of data) {
            var emLeft = parseInt(item.left);
            var emTop = parseInt(item.top);
            var pixelLeft = parseInt(me.ui.basic.emToPixels(window.var.terms, item.left));
            var pixelTop = parseInt(me.ui.basic.emToPixels(window.var.terms, item.top));
            me.createTerm(object, {
                "ui.attribute.pixelLeft": pixelLeft,
                "ui.attribute.pixelTop": pixelTop,
                "ui.attribute.emLeft": emLeft,
                "ui.attribute.emTop": emTop,
                "ui.style.left": emLeft + "em",
                "ui.style.top": emTop + "em",
                "ui.attribute.text": item.text
            });
        }
        me.reload(window);
    };
    me.exportData = function (object) {
        var window = me.widget.window.get(object);
        var elements = me.ui.node.childList(window.var.terms);
        var data = [];
        for (var element of elements) {
            var text = me.core.property.get(element, "ui.attribute.text");
            var left = me.core.property.get(element, "ui.attribute.emLeft");
            var top = me.core.property.get(element, "ui.attribute.emTop");
            data.push({ left, top, text });
        }
        if (!data.length) {
            return [];
        }
        return [JSON.stringify(data), {}];
    };
    me.term = async function (object, text) {
        var html = await me.transform.term(object, text);
        me.core.property.set(object, "ui.attribute.text", text);
        html += me.ui.html.item({
            tag: "i",
            classes: ["fas", "fa-minus-circle", "app-visualize-delete"],
            attributes: {
                onclick: "screens.app.visualize.remove(this)"
            }
        });
        me.core.property.set(object, "ui.basic.html", html);
    };
    me.remove = function (object) {
        me.ui.node.removeChild(object.parentNode.parentNode, object.parentNode);
    };
    me.update = function (object) {
        var window = me.widget.window.get(object);
        me.core.property.set(window.var.terms, "ui.style.fontSize", window.options.fontSize);
    };
    me.reload = function (object) {
        var window = me.widget.window.get(object);
        var elements = me.ui.node.childList(window.var.terms);
        for (var element of elements) {
            var text = me.core.property.get(element, "ui.attribute.text");
            me.core.property.set(element, "app.visualize.term", text);
        }
    };
    me.clear = function (object) {
        var window = me.widget.window.get(object);
        me.core.property.set(window, "widget.window.name", "");
        me.ui.node.empty(window.var.terms);
    };
    me.sortDown = function (object, order) {
        me.sort(object, order, "down");
        me.sort(object, order, "down");
    };
    me.sortUp = function (object, order) {
        me.sort(object, order, "up");
        me.sort(object, order, "up");
    };
    me.sort = function (object, order, direction) {
        var window = me.widget.window.get(object);
        var { width } = me.ui.rect.absoluteRegion(window.var.terms);
        var elements = me.ui.node.childList(window.var.terms);
        if (typeof order === "string") {
            var orderList = order.split("/");
            for (let orderItem of orderList) {
                elements = elements.filter(Boolean);
                elements.sort((a, b) => {
                    var aChild = a.firstElementChild;
                    var bChild = b.firstElementChild;
                    if (!aChild || !bChild) {
                        return 0;
                    }
                    var aAttribute = aChild.getAttribute("kab-" + orderItem);
                    var bAttribute = bChild.getAttribute("kab-" + orderItem);
                    if (!aAttribute || !bAttribute) {
                        return 0;
                    }
                    var aText = aAttribute.split("/")[0];
                    var bText = bAttribute.split("/")[0];
                    if (orderItem === "phase") {
                        let aIndex = me.widget.transform.phases[aText];
                        let bIndex = me.widget.transform.phases[bText];
                        if (typeof aIndex === "undefined") {
                            aIndex = -1;
                        }
                        if (typeof bIndex === "undefined") {
                            bIndex = -1;
                        }
                        return aIndex - bIndex;
                    }
                    else {
                        return aText.localeCompare(bText);
                    }
                });
                if (orderItem === "phase" || orderItem === "category") {
                    var previous = null;
                    var result = [];
                    elements.map(element => {
                        if (!element.firstElementChild.getAttribute) {
                            result.push(element);
                            return;
                        }
                        var text = element.firstElementChild.getAttribute("kab-" + orderItem);
                        if (previous !== null && text !== previous) {
                            result.push(null);
                        }
                        previous = text;
                        result.push(element);
                    });
                    elements = result;
                }
            }
        }
        if (direction === "up") {
            elements.reverse();
        }
        var spacePixels = parseInt(me.ui.basic.emToPixels(window.var.terms, 1));
        var left = spacePixels, top = spacePixels;
        var height = 0;
        elements.map(element => me.core.property.set(element, "ui.style.transition", ""));
        for (var element of elements) {
            if (!element) {
                left = spacePixels;
                top += height + (spacePixels * 2);
                continue;
            }
            var region = me.ui.rect.relativeRegion(element, window.var.terms);
            height = parseInt((region.height / spacePixels) + 1) * spacePixels;
            if (left + region.width >= width - spacePixels) {
                left = spacePixels;
                top += height + spacePixels;
            }
            me.moveElement(element, left, top);
            left += parseInt(region.width) + spacePixels;
            left = parseInt((left / spacePixels) + 1) * spacePixels;
        }
    };
    me.applyCurrentElement = function (object) {
        var window = me.widget.window.get(object);
        if (window.currentElement) {
            me.core.property.set(window.currentElement, "ui.class.mobile", false);
            window.currentElement = null;
            window.termInput = "";
        }
    };
    me.createTerm = function (object, properties) {
        var window = me.widget.window.get(object);
        var element = me.ui.element.create(Object.assign({
            "ui.basic.tag": "div",
            "ui.class.class": "app.visualize.term",
            "ui.style.transition": "none",
            "ui.move.extend": "this",
            "ui.move.enabled": true,
            "ui.move.method": "app.visualize.moveTerm",
            "ui.move.relative": true
        }, properties), window.var.terms);
        return element;
    };
    me.updateCurrentElement = async function (object, update) {
        var window = me.widget.window.get(object);
        var text = window.termInput;
        if (text) {
            text = text.trim();
        }
        if (!text) {
            if (window.currentElement) {
                me.core.property.set(window.currentElement, "ui.node.parent");
                window.currentElement = null;
            }
            return;
        }
        var element = window.currentElement;
        if (!element) {
            element = window.currentElement = me.createTerm(object, {
                "ui.class.mobile": true,
                "ui.touch.click": me.applyCurrentElement,
            });
        }
        if (update) {
            await me.core.property.set(element, "app.visualize.term", window.termInput);
        }
        var left = window.termPos.left, top = window.termPos.top;
        var region = me.ui.rect.absoluteRegion(element);
        left -= region.width / 2;
        top -= region.height / 2;
        me.moveElement(element, left, top);
    };
    me.keyPress = function (object, event) {
        var window = me.widget.window.get(object);
        var text = String.fromCharCode(event.charCode);
        event.stopPropagation();
        if (!window.termInput) {
            window.termInput = "";
        }
        if (event.charCode === 13) {
            text = " ";
        }
        if (!window.termInput && text === " ") {
            return;
        }
        window.termInput += text;
        if (window.updateTimer) {
            clearTimeout(window.updateTimer);
        }
        window.updateTimer = setTimeout(() => {
            me.updateCurrentElement(object, true);
        }, 250);
    };
    me.keyUp = function (object, event) {
        var window = me.widget.window.get(object);
        event.stopPropagation();
        if (!window.termInput) {
            window.termInput = "";
        }
        if (event.keyCode === 8) {
            window.termInput = window.termInput.slice(0, -1);
            me.updateCurrentElement(object, true);
        }
    };
    me.touchMove = function (object, event) {
        var window = me.widget.window.get(object);
        var widget = window.var.terms;
        var target_region = me.ui.rect.relativeRegion(widget);
        var left = event.clientX - target_region.left + widget.scrollLeft;
        var top = event.clientY - target_region.top + widget.scrollTop;
        window.termPos = {
            left,
            top
        };
        me.updateCurrentElement(object);
    };
    me.moveTerm = function (object, event) {
        var { left, top, target } = me.ui.move.info;
        var x = event.clientX - left;
        var y = event.clientY - top;
        var emSpace = parseInt(me.ui.basic.pixelsToEm(target.parentNode, 1));
        me.moveElement(target, x, y - (emSpace * 2));
    };
    me.moveElement = function (object, x, y) {
        var emX = parseInt(me.ui.basic.pixelsToEm(object.parentNode, x));
        var emY = parseInt(me.ui.basic.pixelsToEm(object.parentNode, y));
        me.core.property.set(object, "ui.attribute.pixelLeft", x);
        me.core.property.set(object, "ui.attribute.pixelTop", y);
        me.core.property.set(object, "ui.attribute.emLeft", emX);
        me.core.property.set(object, "ui.attribute.emTop", emY);
        me.core.property.set(object, "ui.style.left", emX + "em");
        me.core.property.set(object, "ui.style.top", emY + "em");
        me.core.property.set(object, "ui.style.opacity", "");
    };
};
<commit_msg>bug fix<commit_after>/*
 @author Zakai Hamilton
 @component AppVisualize
 */

screens.app.visualize = function AppVisualize(me) {
    me.init = async function () {
        await me.ui.transform.attach(me);
    };
    me.ready = async function () {
        await me.ui.content.attach(me);
    };
    me.launch = function (args) {
        if (me.core.property.get(me.singleton, "ui.node.parent")) {
            me.core.property.set(me.singleton, "widget.window.close");
        }
        if (!args) {
            args = [""];
        }
        var json = me.json;
        var params = args[0];
        if (params && params.fullscreen) {
            json["widget.window.fullscreen"] = null;
        }
        var window = me.singleton = me.ui.element.create(json, "workspace", "self", params);
        if (window.var.terms) {
            window.var.terms.focus();
        }
        if (typeof args[0] === "string") {
            me.content.import(window, args[0], args[1]);
        }
        else {
            me.core.property.set(window, "ui.property.after", {
                "app.visualize.sortDown": null,
                "timeout": 1000
            });
        }
        return window;
    };
    me.initOptions = {
        set: function (object) {
            var window = me.widget.window.get(object);
            var options = me.transform.options();
            me.ui.options.load(me, window, Object.assign({}, options.load));
            me.ui.options.toggleSet(me, null, Object.assign({}, options.toggle));
            me.ui.options.choiceSet(me, null, Object.assign({
                "fontSize": (object, value) => {
                    me.core.property.set(window.var.terms, "ui.style.fontSize", value);
                    me.core.property.notify(window, "update");
                }
            }, options.choice));
            me.core.property.set(window, "app", me);
        }
    };
    me.importData = function (object, text, title, options) {
        var window = me.widget.window.get(object);
        me.clear(window);
        me.core.property.set(window, "widget.window.name", title);
        var data = JSON.parse(text);
        for (var item of data) {
            var emLeft = parseInt(item.left);
            var emTop = parseInt(item.top);
            var pixelLeft = parseInt(me.ui.basic.emToPixels(window.var.terms, item.left));
            var pixelTop = parseInt(me.ui.basic.emToPixels(window.var.terms, item.top));
            me.createTerm(object, {
                "ui.attribute.pixelLeft": pixelLeft,
                "ui.attribute.pixelTop": pixelTop,
                "ui.attribute.emLeft": emLeft,
                "ui.attribute.emTop": emTop,
                "ui.style.left": emLeft + "em",
                "ui.style.top": emTop + "em",
                "ui.attribute.text": item.text
            });
        }
        me.reload(window);
    };
    me.exportData = function (object) {
        var window = me.widget.window.get(object);
        var elements = me.ui.node.childList(window.var.terms);
        var data = [];
        for (var element of elements) {
            var text = me.core.property.get(element, "ui.attribute.text");
            var left = me.core.property.get(element, "ui.attribute.emLeft");
            var top = me.core.property.get(element, "ui.attribute.emTop");
            data.push({ left, top, text });
        }
        if (!data.length) {
            return [];
        }
        return [JSON.stringify(data), {}];
    };
    me.term = async function (object, text) {
        var html = await me.transform.term(object, text);
        me.core.property.set(object, "ui.attribute.text", text);
        html += me.ui.html.item({
            tag: "i",
            classes: ["fas", "fa-minus-circle", "app-visualize-delete"],
            attributes: {
                onclick: "screens.app.visualize.remove(this)"
            }
        });
        me.core.property.set(object, "ui.basic.html", html);
    };
    me.remove = function (object) {
        me.ui.node.removeChild(object.parentNode.parentNode, object.parentNode);
    };
    me.update = function (object) {
        var window = me.widget.window.get(object);
        me.core.property.set(window.var.terms, "ui.style.fontSize", window.options.fontSize);
    };
    me.reload = function (object) {
        var window = me.widget.window.get(object);
        var elements = me.ui.node.childList(window.var.terms);
        for (var element of elements) {
            var text = me.core.property.get(element, "ui.attribute.text");
            me.core.property.set(element, "app.visualize.term", text);
        }
    };
    me.clear = function (object) {
        var window = me.widget.window.get(object);
        me.core.property.set(window, "widget.window.name", "");
        me.ui.node.empty(window.var.terms);
    };
    me.sortDown = function (object, order) {
        me.sort(object, order, "down");
        me.sort(object, order, "down");
    };
    me.sortUp = function (object, order) {
        me.sort(object, order, "up");
        me.sort(object, order, "up");
    };
    me.sort = function (object, order, direction) {
        var window = me.widget.window.get(object);
        var { width } = me.ui.rect.absoluteRegion(window.var.terms);
        var elements = me.ui.node.childList(window.var.terms);
        if (typeof order === "string") {
            var orderList = order.split("/");
            for (let orderItem of orderList) {
                elements = elements.filter(Boolean);
                elements.sort((a, b) => {
                    var aChild = a.firstElementChild;
                    var bChild = b.firstElementChild;
                    if (!aChild || !bChild) {
                        return 0;
                    }
                    var aAttribute = aChild.getAttribute("kab-" + orderItem);
                    var bAttribute = bChild.getAttribute("kab-" + orderItem);
                    if (!aAttribute || !bAttribute) {
                        return 0;
                    }
                    var aText = aAttribute.split("/")[0];
                    var bText = bAttribute.split("/")[0];
                    if (orderItem === "phase") {
                        let aIndex = me.widget.transform.phases[aText];
                        let bIndex = me.widget.transform.phases[bText];
                        if (typeof aIndex === "undefined") {
                            aIndex = -1;
                        }
                        if (typeof bIndex === "undefined") {
                            bIndex = -1;
                        }
                        return aIndex - bIndex;
                    }
                    else {
                        return aText.localeCompare(bText);
                    }
                });
                if (orderItem === "phase" || orderItem === "category") {
                    var previous = null;
                    var result = [];
                    elements.map(element => {
                        if (!element.firstElementChild.getAttribute) {
                            result.push(element);
                            return;
                        }
                        var text = element.firstElementChild.getAttribute("kab-" + orderItem);
                        if (previous !== null && text !== previous) {
                            result.push(null);
                        }
                        previous = text;
                        result.push(element);
                    });
                    elements = result;
                }
            }
        }
        if (direction === "up") {
            elements.reverse();
        }
        var spacePixels = parseInt(me.ui.basic.emToPixels(window.var.terms, 1));
        var left = spacePixels, top = spacePixels;
        var height = 0;
        elements.map(element => me.core.property.set(element, "ui.style.transition", ""));
        for (var element of elements) {
            if (!element) {
                left = spacePixels;
                top += height + (spacePixels * 2);
                continue;
            }
            var region = me.ui.rect.relativeRegion(element, window.var.terms);
            height = parseInt((region.height / spacePixels) + 1) * spacePixels;
            if (left + region.width >= width - spacePixels) {
                left = spacePixels;
                top += height + spacePixels;
            }
            me.moveElement(element, left, top);
            left += parseInt(region.width) + spacePixels;
            left = parseInt((left / spacePixels) + 1) * spacePixels;
        }
    };
    me.applyCurrentElement = function (object) {
        var window = me.widget.window.get(object);
        if (window.currentElement) {
            me.core.property.set(window.currentElement, "ui.class.mobile", false);
            window.currentElement = null;
            window.termInput = "";
        }
    };
    me.createTerm = function (object, properties) {
        var window = me.widget.window.get(object);
        var element = me.ui.element.create(Object.assign({
            "ui.basic.tag": "div",
            "ui.class.class": "app.visualize.term",
            "ui.style.transition": "none",
            "ui.move.extend": "this",
            "ui.move.enabled": true,
            "ui.move.method": "app.visualize.moveTerm",
            "ui.move.relative": true
        }, properties), window.var.terms);
        return element;
    };
    me.updateCurrentElement = async function (object, update) {
        var window = me.widget.window.get(object);
        var text = window.termInput;
        if (text) {
            text = text.trim();
        }
        if (!text) {
            if (window.currentElement) {
                me.core.property.set(window.currentElement, "ui.node.parent");
                window.currentElement = null;
            }
            return;
        }
        var element = window.currentElement;
        if (!element) {
            element = window.currentElement = me.createTerm(object, {
                "ui.class.mobile": true,
                "ui.touch.click": me.applyCurrentElement,
            });
        }
        if (update) {
            await me.core.property.set(element, "app.visualize.term", window.termInput);
        }
        var left = window.termPos.left, top = window.termPos.top;
        var region = me.ui.rect.absoluteRegion(element);
        left -= region.width / 2;
        top -= region.height / 2;
        me.moveElement(element, left, top);
    };
    me.keyPress = function (object, event) {
        var window = me.widget.window.get(object);
        var text = String.fromCharCode(event.charCode);
        event.stopPropagation();
        if (!window.termInput) {
            window.termInput = "";
        }
        if (event.charCode === 13) {
            text = " ";
        }
        if (!window.termInput && text === " ") {
            return;
        }
        window.termInput += text;
        if (window.updateTimer) {
            clearTimeout(window.updateTimer);
        }
        window.updateTimer = setTimeout(() => {
            me.updateCurrentElement(object, true);
        }, 250);
    };
    me.keyUp = function (object, event) {
        var window = me.widget.window.get(object);
        event.stopPropagation();
        if (!window.termInput) {
            window.termInput = "";
        }
        if (event.keyCode === 8) {
            window.termInput = window.termInput.slice(0, -1);
            me.updateCurrentElement(object, true);
        }
    };
    me.touchMove = function (object, event) {
        var window = me.widget.window.get(object);
        var widget = window.var.terms;
        var target_region = me.ui.rect.relativeRegion(widget);
        var left = event.clientX - target_region.left + widget.scrollLeft;
        var top = event.clientY - target_region.top + widget.scrollTop;
        window.termPos = {
            left,
            top
        };
        me.updateCurrentElement(object);
    };
    me.moveTerm = function (object, event) {
        var { left, top, target } = me.ui.move.info;
        var x = event.clientX - left;
        var y = event.clientY - top;
        var emSpace = parseInt(me.ui.basic.pixelsToEm(target.parentNode, 1));
        me.moveElement(target, x, y - (emSpace * 2));
    };
    me.moveElement = function (object, x, y) {
        var emX = parseInt(me.ui.basic.pixelsToEm(object.parentNode, x));
        var emY = parseInt(me.ui.basic.pixelsToEm(object.parentNode, y));
        me.core.property.set(object, "ui.attribute.pixelLeft", x);
        me.core.property.set(object, "ui.attribute.pixelTop", y);
        me.core.property.set(object, "ui.attribute.emLeft", emX);
        me.core.property.set(object, "ui.attribute.emTop", emY);
        me.core.property.set(object, "ui.style.left", emX + "em");
        me.core.property.set(object, "ui.style.top", emY + "em");
        me.core.property.set(object, "ui.style.opacity", "");
    };
};
<|endoftext|>


<commit_before>const fs = require('fs');
const { promisify } = require('util');
const path = require('path');
const parse5 = require('parse5');
const nwmatcher = require('nwmatcher');
const css = require('css');
const prettyBytes = require('pretty-bytes');

const readFile = promisify(fs.readFile);

const treeAdapter = parse5.treeAdapters.htmlparser2;

const PLUGIN_NAME = 'critters-webpack-plugin';

const PARSE5_OPTS = {
  treeAdapter
};

/** Critters: Webpack Plugin Edition!
 *  @class
 *  @param {Object} options
 *  @param {Boolean} [options.external=true]  Fetch and inline critical styles from external stylesheets
 *  @param {Boolean} [options.async=false]    Convert critical-inlined external stylesheets to load asynchronously (via link rel="preload")
 *  @param {Boolean} [options.compress=true]  Compress resulting critical CSS
 */
module.exports = class CrittersWebpackPlugin {
  constructor(options) {
    this.options = options || {};
  }

  /** Invoked by Webpack during plugin initialization */
  apply(compiler) {
    const outputPath = compiler.options.output.path;

    // hook into the compiler to get a Compilation instance...
    compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
      // ... which is how we get an "after" hook into html-webpack-plugin's HTML generation.
      compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(PLUGIN_NAME, (htmlPluginData, callback) => {
        // Parse the generated HTML in a DOM we can mutate
        const document = parse5.parse(htmlPluginData.html, PARSE5_OPTS);
        makeDomInteractive(document);

        let externalStylesProcessed = Promise.resolve();

        // `external:false` skips processing of external sheets
        if (this.options.external!==false) {
          const externalSheets = document.querySelectorAll('link[rel="stylesheet"]');
          externalStylesProcessed = Promise.all(externalSheets.map(
            link => this.embedLinkedStylesheet(link, compilation, outputPath)
          ));
        }

        externalStylesProcessed
          .then(() => {
            // go through all the style tags in the document and reduce them to only critical CSS
            const styles = document.querySelectorAll('style');
            return Promise.all(styles.map(style => this.processStyle(style, document)));
          })
          .then(() => {
            // serialize the document back to HTML and we're done
            const html = parse5.serialize(document, PARSE5_OPTS);
            callback(null, { html });
          })
          .catch(callback);
      });
    });
  }

  /** Inline the target stylesheet referred to by a <link rel="stylesheet"> (assuming it passes `options.filter`) */
  embedLinkedStylesheet(link, compilation, outputPath) {
    const href = link.getAttribute('href');
    const document = link.ownerDocument;

    // skip filtered resources, or network resources if no filter is provided
    if (this.urlFilter ? this.urlFilter(href) : href.match(/^(https?:)?\/\//)) return Promise.resolve();

    // path on disk
    const filename = path.resolve(outputPath, href.replace(/^\//, ''));

    // try to find a matching asset by filename in webpack's output (not yet written to disk)
    const asset = compilation.assets[path.relative(outputPath, filename).replace(/^\.\//, '')];

    // wait for a disk read if we had to go to disk
    const promise = asset ? Promise.resolve(asset.source()) : readFile(filename, 'utf8');
    return promise.then(sheet => {
    // the reduced critical CSS gets injected into a new <style> tag
      const style = document.createElement('style');
      style.appendChild(document.createTextNode(sheet));
      link.parentNode.insertBefore(style, link.nextSibling);

      // drop a reference to the original URL onto the tag (used for reporting to console later)
      style.$$name = href;

      // the `async` option changes any critical'd <link rel="stylesheet"> tags to async-loaded equivalents
      if (this.options.async) {
        link.setAttribute('rel', 'preload');
        link.setAttribute('as', 'style');
        link.setAttribute('onload', "this.rel='stylesheet'");
      }
    });
  }

  /** Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document. */
  processStyle(style) {
    const done = Promise.resolve();
    const document = style.ownerDocument;

    // basically `.textContent`
    let sheet = style.childNodes.length>0 && style.childNodes.map( node => node.nodeValue ).join('\n');

    // store a reference to the previous serialized stylesheet for reporting stats
    const before = sheet;

    // Skip empty stylesheets
    if (!sheet) return done;

    const ast = css.parse(sheet);

    // Walk all CSS rules, transforming unused rules to comments (which get removed)
    visit(ast, rule => {
      if (rule.type==='rule') {
        // Filter the selector list down to only those matche
        rule.selectors = rule.selectors.filter(sel => {
          // Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
          // This means any selector for a pseudo-element or having a pseudo-class will be inlined if the rest of the selector matches.
          sel = sel.replace(/::?(?:[a-z-]+)([.[#~&^:*]|\s|\n|$)/gi, '$1');
          return document.querySelector(sel, document) != null;
        });
        // If there are no matched selectors, replace the rule with a comment.
        if (rule.selectors.length===0) {
          rule.type = 'comment';
          rule.comment = '';
          delete rule.selectors;
          delete rule.declarations;
        }
      }

      if (rule.rules) {
        // Filter out comments
        rule.rules = rule.rules.filter(notComment);
        // If there are no remaining rules, replace the parent rule with a comment.
        if (rule.rules.length===0) {
          rule.type = 'comment';
          rule.comment = '';
          delete rule.rules;
        }
      }
    });

    sheet = css.stringify(ast, { compress: this.options.compress!==false });

    return done.then(() => {
      // If all rules were removed, get rid of the style element entirely
      if (sheet.trim().length===0) {
        sheet.parentNode.removeChild(sheet);
      }
      else {
        // replace the inline stylesheet with its critical'd counterpart
        while (style.lastChild) {
          style.removeChild(style.lastChild);
        }
        style.appendChild(document.createTextNode(sheet));
      }

      // output some stats
      const name = style.$$name ? style.$$name.replace(/^\//, '') : 'inline CSS';
      const percent = (before.length - sheet.length) / before.length * 100 | 0;
      console.log('\u001b[32mCritters: inlined ' + prettyBytes(sheet.length) + ' (' + percent + '% of original ' + prettyBytes(before.length) + ') of ' + name + '.\u001b[39m');
    });
  }
};

/** Recursively walk all rules in a stylesheet.
 *  The iterator can explicitly return `false` to remove the current node.
 */
function visit(node, fn) {
  if (node.stylesheet) return visit(node.stylesheet, fn);

  node.rules = node.rules.filter(rule => {
    if (rule.rules) {
      visit(rule, fn);
    }
    return fn(rule)!==false;
  });
}

/** Enhance an htmlparser2-style DOM with basic manipulation methods. */
function makeDomInteractive(document) {
  defineProperties(document, DocumentExtensions);
  // Find the first <html> element within the document
  document.documentElement = document.childNodes.filter( child => String(child.tagName).toLowerCase()==='html' )[0];

  // Extend Element.prototype with DOM manipulation methods.
  //   Note: document.$$scratchElement is also used by createTextNode()
  const scratch = document.$$scratchElement = document.createElement('div');
  const elementProto = Object.getPrototypeOf(scratch);
  defineProperties(elementProto, ElementExtensions);
  elementProto.ownerDocument = document;

  // nwmatcher is a selector engine that happens to work with Parse5's htmlparser2 DOM (they form the base of jsdom).
  // It is exposed to the document so that it can be used within Element.prototype methods.
  document.$match = nwmatcher({ document });
  document.$match.configure({
    CACHING: false,
    USE_QSAPI: false,
    USE_HTML5: false
  });
}

/** Essentially Object.defineProperties() except any functions are assigned as values rather than descriptors. */
function defineProperties(obj, properties) {
  for (const i in properties) {
    const value = properties[i];
    Object.defineProperty(obj, i, typeof value === 'function' ? { value } : value);
  }
}

/** {document,Element}.getElementsByTagName() is the only traversal method required by nwmatcher. */
function getElementsByTagName(tagName) {
  const stack = [this];
  const matches = [];
  const isWildCard = tagName === '*';
  const tagNameUpper = tagName.toUpperCase();
  while (stack.length !== 0) {
    const el = stack.pop();
    let child = el.lastChild;
    while (child) {
      if (child.nodeType === 1) stack.push(child);
      child = child.previousSibling;
    }
    if (isWildCard || (el.tagName != null && (el.tagName === tagNameUpper || el.tagName.toUpperCase() === tagNameUpper))) {
      matches.push(el);
    }
  }
  return matches;
}

/** Methods and descriptors to mix into Element.prototype */
const ElementExtensions = {
  nodeName: {
    get() {
      return this.tagName;
    }
  },
  insertBefore(child, referenceNode) {
    if (!referenceNode) return this.appendChild(child);
    treeAdapter.insertBefore(this, child, referenceNode);
    return child;
  },
  appendChild(child) {
    treeAdapter.appendChild(this, child);
    return child;
  },
  removeChild(child) {
    treeAdapter.detachNode(child);
  },
  setAttribute(name, value) {
    if (this.attribs == null) this.attribs = {};
    if (value == null) value = '';
    this.attribs[name] = value;
  },
  removeAttribute(name) {
    if (this.attribs != null) {
      delete this.attribs[name];
    }
  },
  getAttribute(name) {
    return this.attribs != null && this.attribs[name];
  },
  hasAttribute(name) {
    return this.attribs != null && this.attribs[name] != null;
  },
  getAttributeNode(name) {
    const value = this.getAttribute(name);
    if (value != null) return { specified: true, value };
  },
  getElementsByTagName
};

/** Methods and descriptors to mix into the global document instance */
const DocumentExtensions = {
  // document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
  // nwmatcher requires that it at least report a correct nodeType of DOCUMENT_NODE.
  nodeType: {
    get() {
      return 11;
    }
  },
  createElement(name) {
    return treeAdapter.createElement(name, null, []);
  },
  createTextNode(text) {
    // there is no dedicated createTextNode equivalent in htmlparser2's DOM, so
    // we have to insert Text and then remove and return the resulting Text node.
    const scratch = this.$$scratchElement;
    treeAdapter.insertText(scratch, text);
    const node = scratch.lastChild;
    treeAdapter.detachNode(node);
    return node;
  },
  querySelector(sel) {
    return this.$match.first(sel);
  },
  querySelectorAll(sel) {
    return this.$match.select(sel);
  },
  getElementsByTagName,
  // nwmatcher uses inexistence of `document.addEventListener` to detect IE:
  // https://github.com/dperini/nwmatcher/blob/3edb471e12ce7f7d46dc1606c7f659ff45675a29/src/nwmatcher.js#L353
  addEventListener: Object
};<commit_msg>Instead of replacing empty rules with comments, remove them.<commit_after>const fs = require('fs');
const { promisify } = require('util');
const path = require('path');
const parse5 = require('parse5');
const nwmatcher = require('nwmatcher');
const css = require('css');
const prettyBytes = require('pretty-bytes');

const readFile = promisify(fs.readFile);

const treeAdapter = parse5.treeAdapters.htmlparser2;

const PLUGIN_NAME = 'critters-webpack-plugin';

const PARSE5_OPTS = {
  treeAdapter
};

/** Critters: Webpack Plugin Edition!
 *  @class
 *  @param {Object} options
 *  @param {Boolean} [options.external=true]  Fetch and inline critical styles from external stylesheets
 *  @param {Boolean} [options.async=false]    Convert critical-inlined external stylesheets to load asynchronously (via link rel="preload")
 *  @param {Boolean} [options.compress=true]  Compress resulting critical CSS
 */
module.exports = class CrittersWebpackPlugin {
  constructor(options) {
    this.options = options || {};
  }

  /** Invoked by Webpack during plugin initialization */
  apply(compiler) {
    const outputPath = compiler.options.output.path;

    // hook into the compiler to get a Compilation instance...
    compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
      // ... which is how we get an "after" hook into html-webpack-plugin's HTML generation.
      compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(PLUGIN_NAME, (htmlPluginData, callback) => {
        // Parse the generated HTML in a DOM we can mutate
        const document = parse5.parse(htmlPluginData.html, PARSE5_OPTS);
        makeDomInteractive(document);

        let externalStylesProcessed = Promise.resolve();

        // `external:false` skips processing of external sheets
        if (this.options.external!==false) {
          const externalSheets = document.querySelectorAll('link[rel="stylesheet"]');
          externalStylesProcessed = Promise.all(externalSheets.map(
            link => this.embedLinkedStylesheet(link, compilation, outputPath)
          ));
        }

        externalStylesProcessed
          .then(() => {
            // go through all the style tags in the document and reduce them to only critical CSS
            const styles = document.querySelectorAll('style');
            return Promise.all(styles.map(style => this.processStyle(style, document)));
          })
          .then(() => {
            // serialize the document back to HTML and we're done
            const html = parse5.serialize(document, PARSE5_OPTS);
            callback(null, { html });
          })
          .catch(callback);
      });
    });
  }

  /** Inline the target stylesheet referred to by a <link rel="stylesheet"> (assuming it passes `options.filter`) */
  embedLinkedStylesheet(link, compilation, outputPath) {
    const href = link.getAttribute('href');
    const document = link.ownerDocument;

    // skip filtered resources, or network resources if no filter is provided
    if (this.urlFilter ? this.urlFilter(href) : href.match(/^(https?:)?\/\//)) return Promise.resolve();

    // path on disk
    const filename = path.resolve(outputPath, href.replace(/^\//, ''));

    // try to find a matching asset by filename in webpack's output (not yet written to disk)
    const asset = compilation.assets[path.relative(outputPath, filename).replace(/^\.\//, '')];

    // wait for a disk read if we had to go to disk
    const promise = asset ? Promise.resolve(asset.source()) : readFile(filename, 'utf8');
    return promise.then(sheet => {
    // the reduced critical CSS gets injected into a new <style> tag
      const style = document.createElement('style');
      style.appendChild(document.createTextNode(sheet));
      link.parentNode.insertBefore(style, link.nextSibling);

      // drop a reference to the original URL onto the tag (used for reporting to console later)
      style.$$name = href;

      // the `async` option changes any critical'd <link rel="stylesheet"> tags to async-loaded equivalents
      if (this.options.async) {
        link.setAttribute('rel', 'preload');
        link.setAttribute('as', 'style');
        link.setAttribute('onload', "this.rel='stylesheet'");
      }
    });
  }

  /** Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document. */
  processStyle(style) {
    const done = Promise.resolve();
    const document = style.ownerDocument;

    // basically `.textContent`
    let sheet = style.childNodes.length>0 && style.childNodes.map( node => node.nodeValue ).join('\n');

    // store a reference to the previous serialized stylesheet for reporting stats
    const before = sheet;

    // Skip empty stylesheets
    if (!sheet) return done;

    const ast = css.parse(sheet);

    // Walk all CSS rules, transforming unused rules to comments (which get removed)
    visit(ast, rule => {
      if (rule.type==='rule') {
        // Filter the selector list down to only those matche
        rule.selectors = rule.selectors.filter(sel => {
          // Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
          // This means any selector for a pseudo-element or having a pseudo-class will be inlined if the rest of the selector matches.
          sel = sel.replace(/::?(?:[a-z-]+)([.[#~&^:*]|\s|\n|$)/gi, '$1');
          return document.querySelector(sel, document) != null;
        });
        // If there are no matched selectors, remove the rule:
        if (rule.selectors.length===0) {
          return false;
        }
      }

      // If there are no remaining rules, remove the whole rule.
      return !rule.rules || rule.rules.length!==0;
    });

    sheet = css.stringify(ast, { compress: this.options.compress!==false });

    return done.then(() => {
      // If all rules were removed, get rid of the style element entirely
      if (sheet.trim().length===0) {
        sheet.parentNode.removeChild(sheet);
      }
      else {
        // replace the inline stylesheet with its critical'd counterpart
        while (style.lastChild) {
          style.removeChild(style.lastChild);
        }
        style.appendChild(document.createTextNode(sheet));
      }

      // output some stats
      const name = style.$$name ? style.$$name.replace(/^\//, '') : 'inline CSS';
      const percent = (before.length - sheet.length) / before.length * 100 | 0;
      console.log('\u001b[32mCritters: inlined ' + prettyBytes(sheet.length) + ' (' + percent + '% of original ' + prettyBytes(before.length) + ') of ' + name + '.\u001b[39m');
    });
  }
};

/** Recursively walk all rules in a stylesheet.
 *  The iterator can explicitly return `false` to remove the current node.
 */
function visit(node, fn) {
  if (node.stylesheet) return visit(node.stylesheet, fn);

  node.rules = node.rules.filter(rule => {
    if (rule.rules) {
      visit(rule, fn);
    }
    return fn(rule)!==false;
  });
}

/** Enhance an htmlparser2-style DOM with basic manipulation methods. */
function makeDomInteractive(document) {
  defineProperties(document, DocumentExtensions);
  // Find the first <html> element within the document
  document.documentElement = document.childNodes.filter( child => String(child.tagName).toLowerCase()==='html' )[0];

  // Extend Element.prototype with DOM manipulation methods.
  //   Note: document.$$scratchElement is also used by createTextNode()
  const scratch = document.$$scratchElement = document.createElement('div');
  const elementProto = Object.getPrototypeOf(scratch);
  defineProperties(elementProto, ElementExtensions);
  elementProto.ownerDocument = document;

  // nwmatcher is a selector engine that happens to work with Parse5's htmlparser2 DOM (they form the base of jsdom).
  // It is exposed to the document so that it can be used within Element.prototype methods.
  document.$match = nwmatcher({ document });
  document.$match.configure({
    CACHING: false,
    USE_QSAPI: false,
    USE_HTML5: false
  });
}

/** Essentially Object.defineProperties() except any functions are assigned as values rather than descriptors. */
function defineProperties(obj, properties) {
  for (const i in properties) {
    const value = properties[i];
    Object.defineProperty(obj, i, typeof value === 'function' ? { value } : value);
  }
}

/** {document,Element}.getElementsByTagName() is the only traversal method required by nwmatcher. */
function getElementsByTagName(tagName) {
  const stack = [this];
  const matches = [];
  const isWildCard = tagName === '*';
  const tagNameUpper = tagName.toUpperCase();
  while (stack.length !== 0) {
    const el = stack.pop();
    let child = el.lastChild;
    while (child) {
      if (child.nodeType === 1) stack.push(child);
      child = child.previousSibling;
    }
    if (isWildCard || (el.tagName != null && (el.tagName === tagNameUpper || el.tagName.toUpperCase() === tagNameUpper))) {
      matches.push(el);
    }
  }
  return matches;
}

/** Methods and descriptors to mix into Element.prototype */
const ElementExtensions = {
  nodeName: {
    get() {
      return this.tagName;
    }
  },
  insertBefore(child, referenceNode) {
    if (!referenceNode) return this.appendChild(child);
    treeAdapter.insertBefore(this, child, referenceNode);
    return child;
  },
  appendChild(child) {
    treeAdapter.appendChild(this, child);
    return child;
  },
  removeChild(child) {
    treeAdapter.detachNode(child);
  },
  setAttribute(name, value) {
    if (this.attribs == null) this.attribs = {};
    if (value == null) value = '';
    this.attribs[name] = value;
  },
  removeAttribute(name) {
    if (this.attribs != null) {
      delete this.attribs[name];
    }
  },
  getAttribute(name) {
    return this.attribs != null && this.attribs[name];
  },
  hasAttribute(name) {
    return this.attribs != null && this.attribs[name] != null;
  },
  getAttributeNode(name) {
    const value = this.getAttribute(name);
    if (value != null) return { specified: true, value };
  },
  getElementsByTagName
};

/** Methods and descriptors to mix into the global document instance */
const DocumentExtensions = {
  // document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
  // nwmatcher requires that it at least report a correct nodeType of DOCUMENT_NODE.
  nodeType: {
    get() {
      return 11;
    }
  },
  createElement(name) {
    return treeAdapter.createElement(name, null, []);
  },
  createTextNode(text) {
    // there is no dedicated createTextNode equivalent in htmlparser2's DOM, so
    // we have to insert Text and then remove and return the resulting Text node.
    const scratch = this.$$scratchElement;
    treeAdapter.insertText(scratch, text);
    const node = scratch.lastChild;
    treeAdapter.detachNode(node);
    return node;
  },
  querySelector(sel) {
    return this.$match.first(sel);
  },
  querySelectorAll(sel) {
    return this.$match.select(sel);
  },
  getElementsByTagName,
  // nwmatcher uses inexistence of `document.addEventListener` to detect IE:
  // https://github.com/dperini/nwmatcher/blob/3edb471e12ce7f7d46dc1606c7f659ff45675a29/src/nwmatcher.js#L353
  addEventListener: Object
};<|endoftext|>


<commit_before>const express = require('express');
const request = require('supertest');
const assert = require('assert');

describe('/packages/healthcheck', () => {
  let app;
  const healthcheck = require('../');

  beforeEach(() => {
    app = express();
  });

  describe('/', () => {
    it('allows advanced config', () => {
      app.use(
        '/health',
        healthcheck({ healthChecks: { github: { method: 'POST', url: 'https://status.github.com/api/status.json' } } })
      );
      return request(app)
        .get('/health')
        .expect(503)
        .then(res => {
          assert.deepEqual(res.body, { github: 404 });
        });
    });
    it('returns status 503 if a service is erroneous', () => {
      app.use(
        '/health',
        healthcheck({
          healthChecks: { google: 'https://google.com/ping', github: 'https://status.github.com/api/status.json' },
        })
      );
      return request(app)
        .get('/health')
        .expect(503)
        .then(res => {
          assert.deepEqual(res.body, { google: 400, github: 200 });
        });
    });
    it('returns status 200 all services are ok', () => {
      app.use('/health', healthcheck({ healthChecks: { github: 'https://status.github.com/api/status.json' } }));
      return request(app)
        .get('/health')
        .expect(200)
        .then(res => {
          assert.deepEqual(res.body, { github: 200 });
        });
    });
  });

  describe('/details', () => {
    it('allows advanced config', () => {
      app.use(
        '/health',
        healthcheck({
          healthChecks: { github: { method: 'POST', url: 'https://status.github.com/api/status.json' } },
        })
      );
      return request(app)
        .get('/health/details')
        .expect(503)
        .then(res => {
          assert.equal(res.body.github.method, 'POST');
          assert.equal(res.body.github.url, 'https://status.github.com/api/status.json');
          assert.equal(res.body.github.status, 404);
          assert.equal(typeof res.body.github.request.headers, 'object');
          assert.equal(typeof res.body.github.response.headers, 'object');
        });
    });
    it('returns status 503 if a service is erroneous', () => {
      app.use(
        '/health',
        healthcheck({
          healthChecks: { google: 'https://google.com/ping', github: 'https://status.github.com/api/status.json' },
        })
      );
      return request(app)
        .get('/health/details')
        .expect(503)
        .then(res => {
          assert.equal(res.body.google.method, 'GET');
          assert.equal(res.body.google.url, 'https://google.com/ping');
          assert.equal(res.body.google.status, 400);
          assert.equal(typeof res.body.google.request.headers, 'object');
          assert.equal(typeof res.body.google.response.headers, 'object');
          assert.equal(res.body.github.method, 'GET');
          assert.equal(res.body.github.url, 'https://status.github.com/api/status.json');
          assert.equal(res.body.github.status, 200);
          assert.equal(typeof res.body.github.request.headers, 'object');
          assert.equal(typeof res.body.github.response.headers, 'object');
        });
    });
    it('returns status 200 all services are ok', () => {
      app.use('/health', healthcheck({ healthChecks: { github: 'https://status.github.com/api/status.json' } }));
      return request(app)
        .get('/health/details')
        .expect(200)
        .then(res => {
          assert.equal(res.body.github.method, 'GET');
          assert.equal(res.body.github.url, 'https://status.github.com/api/status.json');
          assert.equal(res.body.github.status, 200);
          assert.equal(typeof res.body.github.request.headers, 'object');
          assert.equal(typeof res.body.github.response.headers, 'object');
          assert.equal(typeof res.body.github.response.body, 'object');
        });
    });
  });
});
<commit_msg>Fixes the healthcheck tests<commit_after>const express = require('express');
const request = require('supertest');
const assert = require('assert');

describe('/packages/healthcheck', () => {
  let app;
  const healthcheck = require('../');

  beforeEach(() => {
    app = express();
  });

  describe('/', () => {
    it('allows advanced config', () => {
      app.use(
        '/health',
        healthcheck({ healthChecks: { github: { method: 'POST', url: 'https://status.github.com/api/status.json' } } })
      );
      return request(app)
        .get('/health')
        .expect(503)
        .then(res => {
          assert.deepEqual(res.body, { github: 404 });
        });
    });
    it('returns status 503 if a service is erroneous', () => {
      app.use(
        '/health',
        healthcheck({
          healthChecks: { google: 'https://google.com/ping', github: 'https://status.github.com/api/status.json' },
        })
      );
      return request(app)
        .get('/health')
        .expect(503)
        .then(res => {
          assert.deepEqual(res.body, { google: 400, github: 200 });
        });
    });
    it('returns status 200 all services are ok', () => {
      app.use('/health', healthcheck({ healthChecks: { github: 'https://status.github.com/api/status.json' } }));
      return request(app)
        .get('/health')
        .expect(200)
        .then(res => {
          assert.deepEqual(res.body, { github: 200 });
        });
    });
  });

  describe('/details', () => {
    it('allows advanced config', () => {
      app.use(
        '/health',
        healthcheck({
          healthChecks: { github: { method: 'POST', url: 'https://status.github.com/api/status.json' } },
        })
      );
      return request(app)
        .get('/health/details')
        .expect(503)
        .then(res => {
          assert.equal(res.body.github.method, 'POST');
          assert.equal(res.body.github.url, 'https://status.github.com/api/status.json');
          assert.equal(res.body.github.status, 404);
          assert.equal(typeof res.body.github.response.headers, 'object');
        });
    });
    it('returns status 503 if a service is erroneous', () => {
      app.use(
        '/health',
        healthcheck({
          healthChecks: { google: 'https://google.com/ping', github: 'https://status.github.com/api/status.json' },
        })
      );
      return request(app)
        .get('/health/details')
        .expect(503)
        .then(res => {
          assert.equal(res.body.google.method, 'GET');
          assert.equal(res.body.google.url, 'https://google.com/ping');
          assert.equal(res.body.google.status, 400);
          assert.equal(typeof res.body.google.response.headers, 'object');
          assert.equal(res.body.github.method, 'GET');
          assert.equal(res.body.github.url, 'https://status.github.com/api/status.json');
          assert.equal(res.body.github.status, 200);
          assert.equal(typeof res.body.github.response.headers, 'object');
        });
    });
    it('returns status 200 all services are ok', () => {
      app.use('/health', healthcheck({ healthChecks: { github: 'https://status.github.com/api/status.json' } }));
      return request(app)
        .get('/health/details')
        .expect(200)
        .then(res => {
          assert.equal(res.body.github.method, 'GET');
          assert.equal(res.body.github.url, 'https://status.github.com/api/status.json');
          assert.equal(res.body.github.status, 200);
          assert.equal(typeof res.body.github.response.headers, 'object');
          assert.equal(typeof res.body.github.response.body, 'object');
        });
    });
  });
});
<|endoftext|>


<commit_before>/**
 * @module echarts/component/helper/RoamController
 */

define(function (require) {

    var Eventful = require('zrender/mixin/Eventful');
    var zrUtil = require('zrender/core/util');

    function mousedown(e) {
        if (e.target && e.target.draggable) {
            return;
        }

        var x = e.offsetX;
        var y = e.offsetY;
        var rect = this.rect;
        if (rect && rect.contain(x, y)) {
            this._x = x;
            this._y = y;
            this._dragging = true;
        }
    }

    function mousemove(e) {
        if (this._dragging && e.gestureEvent !== 'pinchchange') {

            var rawE = e.event;
            rawE.preventDefault && rawE.preventDefault();

            var x = e.offsetX;
            var y = e.offsetY;

            var dx = x - this._x;
            var dy = y - this._y;

            this._x = x;
            this._y = y;

            var target = this.target;

            if (target) {
                var pos = target.position;
                pos[0] += dx;
                pos[1] += dy;
                target.dirty();
            }

            this.trigger('pan', dx, dy);
        }
    }

    function mouseup(e) {
        this._dragging = false;
    }

    function mousewheel(e) {
        var zoomDelta = e.wheelDelta < 0 ? 1.1 : 1 / 1.1;
        zoom.call(this, e, zoomDelta, e.offsetX, e.offsetY);
    }

    function pinch(e) {
        var zoomDelta = e.pinchScale > 1 ? 1.1 : 1 / 1.1;
        zoom.call(this, e, zoomDelta, e.pinchX, e.pinchY);
    }

    function zoom(e, zoomDelta, zoomX, zoomY) {
        var rect = this.rect;

        if (rect && rect.contain(zoomX, zoomY)) {

            var target = this.target;

            if (target) {
                var pos = target.position;
                var scale = target.scale;

                var newZoom = this._zoom = this._zoom || 1;
                newZoom *= zoomDelta;
                // newZoom = Math.max(
                //     Math.min(target.maxZoom, newZoom),
                //     target.minZoom
                // );
                var zoomScale = newZoom / this._zoom;
                this._zoom = newZoom;
                // Keep the mouse center when scaling
                pos[0] -= (zoomX - pos[0]) * (zoomScale - 1);
                pos[1] -= (zoomY - pos[1]) * (zoomScale - 1);
                scale[0] *= zoomScale;
                scale[1] *= zoomScale;

                target.dirty();
            }

            this.trigger('zoom', zoomDelta, zoomX, zoomY);
        }
    }

    /**
     * @alias module:echarts/component/helper/RoamController
     * @constructor
     * @mixin {module:zrender/mixin/Eventful}
     *
     * @param {module:zrender/zrender~ZRender} zr
     * @param {module:zrender/Element} target
     * @param {module:zrender/core/BoundingRect} rect
     */
    function RoamController(zr, target, rect) {

        /**
         * @type {module:zrender/Element}
         */
        this.target = target;

        /**
         * @type {module:zrender/core/BoundingRect}
         */
        this.rect = rect;

        // Avoid two roamController bind the same handler
        var bind = zrUtil.bind;
        var mousedownHandler = bind(mousedown, this);
        var mousemoveHandler = bind(mousemove, this);
        var mouseupHandler = bind(mouseup, this);
        var mousewheelHandler = bind(mousewheel, this);
        var pinchHandler = bind(pinch, this);

        Eventful.call(this);

        /**
         * @param  {boolean} [controlType=true] Specify the control type, which can be only 'pan' or 'zoom'
         */
        this.enable = function (controlType) {
            // Disable previous first
            this.disable();
            if (controlType == null) {
                controlType = true;
            }
            if (controlType && controlType !== 'zoom') {
                zr.on('mousedown', mousedownHandler);
                zr.on('mousemove', mousemoveHandler);
                zr.on('mouseup', mouseupHandler);
            }
            if (controlType && controlType !== 'pan') {
                zr.on('mousewheel', mousewheelHandler);
                zr.on('pinch', pinchHandler);
            }
        };

        this.disable = function () {
            zr.off('mousedown', mousedownHandler);
            zr.off('mousemove', mousemoveHandler);
            zr.off('mouseup', mouseupHandler);
            zr.off('mousewheel', mousewheelHandler);
            zr.off('pinch', pinchHandler);
        };

        this.dispose = this.disable;

        this.isDragging = function () {
            return this._dragging;
        };

        this.isPinching = function () {
            return this._pinching;
        };
    }

    zrUtil.mixin(RoamController, Eventful);

    return RoamController;
});<commit_msg>roam fix<commit_after>/**
 * @module echarts/component/helper/RoamController
 */

define(function (require) {

    var Eventful = require('zrender/mixin/Eventful');
    var zrUtil = require('zrender/core/util');

    function mousedown(e) {
        if (e.target && e.target.draggable) {
            return;
        }

        var x = e.offsetX;
        var y = e.offsetY;
        var rect = this.rect;
        if (rect && rect.contain(x, y)) {
            this._x = x;
            this._y = y;
            this._dragging = true;
        }
    }

    function mousemove(e) {
        if (this._dragging && e.gestureEvent !== 'pinch') {

            var rawE = e.event;
            rawE.preventDefault && rawE.preventDefault();

            var x = e.offsetX;
            var y = e.offsetY;

            var dx = x - this._x;
            var dy = y - this._y;

            this._x = x;
            this._y = y;

            var target = this.target;

            if (target) {
                var pos = target.position;
                pos[0] += dx;
                pos[1] += dy;
                target.dirty();
            }

            this.trigger('pan', dx, dy);
        }
    }

    function mouseup(e) {
        this._dragging = false;
    }

    function mousewheel(e) {
        var zoomDelta = e.wheelDelta < 0 ? 1.1 : 1 / 1.1;
        zoom.call(this, e, zoomDelta, e.offsetX, e.offsetY);
    }

    function pinch(e) {
        var zoomDelta = e.pinchScale > 1 ? 1.1 : 1 / 1.1;
        zoom.call(this, e, zoomDelta, e.pinchX, e.pinchY);
    }

    function zoom(e, zoomDelta, zoomX, zoomY) {
        var rect = this.rect;

        if (rect && rect.contain(zoomX, zoomY)) {

            var target = this.target;

            if (target) {
                var pos = target.position;
                var scale = target.scale;

                var newZoom = this._zoom = this._zoom || 1;
                newZoom *= zoomDelta;
                // newZoom = Math.max(
                //     Math.min(target.maxZoom, newZoom),
                //     target.minZoom
                // );
                var zoomScale = newZoom / this._zoom;
                this._zoom = newZoom;
                // Keep the mouse center when scaling
                pos[0] -= (zoomX - pos[0]) * (zoomScale - 1);
                pos[1] -= (zoomY - pos[1]) * (zoomScale - 1);
                scale[0] *= zoomScale;
                scale[1] *= zoomScale;

                target.dirty();
            }

            this.trigger('zoom', zoomDelta, zoomX, zoomY);
        }
    }

    /**
     * @alias module:echarts/component/helper/RoamController
     * @constructor
     * @mixin {module:zrender/mixin/Eventful}
     *
     * @param {module:zrender/zrender~ZRender} zr
     * @param {module:zrender/Element} target
     * @param {module:zrender/core/BoundingRect} rect
     */
    function RoamController(zr, target, rect) {

        /**
         * @type {module:zrender/Element}
         */
        this.target = target;

        /**
         * @type {module:zrender/core/BoundingRect}
         */
        this.rect = rect;

        // Avoid two roamController bind the same handler
        var bind = zrUtil.bind;
        var mousedownHandler = bind(mousedown, this);
        var mousemoveHandler = bind(mousemove, this);
        var mouseupHandler = bind(mouseup, this);
        var mousewheelHandler = bind(mousewheel, this);
        var pinchHandler = bind(pinch, this);

        Eventful.call(this);

        /**
         * @param  {boolean} [controlType=true] Specify the control type, which can be only 'pan' or 'zoom'
         */
        this.enable = function (controlType) {
            // Disable previous first
            this.disable();
            if (controlType == null) {
                controlType = true;
            }
            if (controlType && controlType !== 'zoom') {
                zr.on('mousedown', mousedownHandler);
                zr.on('mousemove', mousemoveHandler);
                zr.on('mouseup', mouseupHandler);
            }
            if (controlType && controlType !== 'pan') {
                zr.on('mousewheel', mousewheelHandler);
                zr.on('pinch', pinchHandler);
            }
        };

        this.disable = function () {
            zr.off('mousedown', mousedownHandler);
            zr.off('mousemove', mousemoveHandler);
            zr.off('mouseup', mouseupHandler);
            zr.off('mousewheel', mousewheelHandler);
            zr.off('pinch', pinchHandler);
        };

        this.dispose = this.disable;

        this.isDragging = function () {
            return this._dragging;
        };

        this.isPinching = function () {
            return this._pinching;
        };
    }

    zrUtil.mixin(RoamController, Eventful);

    return RoamController;
});<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import styles from './styles.scss';
import dynamicsFont from '../../assets/DynamicsFont/DynamicsFont.scss';

class DropdownSelect extends Component {
  constructor(props) {
    super(props);
    this.state = {
      open: this.props.open,
    };
  }
  toggleShowDropdown = (e) => {
    if (!this.state.open) {
      window.addEventListener('click', this._handleDocumentClick, false);
    } else {
      window.removeEventListener('click', this._handleDocumentClick, false);
    }
    if (e && this.props.stopPropagation) {
      e.stopPropagation();
    }
    if (this.props.disabled) {
      return;
    }
    this.props.onToggle(!this.state.open);
    this.setState(preState => ({
      open: !preState.open,
    }));
  }

  onChange = (e, option, idx) => {
    e.stopPropagation();
    if (this.props.placeholder && idx === 0) {
      this.toggleShowDropdown();
      return;
    }
    this.props.onChange(option, idx);
    this.toggleShowDropdown();
  };

  _handleDocumentClick = (e) => {
    if (!this.mounted) {
      return;
    }
    if (this.wrapper && this.wrapper.contains(e.target)) {
      return;
    }
    if (this.dropdownMenu && this.dropdownMenu.contains(e.target)) {
      return;
    }
    this.props.onToggle(false);
    this.setState({
      open: false,
    });
  }

  componentDidMount() {
    this.mounted = true;
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.open !== this.state.open) {
      if (this.props.renderDropdownMenu && this.wrapper) {
        const menu = this.renderDropdownMenu();
        const buttomPosition = this.wrapper.getBoundingClientRect();
        this.props.renderDropdownMenu(menu, this.state.open, buttomPosition);
      }
    }
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.open !== undefined &&
      nextProps.open !== this.state.open) {
      this.setState({
        open: nextProps.open,
      });
    }
  }
  componentWillUnmount() {
    this.mounted = false;
    window.removeEventListener('click', this._handleDocumentClick, false);
  }

  valueFunction(_, idx) {
    if (this.props.placeholder) {
      idx = `${idx - 1}`;
    }
    return this.props.valueFunction(_, idx);
  }

  renderFunction(option, idx) {
    if (this.props.placeholder) {
      return idx === 0 ? this.props.placeholder : this.props.renderFunction(option, idx);
    }
    return this.props.renderFunction(option, idx);
  }

  renderValue(value) {
    if (this.props.placeholder) {
      value = parseInt(value, 10) + 1;
      return value === 0 ? this.props.placeholder : this.props.renderValue(value - 1);
    }
    return this.props.renderValue(value);
  }

  renderTitle(selectedOption, defaultTitle) {
    if (this.props.titleEnabled) {
      return typeof this.props.renderTitle === 'function' ?
        this.props.renderTitle(selectedOption) : defaultTitle;
    }
    return '';
  }
  renderDropdownMenu() {
    let options;
    const { placeholder, ellipsis } = this.props;
    if (placeholder) {
      options = [
        {},
        ...this.props.options,
      ];
    } else {
      options = this.props.options;
    }
    return (

 { this.dropdownMenu = ref; }}>
        {
          options.map((option, idx) => {
            const currentValue = this.valueFunction(option, idx);
            const className = classnames(
              styles.dropdownItem,
              this.props.value === currentValue ? styles.selected : null,
            );
            const display = this.renderFunction(option, idx);
            return (

 this.onChange(e, option, idx)}
              >
                {display}

            );
          })
        }

    );
  }

  render() {
    const ellipsis = this.props.ellipsis;
    const reference = this.props.reference;
    const label = this.props.label ?
      (

          {this.props.label}

      ) : null;
    const iconClassName = classnames(
      styles.icon,
      this.state.open ? styles.iconUp : null,
      this.props.iconClassName,
    );
    const containerClassName = classnames(
      styles.root,
      this.props.className,
      this.props.disabled ? styles.disabled : null,
      this.state.open ? styles.open : null,
      this.props.noPadding ? styles.noPadding : null,
    );
    const buttonClassName = classnames(
      styles.button,
      this.props.disabled ? styles.disabled : null,
    );
    const dropdownMenu = this.props.renderDropdownMenu ?
      null :
      this.renderDropdownMenu();

    const renderValue = this.renderValue(this.props.value);
    return (

 {
          if (reference) reference(ref);
          this.wrapper = ref;
        }}
      >

          {label}

            {renderValue}

        {dropdownMenu}

    );
  }
}

DropdownSelect.propTypes = {
  reference: PropTypes.func,
  className: PropTypes.string,
  iconClassName: PropTypes.string,
  value: PropTypes.string,
  label: PropTypes.string,
  onChange: PropTypes.func,
  disabled: PropTypes.bool,
  options: PropTypes.arrayOf(PropTypes.any).isRequired,
  valueFunction: PropTypes.func,
  renderFunction: PropTypes.func,
  renderValue: PropTypes.func,
  renderDropdownMenu: PropTypes.func,
  renderTitle: PropTypes.func,
  titleEnabled: PropTypes.bool,
  dropdownAlign: PropTypes.oneOf(['left', 'center', 'right']),
  stopPropagation: PropTypes.bool,
  placeholder: PropTypes.string,
  ellipsis: PropTypes.bool,
  noPadding: PropTypes.bool,
  onToggle: PropTypes.func,
  open: PropTypes.bool,
  wrapperStyle: PropTypes.string,
  buttonStyle: PropTypes.string,
};

DropdownSelect.defaultProps = {
  reference: undefined,
  className: null,
  iconClassName: null,
  value: null,
  label: null,
  onChange: undefined,
  disabled: false,
  renderDropdownMenu: undefined,
  renderTitle: undefined,
  valueFunction: (_, idx) => idx,
  renderFunction: option => option,
  renderValue: option => option,
  dropdownAlign: 'center',
  titleEnabled: undefined,
  stopPropagation: false,
  placeholder: undefined,
  ellipsis: true,
  noPadding: false,
  onToggle: () => {},
  open: false,
  wrapperStyle: '',
  buttonStyle: ''
};

export default DropdownSelect;
<commit_msg>no message (#695)<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import styles from './styles.scss';
import dynamicsFont from '../../assets/DynamicsFont/DynamicsFont.scss';

class DropdownSelect extends Component {
  constructor(props) {
    super(props);
    this.state = {
      open: this.props.open,
    };
  }
  toggleShowDropdown = (e) => {
    if (!this.state.open) {
      window.addEventListener('click', this._handleDocumentClick, false);
    } else {
      window.removeEventListener('click', this._handleDocumentClick, false);
    }
    if (e && this.props.stopPropagation) {
      e.stopPropagation();
    }
    if (this.props.disabled) {
      return;
    }
    this.props.onToggle(!this.state.open);
    this.setState(preState => ({
      open: !preState.open,
    }));
  }

  onChange = (e, option, idx) => {
    e.stopPropagation();
    if (this.props.placeholder && idx === 0) {
      this.toggleShowDropdown();
      return;
    }
    this.props.onChange(option, idx);
    this.toggleShowDropdown();
  };

  _handleDocumentClick = (e) => {
    if (!this.mounted) {
      return;
    }
    if (this.wrapper && this.wrapper.contains(e.target)) {
      return;
    }
    if (this.dropdownMenu && this.dropdownMenu.contains(e.target)) {
      return;
    }
    this.props.onToggle(false);
    this.setState({
      open: false,
    });
  }

  componentDidMount() {
    this.mounted = true;
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.open !== this.state.open) {
      if (this.props.renderDropdownMenu && this.wrapper) {
        const menu = this.renderDropdownMenu();
        const buttomPosition = this.wrapper.getBoundingClientRect();
        this.props.renderDropdownMenu(menu, this.state.open, buttomPosition);
      }
    }
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.open !== undefined &&
      nextProps.open !== this.props.open) {
      this.setState({
        open: nextProps.open,
      });
    }
  }
  componentWillUnmount() {
    this.mounted = false;
    window.removeEventListener('click', this._handleDocumentClick, false);
  }

  valueFunction(_, idx) {
    if (this.props.placeholder) {
      idx = `${idx - 1}`;
    }
    return this.props.valueFunction(_, idx);
  }

  renderFunction(option, idx) {
    if (this.props.placeholder) {
      return idx === 0 ? this.props.placeholder : this.props.renderFunction(option, idx);
    }
    return this.props.renderFunction(option, idx);
  }

  renderValue(value) {
    if (this.props.placeholder) {
      value = parseInt(value, 10) + 1;
      return value === 0 ? this.props.placeholder : this.props.renderValue(value - 1);
    }
    return this.props.renderValue(value);
  }

  renderTitle(selectedOption, defaultTitle) {
    if (this.props.titleEnabled) {
      return typeof this.props.renderTitle === 'function' ?
        this.props.renderTitle(selectedOption) : defaultTitle;
    }
    return '';
  }
  renderDropdownMenu() {
    let options;
    const { placeholder, ellipsis } = this.props;
    if (placeholder) {
      options = [
        {},
        ...this.props.options,
      ];
    } else {
      options = this.props.options;
    }
    return (

 { this.dropdownMenu = ref; }}>
        {
          options.map((option, idx) => {
            const currentValue = this.valueFunction(option, idx);
            const className = classnames(
              styles.dropdownItem,
              this.props.value === currentValue ? styles.selected : null,
            );
            const display = this.renderFunction(option, idx);
            return (

 this.onChange(e, option, idx)}
              >
                {display}

            );
          })
        }

    );
  }

  render() {
    const ellipsis = this.props.ellipsis;
    const reference = this.props.reference;
    const label = this.props.label ?
      (

          {this.props.label}

      ) : null;
    const iconClassName = classnames(
      styles.icon,
      this.state.open ? styles.iconUp : null,
      this.props.iconClassName,
    );
    const containerClassName = classnames(
      styles.root,
      this.props.className,
      this.props.disabled ? styles.disabled : null,
      this.state.open ? styles.open : null,
      this.props.noPadding ? styles.noPadding : null,
    );
    const buttonClassName = classnames(
      styles.button,
      this.props.disabled ? styles.disabled : null,
    );
    const dropdownMenu = this.props.renderDropdownMenu ?
      null :
      this.renderDropdownMenu();

    const renderValue = this.renderValue(this.props.value);
    return (

 {
          if (reference) reference(ref);
          this.wrapper = ref;
        }}
      >

          {label}

            {renderValue}

        {dropdownMenu}

    );
  }
}

DropdownSelect.propTypes = {
  reference: PropTypes.func,
  className: PropTypes.string,
  iconClassName: PropTypes.string,
  value: PropTypes.string,
  label: PropTypes.string,
  onChange: PropTypes.func,
  disabled: PropTypes.bool,
  options: PropTypes.arrayOf(PropTypes.any).isRequired,
  valueFunction: PropTypes.func,
  renderFunction: PropTypes.func,
  renderValue: PropTypes.func,
  renderDropdownMenu: PropTypes.func,
  renderTitle: PropTypes.func,
  titleEnabled: PropTypes.bool,
  dropdownAlign: PropTypes.oneOf(['left', 'center', 'right']),
  stopPropagation: PropTypes.bool,
  placeholder: PropTypes.string,
  ellipsis: PropTypes.bool,
  noPadding: PropTypes.bool,
  onToggle: PropTypes.func,
  open: PropTypes.bool,
  wrapperStyle: PropTypes.string,
  buttonStyle: PropTypes.string,
};

DropdownSelect.defaultProps = {
  reference: undefined,
  className: null,
  iconClassName: null,
  value: null,
  label: null,
  onChange: undefined,
  disabled: false,
  renderDropdownMenu: undefined,
  renderTitle: undefined,
  valueFunction: (_, idx) => idx,
  renderFunction: option => option,
  renderValue: option => option,
  dropdownAlign: 'center',
  titleEnabled: undefined,
  stopPropagation: false,
  placeholder: undefined,
  ellipsis: true,
  noPadding: false,
  onToggle: () => {},
  open: false,
  wrapperStyle: '',
  buttonStyle: ''
};

export default DropdownSelect;
<|endoftext|>


<commit_before>import React from 'react'
import { connect } from 'react-redux'
import { cancelTagDescriptionEdit, editTagDescription, editNewTagAndDescription } from '../actions'
import { debounce, keys, isEmpty, map } from 'lodash'
import { hashtagWordRegex } from '../models/hashtag'
import { BareModalWrapper, Modal } from './Modal'
import ModalInput from './ModalInput'
import cx from 'classnames'
const { func, object, bool } = React.PropTypes

@connect((state, props) => ({
  tags: state.tagDescriptionEdits,
  creating: state.creatingTagAndDescription
}))
export default class TagDescriptionEditor extends React.Component {
  static propTypes = {
    tags: object,
    saveParent: func,
    saveTagDescriptions: func,
    updatePostTag: func,
    dispatch: func,
    creating: bool
  }

  render () {
    let { tags, saveParent, updatePostTag, dispatch, creating } = this.props
    const cancel = () => dispatch(cancelTagDescriptionEdit())
    const editAction = creating ? editNewTagAndDescription : editTagDescription
    const edit = debounce((tag, value) =>
      dispatch(editAction(tag, value)), 200)

    if (isEmpty(tags)) {
      if (!creating) return null
      tags = {'': ''}
    }

    const validate = tags => {
      // only called when creating a tag so we can assume there is exactly one tag
      let tag = keys(tags)[0]
      if (tag[0].match(/[^A-Za-z]/)) {
        window.alert('Topic names must start with a letter')
        return false
      } else if (tag.length < 2) {
        window.alert('Topic names must be at least 2 characters')
        return false
      } else if (!tag.match(hashtagWordRegex)) {
        window.alert('Topic names can only use letters, numbers and underscores, with no spaces.')
        return false
      }
      return true
    }
    const createTag = () => {
      if (!validate(tags)) return
      updatePostTag(tags)
      cancel()
    }

    const title = `Hey, you're creating ${keys(tags).length > 1 ? 'new topics.' : 'a new topic.'}`

    return <BareModalWrapper>
      <Modal id='tag-description-editor' title={title}>
        {map(tags, (description, tag, i) =>

            {creating
              ? <ModalInput label='Topic name' defaultValue={tag}
              onChange={event => edit(event.target.value, description)}/>
              :

                  Topic name
                  #{tag}

}
            <ModalInput label='Description' defaultValue={description}
              onChange={event => edit(tag, event.target.value)}/>

)}

          <button onClick={creating ? createTag : () => saveParent(tags)}
            className='ok'>
            Create
          </button>

      </Modal>
    </BareModalWrapper>
  }
}
<commit_msg>fix cancel button in tag description editor<commit_after>import React from 'react'
import { connect } from 'react-redux'
import { cancelTagDescriptionEdit, editTagDescription, editNewTagAndDescription } from '../actions'
import { debounce, keys, isEmpty, map } from 'lodash'
import { hashtagWordRegex } from '../models/hashtag'
import { BareModalWrapper, Modal } from './Modal'
import ModalInput from './ModalInput'
import cx from 'classnames'
const { func, object, bool } = React.PropTypes

@connect((state, props) => ({
  tags: state.tagDescriptionEdits,
  creating: state.creatingTagAndDescription
}))
export default class TagDescriptionEditor extends React.Component {
  static propTypes = {
    tags: object,
    saveParent: func,
    saveTagDescriptions: func,
    updatePostTag: func,
    dispatch: func,
    creating: bool
  }

  render () {
    let { tags, saveParent, updatePostTag, dispatch, creating } = this.props
    const cancel = () => dispatch(cancelTagDescriptionEdit())
    const editAction = creating ? editNewTagAndDescription : editTagDescription
    const edit = debounce((tag, value) =>
      dispatch(editAction(tag, value)), 200)

    if (isEmpty(tags)) {
      if (!creating) return null
      tags = {'': ''}
    }

    const validate = tags => {
      // only called when creating a tag so we can assume there is exactly one tag
      let tag = keys(tags)[0]
      if (tag[0].match(/[^A-Za-z]/)) {
        window.alert('Topic names must start with a letter')
        return false
      } else if (tag.length < 2) {
        window.alert('Topic names must be at least 2 characters')
        return false
      } else if (!tag.match(hashtagWordRegex)) {
        window.alert('Topic names can only use letters, numbers and underscores, with no spaces.')
        return false
      }
      return true
    }
    const createTag = () => {
      if (!validate(tags)) return
      updatePostTag(tags)
      cancel()
    }

    const title = `Hey, you're creating ${keys(tags).length > 1 ? 'new topics.' : 'a new topic.'}`

    return <BareModalWrapper>
      <Modal id='tag-description-editor' title={title} onCancel={cancel}>
        {map(tags, (description, tag, i) =>

            {creating
              ? <ModalInput label='Topic name' defaultValue={tag}
              onChange={event => edit(event.target.value, description)}/>
              :

                  Topic name
                  #{tag}

}
            <ModalInput label='Description' defaultValue={description}
              onChange={event => edit(tag, event.target.value)}/>

)}

          <button onClick={creating ? createTag : () => saveParent(tags)}
            className='ok'>
            Create
          </button>

      </Modal>
    </BareModalWrapper>
  }
}
<|endoftext|>