Skip to content

Instantly share code, notes, and snippets.

@1Cr18Ni9
Last active April 8, 2020 23:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1Cr18Ni9/21b8bd2bb7839573e7f26d046a5e5eb3 to your computer and use it in GitHub Desktop.
Save 1Cr18Ni9/21b8bd2bb7839573e7f26d046a5e5eb3 to your computer and use it in GitHub Desktop.
PackTree(logical Tree)
license: mit

Author: cxhair@163.com

Amended based on my previous work: Dynamic Pack.

Node category:

  • dataNode: created by use when filling dialog
  • andNode: logical node created by dataNode merged
  • orNode: logical node created by dataNode merged

How does node merging work

  1. If two dataNodes come close merge them into a new andNode
  2. If two andNodes come close,one of them will grow into a bigger andNode and the another will be removed
  3. If two orNodes come close merge them into a new bigger orNode
  4. If one node is dataNode and another is logic node(wrapper node), dataNode will be merged into that logic node
  5. If both are logic node but NOT the same type, andNode will be merged into orNode

Chart Events

  • Right click on blank canvas will pop the dialog that used for creating dataNode
  • Right click on nodes: dismissed from parent logical node or removed after user confirmed.
  • Ctrl + Click: hold on the Ctrl then click on dataNode under the root teee will merge those dataNode into a bigger orNode
  • Drag dataNode close to another dataNode will merged into a andNode
  • Double click on dataNode will pop dialog and update it after user confirmed.

Dialog Events

  • Esc: close dialog
  • Query items can be shifted by dragging on handlers
  • Data validation
{"name":"bbc","items":[{"name":"Lorx xsum","type":"0"},{"name":"dolox xit","type":"1"},{"name":"amet xx ctetur","type":"3"},{"name":"adix xcing","type":"2"},{"name":"elx xit","type":"4"},{"name":"maxime","type":"9"},{"name":"odio","type":"0"},{"name":"sequi","type":"1"},{"name":"mollx xia","type":"3"},{"name":"magni","type":"2"},{"name":"facilis","type":"4"},{"name":"eniam","type":"9"},{"name":"esciunt","type":"0"},{"name":"isa viam","type":"3"},{"name":"consuntur","type":"2"},{"name":"oluta","type":"4"},{"name":"tnimi","type":"9"},{"name":"caecati","type":"1"}]}
{"name":"cba","items":[{"name":"2Lorx xsum","type":"0"},{"name":"2dolox xit","type":"1"},{"name":"2amet xx ctetur","type":"3"},{"name":"2adix xcing","type":"2"},{"name":"2elx xit","type":"4"},{"name":"maxime","type":"9"},{"name":"2odio","type":"0"},{"name":"sequi","type":"1"},{"name":"2mollx xia","type":"3"},{"name":"2magni","type":"2"},{"name":"2facilis","type":"4"},{"name":"2eniam","type":"9"},{"name":"2esciunt","type":"0"},{"name":"2isa viam","type":"3"},{"name":"2consuntur","type":"2"},{"name":"2oluta","type":"4"},{"name":"2tnimi","type":"9"},{"name":"2caecati","type":"1"}]}
(function (window, d3) {
"use strict";
if (window.PackTree) { return console.log("PackTree has been loaded!"); }
// svg filter
d3.select(document.body)
.append("svg")
.attr("class", "filter_only_svg")
.attr("style", "position:absolute;width:0;height:0;z-index:-5;")
.html('<defs> \
<filter id="packTreeBlur" x="-25%" y="-25%" width="150%" height="150%"> \
<feOffset dx="0" dy="0" in="SourceGraphic" result="o1" /> \
<feGaussianBlur in="o1" stdDeviation="5" result="b1" /> \
<feMerge> \
<feMergeNode in="b1" /> \
<feMergeNode in="SourceGraphic" /> \
</feMerge> \
</filter> \
</defs>'
);
function clearEmptyOrOnlyOneChildLogicalNode (node, parent) {
// checking if node is leaf node
if (node.type == "data") { return; }
var index;
(parent && (index = parent.children.findIndex(function (d) { return d === node; })));
if (parent && node.children.length == 0) {
parent.children.splice(index, 1);
}
else if (parent && node.children.length == 1) {
var nNode = node.children[0];
parent.children.push(nNode);
parent.children.splice(index, 1);
clearEmptyOrOnlyOneChildLogicalNode(nNode, parent);
}
else {
for (var i = 0, l = node.children.length; i < l; i++) {
clearEmptyOrOnlyOneChildLogicalNode(node.children[i], node);
}
}
}
var rootInit = (function () {
var loosePack = d3.pack().padding(15);
var sum = function (d) { return d.value; },
sort = function (a, b) { return b.r - a.r; },
andNodeOnly = function (node) { return node.data.type == "and"; },
nodeLocReassignment = function (node) {
// container origin is the offset for children
var x = node.x, y = node.y,
circle,
childNodes = node.children;
d3.packSiblings(childNodes);
childNodes.forEach(function (d) {
d.x += x;
d.y += y;
});
circle = d3.packEnclose(childNodes);
node.r = circle.r;
};
var f = function () {
clearEmptyOrOnlyOneChildLogicalNode(this._tree);
var root = d3.hierarchy(this._tree).sum(sum).sort(sort),
nodes = root.descendants();
loosePack.radius(() => this._r).size([this._w, this._h])(root);
// find all "and" container and relocat all data nodes
nodes
.filter(andNodeOnly)
.forEach(nodeLocReassignment);
return nodes;
};
return f;
})();
var uuid = (function () {
var i = 0, prefix = "w", f;
f = function () { return prefix + "" + i++; };
f.prefix = function (v) { prefix = v; return f; };
return f;
})();
// extract tspan text based on bind data
var appendText = function (selection, d, clipId) {
var f = [],
i = 0,
q = d.data.bind.querys,
y = {"4": -30, "3": -20, "2": -10, "1": 0};
f.push("[" + d.data.bind.collection + "]");
while (q[i] && i < 3) {
f.push(q[i].name + " " + q[i].logic + " " + q[i].query);
i++;
}
selection.select(".svg__text").remove();
selection.append("text")
.attr("class", "svg__text")
.attr("y", y[f.length])
.attr("clip-path", "url(#" + clipId + ")")
.selectAll("tspan")
.data(f)
.enter()
.append("tspan")
.attr("x", (j, i) => i ? 0 : null)
.attr("dy", (j, i) => i ? 20 : null)
.text(j => j);
};
// vm dialog callback function
var dataNodeUpdateFn = function (pack) {
/**
* _node: <g> dom node
* _data: obj.bind
*/
var _node, _data, fn;
fn = function (bind) {
// update tree
var child = findItemFromHierarchy(pack._tree, _data).child;
child.bind = JSON.parse(JSON.stringify(bind));
// update node
var node = d3.select(_node);
node.datum().data.bind = bind;
node.call(appendText, node.datum(), pack._clipId);
node.select("circle")
.attr("r", 70)
.transition()
.duration(1000)
.ease(d3.easeBounceOut)
.attr("r", pack._r)
.attr("fill", d => d.data.bind.color);
_node = null;
_data = null;
};
fn.node = function (n) { _node = n; return fn; };
fn.data = function (d) { _data = d; return fn; };
return fn;
};
function TreeNode (child, parent, grandParent) {
this.child = child;
this.parent = parent;
this.grandParent = grandParent;
}
TreeNode.prototype = {
parentRemoveNode: function (childNode) {
var index = this.parent.children.findIndex(function (node) {
return node === childNode;
});
if (index < 0) {
return console.error("child node is NOT exist in parent node!");
}
this.parent.children.splice(index, 1);
},
removeChild: function () {
this.parentRemoveNode(this.child);
},
parentAdd: function (node) { // an node or an node array
if (node instanceof Array) {
Array.prototype.push.apply(this.parent.children, node);
}
else {
this.parent.children.push(node);
}
},
childAdd: function (node) { // an node or an node array
if (!this.child.children) {
return console.error("child is a prime node that can NOT contain any sub nodes.");
}
if (node instanceof Array) {
Array.prototype.push.apply(this.child.children, node);
}
else {
this.child.children.push(node);
}
}
};
function findItemFromHierarchy (hierarchyData, nodeData) {
// [currentNode, parentNode, grandParentNode, ... root]
var path = nodeData.ancestors(),
length = path.length,
i = length - 2,
child = hierarchyData,
grandParent,
parent,
name;
while (i >= 0) {
name = path[i].data.name;
grandParent = parent;
parent = child;
child = child.children.find(function (d) { return d.name === name; });
i--;
}
return new TreeNode(child, parent, grandParent);
}
// General pick for all PackTreee instance,
// only one PackTree is focused by PackTree.focusMe().
var ctrPick = (function () {
var pickedNodes = [],
pack = null;
// global keypress listener
var keyUp = function () {
// In case ctr is pressed while pressing other keys
if (d3.event.key.toUpperCase() !== "CONTROL") { return; }
// remove keyUp listener
d3.select(this).on("keyup.ctr", null);
var subChildren = [],
rootChildren = pack._tree.children;
// if picked node is less than 2 or all ROOT nodes are picked will do nothing
//if (pickedNodes.length < 2 || pickedNodes.length == rootChildren.length ) {
if (pickedNodes.length < 2) {
pack.gMain.selectAll(".svg__picked").classed("svg__picked", false);
return;
}
rootChildren.push({
name: uuid.prefix("o")(),
type: "or",
children: subChildren
});
pickedNodes.forEach(function (node) {
var name, index, target;
name = d3.select(node).datum().data.name;
index = rootChildren.findIndex(function (d) { return d.name === name; });
target = rootChildren.splice(index, 1)[0];
subChildren.push(target);
});
pack.gMain.selectAll(".svg__picked").classed("svg__picked", false);
pack.nodesUpdate(pack.rootInit());
};
var onKeydown = function () {
if (d3.event.key.toUpperCase() !== "CONTROL" || !pack) { return; }
pickedNodes = []; // clear content
d3.select(this).on("keyup.ctr", keyUp);
};
d3.select(window).on("keydown.ctr", onKeydown);
var click = function (d) {
/**
* 1) Ctrl key should be enabled
* 2) only dataNode is pickable
* 3) dataNode but its parent node is orNode and is not the ROOT
*/
if (
!d3.event.ctrlKey ||
d.data.type !== "data" ||
(d.parent.data.type == "or" && d.parent.data.name !== "ROOT")
) { return; }
var self = this, index;
if (d.parent.data.type == "and") {
self = pack.gMain
.selectAll(".svg__node")
.filter(function ($) { return $ === d.parent; })
.nodes()[0];
}
self.classList.toggle("svg__picked");
index = pickedNodes.findIndex(function (node) {
return node === self;
});
// if this node is included in pickNodes remove it,
// or push it into pickNodes.
index > -1 ? pickedNodes.splice(index, 1) : pickedNodes.push(self);
};
return {
clickFn: click,
clearNodes: function () {
pickedNodes = [];
return this;
},
setPack: function (v) {
if (!arguments.length) { return pack; }
pack = v;
return this;
}
};
})();
var nodeDragFn = function (pack) {
// nodeList is a live HTMLCollection object which contained all g-nodes under gMain
// nodesArray is an alternaltive to nodeList except needed repeately updated
var nodeList, nodesArray = [];
var targetNode = null, // node element, this element will comparing with matchNodes
triggeredNode = null, // one of "matchNodes" member which have insection with targetNode
relatedNodes = [], // targetNode's related nodes, if any.
matchNodes = []; // targetNode's match node array.
// calculation functions
var slice = Array.prototype.slice,
getDistance = function (c1, c2) {
return Math.sqrt((c1.x - c2.x) * (c1.x - c2.x) + (c1.y - c2.y) * (c1.y - c2.y));
},
innerReset = function () {
targetNode = null;
triggeredNode = null;
relatedNodes = [];
matchNodes = [];
nodesArray = [];
};
function onDragStart (d) {
// In case onEnd event don't trigged by user,
// reassure every thing on the same page.
innerReset();
// update nodesArray
nodeList = nodeList || pack.gMain.node().getElementsByClassName("svg__node");
nodesArray = slice.call(nodeList, 0);
var self = this, parent = d.parent, descendants;
if (parent.data.type == "or") {
targetNode = self;
descendants = d.descendants();
nodesArray.forEach(function (node) {
if (self === node) { return; }
var datum = d3.select(node).datum(),
children = parent.children; // or-Node's children
if (children.includes(datum)) { matchNodes.push(node); return; }
if (d.children && descendants.includes(datum)) { relatedNodes.push(node); }
});
}
else { // d.parent.data.type == "and"
targetNode = nodesArray.find(function (node) {
return d3.select(node).datum() === parent;
});
descendants = parent.descendants();
nodesArray.forEach(function (node) {
//if (self === node) { return; }
var datum = d3.select(node).datum(),
children = parent.parent.children; // or-Node's children
if (descendants.includes(datum)) { relatedNodes.push(node); return; }
if (children.includes(datum)) { matchNodes.push(node); }
});
}
} // onDragStart__End
function onDrag (d) {
var x = d3.event.x, y = d3.event.y,
dx = d3.event.dx, dy = d3.event.dy;
/**
* Detecting if targetNode moved outside of parentNode's domain,
* if it does stop coordinates updating by skipping onDrag loop.
* targetNode under the ROOT will be excepted.
*/
var moveCircle = (this === targetNode ? d : d3.select(targetNode).datum()),
fenceCircle = moveCircle.parent,
maxDis1 = fenceCircle.r - d.r - 1,
maxDis2 = fenceCircle.r - moveCircle.r - 1;
if (
(fenceCircle.data.name != "ROOT") &&
(
((moveCircle === d) && (getDistance(fenceCircle, {x: x, y: y}) > maxDis1)) ||
((moveCircle !== d) && (getDistance(fenceCircle, {x: moveCircle.x + dx, y: moveCircle.y + dy}) > maxDis2))
)
) {
return;
} // true: stop onDrag loop, code underbelow will not be excuted.
if (this === targetNode) {
Object.assign(d, {x: x, y: y});
d3.select(this).attr("transform", "translate(" + [x, y] + ")");
}
relatedNodes.forEach(function (node) {
var _node = d3.select(node), datum = _node.datum();
datum.x += dx;
datum.y += dy;
_node.attr("transform", "translate(" + [datum.x, datum.y] + ")");
});
/**
* This code block used for testing if targetNode have intersection with matchNodes.
* Find the triggeredNode which is the closest node to targetNode and should
* have intersection with it too.
*/
var wanted = matchNodes.map(function (node) {
var dm = d3.select(node).datum(),
dt = d3.select(targetNode).datum(),
distance = getDistance(dm, dt),
isIntersected = (distance <= Math.abs(dt.r + dm.r));
return {
node : node,
distance : distance,
isIntersected: isIntersected
};
})
.sort(function (a, b) { return a.distance - b.distance; })
.find(function (d) { return d.isIntersected; });
(triggeredNode && triggeredNode.classList.remove("svg__active"));
if (wanted) {
triggeredNode = wanted.node;
triggeredNode.classList.toggle("svg__active");
}
else {
triggeredNode = null;
}
d3.select(targetNode).classed("svg__active", true);
} // onDrag End
function onDragEnd (d) {
pack.gMain.selectAll(".svg__active").classed("svg__active", false);
if (!triggeredNode) { return innerReset(); }
var target = d3.select(targetNode).datum(),
trigger = d3.select(triggeredNode).datum(),
tt = findItemFromHierarchy(pack._tree, target),
tg = findItemFromHierarchy(pack._tree, trigger);
// 1) If two dataNodes come close merge them into a new andNode
if (target.data.type == "data" && trigger.data.type == "data") {
tt.removeChild();
tg.removeChild();
tt.parentAdd({
name: uuid.prefix("a")(),
type: "and",
children: [tt.child, tg.child]
});
}
// 2) If two andNodes come close tt grow into a bigger andNode and remove tg
else if (target.data.type == "and" && trigger.data.type == "and") {
tg.removeChild();
tt.childAdd(tg.child.children);
}
// 3) If two orNodes come close merge them into a new bigger orNode
else if (target.data.type == "or" && trigger.data.type == "or") {
tt.removeChild();
tg.removeChild();
tg.parentAdd({
name: uuid.prefix("o")(),
type: "or",
children: Array.prototype.concat([], tt.child.children, tg.child.children)
});
}
// 4) one node is dataNode and another is logic node(wrapper node)
// dataNode will be merged into that logic node
else if (
(target.data.type == "data" && trigger.data.type != "data") ||
(trigger.data.type == "data" && target.data.type != "data")
) {
if (target.data.type == "data") {
tt.removeChild();
tg.childAdd(tt.child);
}
else {
tg.removeChild();
tt.childAdd(tg.child);
}
}
// _______________ UNTESTED _______________
// 5) both are logic node but NOT the same type
// andNode will be merged into orNode
else {
if (target.data.type == "and" && trigger.data.type == "or") {
tt.removeChild();
tg.childAdd(tt.child);
}
else if (trigger.data.type == "and" && target.data.type == "or") {
tg.removeChild();
tt.childAdd(tg.child);
}
}
pack.nodesUpdate(pack.rootInit());
// release memory
innerReset();
} // onDragEnd End
return d3.drag()
.filter(function () {
return !d3.event.ctrlKey && !d3.event.button;
})
.on("start",onDragStart)
.on("drag", onDrag)
.on("end", onDragEnd);
}; // nodeDragFn END
function nodesUpdate (nodes) {
var u = this.gMain.selectAll(".svg__node")
.data(nodes.slice(1), function (d) { return d.data.name; });
var clipId = this._clipId;
u.enter()
.append("g")
.attr("class", d => { return "svg__node svg__node-" + d.data.type; })
.call(this.onNodeDrag)
.on("click", ctrPick.clickFn)
.on("dblclick", this.onDataNodeDBLClick)
.on("contextmenu", this.onNodeContextmenu)
.each(function (d) {
var self = d3.select(this);
// add circle backgorund, circle is event-responsible
self.append("circle")
.attr("fill", d.children ? null : d.data.bind.color)
.attr("r", d.r);
// add text content for leaf nodes
if (!d.children) {
self.call(appendText, d, clipId);
}
})
.attr("transform", "translate(" + [this._w / 2, this._h / 2] + ")")
.merge(u)
.sort(function (a, b) { return b.r - a.r; })
.each(function (d) {
// data rebine and change apprence
d3.select(this).select("circle").datum(d).attr("r", d.r);
// TODO: text should rebine if neccessory ... ...
})
.transition()
.duration(800)
.attr("transform", d => { return "translate(" + [d.x, d.y] + ")"; });
u.exit()
.attr("transform", d => { return "translate(" + [d.x, d.y] + ") scale(1) rotate(0)"; })
.transition()
.duration(400)
.attr("transform", d => { return "translate(" + [d.x, d.y] + ") scale(0.01) rotate(90)"; })
.remove();
}
function PackTree () {
this._tree = { name: "ROOT", type: "or", children: [] };
this._w = 1000;
this._h = 550;
this._r = 50;
this._clipId = uuid.prefix("clip")();
this._count = 0;
Object.defineProperty(this, "count", {
get: function () { return this._count; },
set: function (v) {
this._count = v;
if (this.onCount) { this.onCount(this._count); }
}
});
this.svg = null;
this.gMain = null;
this.vm = null; // pop window
var self = this;
this.dataNodeUpdate = dataNodeUpdateFn(this);
this.onNodeDrag = nodeDragFn(this);
this.onDataNodeDBLClick = function (d) {
if (d3.event.ctrlKey || d.data.type !== "data" || !self.vm) { return; }
self.vm.show(
self.dataNodeUpdate.data(d).node(this),
JSON.parse(JSON.stringify(d.data.bind))
);
}; // onDataNodeDBLClick END
this.onNodeContextmenu = function (d) {
d3.event.preventDefault();
var node = findItemFromHierarchy(self._tree, d);
// dataNode under the ROOT will be removed after user confirmed
if (node.parent.name == "ROOT" && node.child.type == "data") {
if (window.confirm("确认删除该节点?")) {
node.removeChild();
self.count -= 1;
return self.nodesUpdate(self.rootInit());
}
return;
}
// this code block will never be excuted as for
// orNode point-event is set to none.
if (node.child.type == "or") {
node.removeChild();
node.parentAdd(node.child.children);
return self.nodesUpdate(self.rootInit());
}
if (
(node.child.type == "data") &&
(node.parent.type == "and" || node.parent.type == "or")
) {
node.removeChild();
node.grandParent.children.push(node.child);
return self.nodesUpdate(self.rootInit());
}
}; // onNodeContextmenu END
}
// chainabel configure functions
PackTree.prototype.width = function (w) {
if (!arguments.length) { return this._w; }
this._w = w;
return this;
};
PackTree.prototype.height = function (h) {
if (!arguments.length) { return this._h; }
this._h = h;
return this;
};
PackTree.prototype.radius = function (r) {
if (!arguments.length) { return this._r; }
this._r = (r < 50 ? 50 : r);
return this;
};
PackTree.prototype.vForm = function (vm) {
if (!arguments.length) { return this.vm; }
this.vm = vm;
return this;
};
// Make ctrPick focus this instance so that subsequent
// ctr + mouse pick actions will be functional on this instance.
PackTree.prototype.focusMe = function () {
ctrPick.setPack(this);
return this;
};
PackTree.prototype.rootInit = rootInit;
PackTree.prototype.nodesUpdate = nodesUpdate;
PackTree.prototype.svgInit = function (id) {
var svg, gMain, vm = this.vm,
self = this,
clipId = this._clipId,
radius = this._r, w = this._w, h = this._h;
var formCallback = function (nodeData) {
self.addDataNode(nodeData);
};
var canvasDrag = d3.drag()
.subject(function () { return gMain.datum(); })
.on("drag", function () {
// if chart is empty diseabled drag event
if (!self._tree.children.length) { return; }
var x = d3.event.x, y = d3.event.y;
gMain.datum({x: x, y: y})
.attr("transform", "translate(" + [x, y] + ")");
});
svg = d3.select(id).append("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("version", "1.1")
.attr("tabIndex", 0) // focus: document.activeElement
.attr("class", "svg")
.attr("width", w)
.attr("height", h);
svg.append("defs")
.html('<clipPath id="' + clipId + '"><circle r="' + (radius - 2) +
'"></circle></clipPath>');
svg.append("rect")
.attr("class", "svg__bg")
.attr("width", w)
.attr("height", h)
.call(canvasDrag)
.on("contextmenu", function () {
d3.event.preventDefault();
if (vm) {
// vm.show(callbackFn, bind)
vm.show(formCallback);
}
});
gMain = svg.append("g")
.attr("class", "svg__main")
.datum({x: 0, y: 0})
.attr("transform", "translate(0,0)");
this.svg = svg;
this.gMain = gMain;
}; // svgInit END
PackTree.prototype.initialize = function (id, isFormPop) {
this.svgInit(id);
//this.rootInit()
if (isFormPop && this.vm && !this.vm.confirmCallbackFn) {
this.svg.select(".svg__bg").dispatch("contextmenu");
}
return this;
};
PackTree.prototype.addDataNode = function (bind) {
this._tree.children.push({
name: uuid.prefix("d")(),
type: "data",
bind: bind
});
this.count += 1;
this.nodesUpdate(this.rootInit());
};
PackTree.prototype.clearTree = function () {
this._tree.children = [];
this.count = 0;
this.nodesUpdate([]);
};
PackTree.prototype.toString = function (space) {
if (!space) { space = 0; }
return JSON.stringify(this._tree, null, space);
};
PackTree.prototype.toJSON = function () {
return JSON.parse(this.toString());
};
window.PackTree = PackTree;
})(window, d3);
{
"collections": ["abc", "bbc", "cba", "usa-today"],
"colors": ["#1f77b4", "#ff7f0e", "#d62728", "#9467bd"],
"doc": {
"name": "abc",
"items": [
{ "name": "Lorem ipsum", "type": "0" },
{ "name": "dolor sit", "type": "1" },
{ "name": "amet consectetur", "type": "3" },
{ "name": "adipisicing", "type": "2" },
{ "name": "elit", "type": "4" },
{ "name": "maxime", "type": "9" },
{ "name": "odio", "type": "0" },
{ "name": "sequi", "type": "1" },
{ "name": "mollitia", "type": "3" },
{ "name": "magni", "type": "2" },
{ "name": "facilis", "type": "4" },
{ "name": "veniam", "type": "9" },
{ "name": "Nesciunt", "type": "0" },
{ "name": "ipsam veniam", "type": "3" },
{ "name": "consequuntur", "type": "2" },
{ "name": "soluta", "type": "4" },
{ "name": "animi", "type": "9" },
{ "name": "obcaecati", "type": "1" }
]
}
}
(function (Vue, axios, window) {
"use strict";
// local preserved data object - cache
var db = {
collections: [], // ["abc", "bbc", "cba"]
colorMap: {}, // {"abc": "red", "bbc": "blue", ...}
documents: {},
randomColor: function () {
return "hsl(" + Math.floor(Math.random() * 360) + ",85%,55%)";
},
_initialGetUrl: "",
initialGetUrl: function (v) {
this._initialGetUrl = v;
return this;
},
_subsequentGetUrlFn: function (name) {
return console.error(
"请先设置subsequentGetUrlFn函数!! \n" +
"该函数接收一个collectionName参数\n" +
"通过这个参数返回获取documents列表的url地址。"
);
},
subsequentGetUrlFn: function (fn) {
this._subsequentGetUrlFn = fn;
return this;
},
// _failedFn will be excuted on intialGetUrl or subsequentGetUrl failed.
_failedFn: function (error) {
console.log(error);
},
failedFn: function (fn) {
this._failedFn = fn;
return this;
},
_successFn: function () {
console.error("请配置成功返回函数,先。");
},
successFn: function (fn) {
this._successFn = fn;
return this;
},
run: function () {
var self = this;
axios
.get(this._initialGetUrl)
.then(function (raw) {
var data = raw.data;
self.collections = data.collections;
data.collections.forEach(function (name, index) {
self.colorMap[name] = data.colors[index] || self.randomColor();
});
self.documents[data.doc.name] = {
items: data.doc.items,
querys: []
};
self._successFn(data);
})
.catch(this._failedFn);
}
};
// ------------- 高级检索初始化参数.xlsx -------------
// typeMap shared by queryGroup component and Vue instance as computer property
var typeMap = (function () {
// <option value="value">text</option>
var maps = [
{value: ">", text: ">"},
{value: "<", text: "<"},
{value: "=", text: "="},
{value: ">=", text: ">="},
{value: "<=", text: "<="}
];
var textMaps = [
{value: "同义包含", text: "同义包含"},
{value: "绝对包含", text: "绝对包含"},
{value: "以XXX开头", text: "以XXX开头"},
{value: "以XXX结尾", text: "以XXX结尾"},
{value: "等于", text: "等于"}
];
function checkFn (reg) {
return function (text) {
text = text.trim();
if (!text.length) { return false; }
return !!reg.exec(text);
};
}
function checkEmpty (text) {
text = text.trim();
return !!text.length;
}
// <input type="text" placeholder="eg: 123">
function getPlaceholder (type) {
switch (type) {
case "0": return "整型: 123";
case "1": return "浮点型: 0.23";
case "3": return "日期: 2019-08-01";
}
return "字符串: xxx";
}
// <input type="text" class="bg-integer">
function getClass (type) {
switch (type) {
case "0": return {"bg-integer": true};
case "1": return {"bg-float": true};
//case "3": return {"bg-date": true};
case "2":
case "4":
case "9":
return {"bg-string": true};
}
return {};
}
return {
"0": { // 整形
maps: maps,
relation: "and", /* default relation value */
logic: ">", /* default logic value */
check: checkFn(/^[+-]?[1-9]+\d?$/)
},
"1": { // 浮点型
maps: maps,
relation: "and",
logic: ">",
check: checkFn(/^[+-]?\d+(\.\d+)?$/)
},
"3": { // 日期: 2019-10-21
maps: maps,
relation: "and",
logic: ">",
check: checkFn(/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/)
},
"2": {
maps: textMaps,
relation: "and",
logic: "同义包含",
check: checkEmpty
},
"4": {
maps: textMaps.slice(1),
relation: "and",
logic: "绝对包含",
check: checkEmpty
},
"9": {
maps: textMaps.slice(1),
relation: "and",
logic: "绝对包含",
check: checkEmpty
},
getPlaceholder: getPlaceholder,
getClass: getClass
};
})();
var uuid = (function () {
var i = 0;
return function () { return "i" + i++; };
})();
Vue.config.devtools = false;
var collectionGroup = {
props: ["options"],
template: `
<div class="form-group">
<label class="combine">
<span class="combine__label py-1">集合选择:</span>
<select
@change="$emit('collection-change', $event.target.value)"
class="form-control form-control-sm combine__input">
<option v-for="opt in options" :value="opt">{{opt}}</option>
</select>
</label>
</div>
`
};
var documentGroup = {
props: ["documents"],
data: function () {
return {
filterString: ""
};
},
watch: {
documents: function () {
this.filterString = "";
}
},
computed: {
filteredOptions: function () {
var f = this.filterString.trim().toUpperCase();
if (!f.length) { return this.documents; }
return this.documents.filter(function (d) {
return d.name.toUpperCase().indexOf(f) > -1;
});
}
},
methods: {
onClick: function (name, type) {
this.$emit("add-query", name, type);
}
},
template: `
<div class="form-group">
<input type="text"
v-model="filterString"
class="form-control form-control-sm search-input"
placeholder="请输入筛选条件">
<ul class="list-search">
<li v-for="q in filteredOptions" :key="q.name"
@click="onClick(q.name, q.type)">{{q.name}}</li>
<div v-show="!documents.length"
class="text-center font-italic"
style="line-height: 130px;">Loading . . .</div>
</ul>
</div>
`
};
var queryGroup = {
props: ["items"],
data: function () {
return {
dy: 0, // offset y between mouse and tRow on mousedown event
rowTop: 0, // tRow initial y position
rowHeight: 0, // tRow height
// used for tedecting if the dragNode touch ceilling or floor
// if it does readjust scroll property
minY: 0,
maxY: 0,
// rowIndex on dragBefore and dragAfter
indexBefore: 0,
indexAfter: 0,
// table row node
dragNode: null,
// max scollable value if overflowed
scrollSapn: 0
};
},
computed: {
map: function () { return typeMap; }
},
methods: {
onChange: function (id, key, $event) { // for select controls
this.$emit("update-query", id, key, $event.target.value);
},
onTextChange: function (id, key, type, $event) { // for input controls
var value = $event.target.value.trim();
$event.target.value = value;
this.$emit("update-query", id, key, value);
if (this.map[type].check(value)) {
$event.target.classList.remove("border-danger");
}
else {
$event.target.classList.add("border-danger");
}
},
// following mouse event only fired on dragging behaviour
onMousedown: function (q, $event) {
// div.fix-table>table>tbody>tr>td>span.fix-table__handler
var row = $event.target.parentElement.parentElement,
div = this.$el, //row.closest(".fix-table"),
rowBBox = row.getBoundingClientRect(),
divBBox = div.getBoundingClientRect();
this.dragNode = row;
this.indexBefore = row.rowIndex;
this.indexAfter = row.rowIndex;
// Geometry calculation
this.rowTop = rowBBox.top;
this.rowHeight = rowBBox.height;
this.dy = $event.clientY - rowBBox.top;
this.minY = divBBox.top + div.querySelector("thead").getBoundingClientRect().height;
this.maxY = divBBox.bottom;
this.scrollSapn = div.scrollHeight - div.clientHeight;
row.style.backgroundColor = "lightgreen";
window.addEventListener("mousemove", this.onMousemove);
window.addEventListener("mouseup", this.onMouseup);
},
onMousemove: function (evt) {
evt.preventDefault();
var py = evt.clientY - this.dy;
var pre = this.dragNode.previousElementSibling,
next = this.dragNode.nextElementSibling;
if (pre && pre.getBoundingClientRect().top > py) {
pre.insertAdjacentElement("beforebegin", this.dragNode);
this.rowTop = this.dragNode.getBoundingClientRect().top;
}
else if (next && next.getBoundingClientRect().top < py) {
next.insertAdjacentElement("afterend", this.dragNode);
this.rowTop = this.dragNode.getBoundingClientRect().top;
}
py = evt.clientY - this.dy;
this.indexAfter = this.dragNode.rowIndex;
this.dragNode.style.transform = "translate(0px," + (py - this.rowTop) + "px)";
// auto scroll behaviour
if (py < this.minY && this.$el.scrollTop > 0) {
this.$el.scrollTop -= 10;
}
if ((py + this.rowHeight) > this.maxY && this.$el.scrollTop < this.scrollSapn) {
this.$el.scrollTop += 10;
}
},
onMouseup: function (evt) {
this.dragNode.style= "";
this.dragNode = null;
window.removeEventListener("mousemove", this.onMousemove);
window.removeEventListener("mouseup", this.onMouseup);
if (this.indexBefore !== this.indexAfter) {
this.$emit("move-query", this.indexBefore - 1, this.indexAfter - 1);
}
},
// this method is fired by parent Vue instance
validate: function () {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
// Input controls validate one by one,
// "change" event refer to component template
var inputs = this.$el.querySelectorAll("input");
for (var i = 0, len = inputs.length; i < len; i++) {
// invalid input will give a border-danger class on change event
inputs[i].dispatchEvent(evt);
}
// invalide input will be fenced with red border if any.
// Find one of them and shake this component to alert user.
var invalid = this.$el.querySelector(".border-danger");
if (invalid) {
invalid.scrollIntoView({ behavior: "smooth", block: "center" });
this.$el.classList.add("animated", "shake");
return false;
}
// all input control validate checking passed
return true;
}
},
template: `
<div class="fix-table">
<table class="table table-sm fix-header">
<colgroup>
<col style="width:100px;">
<col>
<col style="max-width:135px;">
<col>
</colgroup>
<thead>
<tr>
<th class="bg-gray font-weight-light sticky-top border-top-0">关系类型</th>
<th class="bg-gray font-weight-light sticky-top border-top-0">字段</th>
<th class="bg-gray font-weight-light sticky-top border-top-0">运算符</th>
<th class="bg-gray font-weight-light sticky-top border-top-0">检索值</th>
</tr>
</thead>
<transition-group tag="tbody" name="body-list">
<tr v-for="q in items" :key="q.id">
<td>
<span
@mousedown="onMousedown(q, $event)"
class="fix-table__handler">&nbsp;</span>
<select
class="form-control form-control-sm"
@change="onChange(q.id, 'relation', $event)"
style="display: inline-block; width: calc(100% - 20px);">
<option value="and">AND</option>
<option value="or">OR</option>
<option value="not">NOT</option>
</select>
</td>
<td>
<div class="roller">
<div class="roller-inner text-break">
<div class="roller__child roller__child-upper">{{q.name}}</div>
<div
@click="$emit('remove-query', q.id)"
class="roller__child roller__child-bottom">删 除</div>
</div>
</div>
</td>
<td>
<select
class="form-control form-control-sm"
@change="onChange(q.id, 'logic', $event)">
<option v-for="v in map[q.type].maps"
:value="v.value">{{v.text}}</option>
</select>
</td>
<td>
<input
:type="q.type == '3' ? 'date' : 'text'"
:placeholder="map.getPlaceholder(q.type)"
:value="q.query"
:class="map.getClass(q.type)"
class="form-control form-control-sm"
@change="onTextChange(q.id, 'query', q.type, $event)"
required>
</td>
</tr>
</transition-group>
</table>
</div>
`
};
var vm = new Vue({
// customed properties
confirmCallbackFn: null,
data: {
currentCollection: "", // EXPORT
collections: [],
documents: [],
queryItems: [],
errorMsg: "",
state: "insert" // insert/update "插入节点" | "更新节点"
},
computed: {
map: function () { return typeMap; },
db: function () { return db; }
},
watch: {
currentCollection: function (newColl, oldColl) {
var doc = this.db.documents, self = this;
// currentCollectin is switch to newColl
this.preservDataToDB(oldColl);
if (doc[newColl]) {
this.$set(this.$data, "documents", doc[newColl].items);
this.$set(this.$data, "queryItems", doc[newColl].querys);
}
else {
this.$set(this.$data, "documents", []);
this.$set(this.$data, "queryItems", []);
axios.get(this.db._subsequentGetUrlFn(newColl))
.then(function (d) {
doc[d.data.name] = {
items: d.data.items,
querys: []
};
self.$set(self.$data, "documents", doc[newColl].items);
self.$set(self.$data, "queryItems", doc[newColl].querys);
})
.catch(this.db._failedFn);
}
}
},
methods: {
addQuery: function (name, type) {
this.queryItems.unshift({
id: this.uuid(),
type: type,
name: name,
relation: this.map[type].relation,
logic: this.map[type].logic,
query: ""
});
},
findQuery: function (id) {
return this.queryItems.findIndex(function (item) {
return item.id == id;
});
},
updateQuery: function (id, key, value) {
var index = this.findQuery(id);
this.queryItems[index][key] = value;
},
removeQuery: function (id) {
var index = this.findQuery(id);
this.queryItems.splice(index, 1);
},
moveQuery: function (fromIndex, toIndex) {
var i = fromIndex, j = toIndex,
node = this.queryItems[fromIndex];
if (fromIndex < toIndex) {
while (i < j) {
this.queryItems[i] = this.queryItems[i + 1];
i++;
}
}
else {
while (i > j) {
this.queryItems[i] = this.queryItems[i - 1];
i--;
}
}
// Trigger reactive feature and place the item at right index
this.queryItems.splice(toIndex, 1, node);
//console.log(this.queryItems.map(d => d.name));
},
uuid: uuid,
toExport: function (collectionName) {
if (!collectionName) { collectionName = this.currentCollection; }
return {
collection: this.currentCollection,
color: this.db.colorMap[collectionName],
querys: this.queryItems.map(function (d) {
return {
id : d.id,
name : d.name,
type : d.type,
relation: d.relation,
logic : d.logic,
query : d.query
};
})
};
},
// preserve data action only when close dialoge or collection switch
preservDataToDB: function (collectionName) {
if (!this.queryItems.length) { return; }
if (!collectionName) { collectionName = this.currentCollection; }
this.db.documents[collectionName].querys = this.toExport(collectionName).querys;
},
// Ref: _animation.scss
onAnimationEnd: function (evt) {
evt.target.classList.remove("animated", "shake", "bounceInDown", "bounceInUp", "bounceOutDown");
},
onEscPressed: function (evt) {
if (evt.key.toUpperCase() === "ESCAPE") {
this.close();
}
},
show: function (callbackFn, bind) {
window.addEventListener("keydown", this.onEscPressed);
this.errorMsg = "";
this.state = "insert";
if (callbackFn) {
this.confirmCallbackFn = callbackFn;
}
if (bind) {
this.currentCollection = bind.collection;
this.$set(this.$data, "queryItems", bind.querys);
this.state = "update";
}
this.$el.style.display = "block";
document.body.classList.add("modal-open");
this.$el.querySelector(".modal-dialog")
.classList.add("animated", "bounceInDown");
},
close: function () {
window.removeEventListener("keydown", this.onEscPressed);
this.$el.style.display = "none";
document.body.classList.remove("modal-open");
this.confirmCallbackFn = null;
this.preservDataToDB(); // finally preserve data
},
confirm: function () {
this.errorMsg = "";
// 1) filter items must not less than 1
if (!this.queryItems.length) {
this.errorMsg = "请点击左侧列表,添加筛选条件到右侧。";
this.$refs.query.$el.classList.add("animated", "shake");
return;
}
// 2) query string is required
if (!this.$refs.query.validate()) {
this.errorMsg = "输入不得为空,且必须按照指定数据类型输入数据。";
return;
}
// 3) get all information then fire callback if any
if (this.confirmCallbackFn) {
this.confirmCallbackFn(this.toExport());
}
// 4) close dialoge finnaly
this.close();
}
},
components: {
collectionGroup: collectionGroup,
documentGroup: documentGroup,
queryGroup: queryGroup
},
template: `
<div class="modal c-modal"
@animationend="onAnimationEnd"
novalidate
style="background-color: rgba(0,0,0,0.2);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header p-2">
<h6 class="modal-title text-white">检索条件</h6>
<button @click="close" type="button" class="close c-close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-4">
<collection-group
ref="collection"
@collection-change="currentCollection = $event"
:options="collections">
</collection-group>
<document-group
ref="doc"
:documents="documents"
@add-query="addQuery">
</document-group>
</div>
<div class="col-8">
<query-group
ref="query"
@update-query="updateQuery"
@move-query="moveQuery"
@remove-query="removeQuery"
:items="queryItems">
</query-group>
</div>
</div>
<div class="row" :class="{'d-none': !errorMsg}">
<div class="col">
<span class="text-danger">{{errorMsg}}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="confirm" class="btn btn-sm btn-secondary c-bg-primary">
{{state == "insert" ? "插入节点" : "更新节点"}}
</button>
<button @click="close" class="btn btn-sm btn-secondary">取消</button>
</div>
</div>
</div>
</div>
`
});
window.vm = vm;
})(Vue, axios, window);
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app" id="app" v-cloak>
<div class="sence">
<div ref="board" class="board">
<div ref="adopted" class="board__face board__face-front"></div>
<div ref="rejected" class="board__face board__face-back"></div>
</div>
</div>
<div class="controls controls__topLeft">
<div class="btn-group btn-group-toggle">
<button type="button"
class="btn"
:class="{active: current == 'adopted', 'btn-primary': current == 'adopted', 'btn-secondary': current != 'adopted'}"
@click="swap('adopted')">
纳入标准 <span class="badge badge-light">{{adopted}}</span>
</button>
<button type="button"
class="btn"
:class="{active: current == 'rejected', 'btn-primary': current == 'rejected', 'btn-secondary': current != 'rejected'}"
@click="swap('rejected')">
排除标准 <span class="badge badge-light">{{rejected}}</span>
</button>
</div>
</div>
</div>
<div id="form"></div><!--弹出框-->
<script src="https://cdn.jsdelivr.net/npm/babel-polyfill@6.26.0/dist/polyfill.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@4.13.0/build/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js"></script>
<script src="chart.js"></script><!--图形-->
<script src="form.js"></script><!--弹出框-->
<script>
var width = 1000, height = 500;
var adopted = new PackTree().width(width).height(height).focusMe(),
rejected = new PackTree().width(width).height(height);
var app = new Vue({
el: "#app",
data: {
adopted: 0,
rejected: 0,
current: "adopted"
},
methods: {
swap: function ($event) {
if (this.current === $event) { return; }
this.current = $event;
if ($event == "adopted") { adopted.focusMe(); }
if ($event == "rejected") { rejected.focusMe(); }
this.$refs.board.classList.toggle("rotate");
}
},
mounted: function () {
var self = this;
adopted.onCount = function (count) { self.adopted = count; };
rejected.onCount = function (count) { self.rejected = count; };
}
});
vm.db
.initialGetUrl("./collections.json") // 1)这是第一条GET请求
.subsequentGetUrlFn(function (name) { // 2)这是后续GET请求url配置函数
return "./" + name + ".json";
})
.failedFn(function (error) {
console.log(error);
})
.successFn(function (data) {
vm.$set(vm.$data, "collections", data.collections);
vm.currentCollection = data.doc.name;
vm.$mount("#form");
adopted.vForm(vm).initialize(app.$refs.adopted, true);
rejected.vForm(vm).initialize(app.$refs.rejected);
})
.run();
d3.select(window.parent.document.querySelector("iframe"))
.style("width", "1000px")
.style("height", "500px");
</script>
</body>
[v-cloak] {
visibility: hidden;
}
body {
font-family: MicrosoftYaHei;
font-size: 14px;
color: #333;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAJklEQVQ4T2NkIBPs3r37PyOZehlGNZMYcqMBNrQCDBRfJLoYrhwACOgv4H6BjQYAAAAASUVORK5CYII=") repeat;
}
.c-bg-primary {
background-color: #3366cc;
}
.c-modal-lg {
width: 800px;
}
.c-modal-md {
width: 400px;
}
.c-modal .modal-body {
padding-bottom: 0;
}
.c-modal .modal-header {
background-color: #3bbdb0;
border-radius: 0;
}
.c-modal .modal-footer {
padding: 0.5em;
border-top: none;
}
.modal-header .c-close {
padding: 1.2rem 1rem 1rem;
}
.c-close {
opacity: 1;
font-size: 1rem;
}
.c-close span {
background-color: #ff3da2;
color: white;
font-weight: normal;
display: inline-block;
width: 18px;
height: 18px;
border-radius: 50%;
}
.search-input {
margin-bottom: 0px;
border-radius: 0;
border: 1px solid #ccc;
border-bottom-style: dashed;
position: relative;
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjIgMjIiIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iMTQiIHkyPSIxNCIgc3Ryb2tlPSJncmF5IiBzdHJva2Utd2lkdGg9IjEiPjwvbGluZT48Y2lyY2xlIGN4PSI4IiBjeT0iOCIgcj0iNCIgc3Ryb2tlPSJncmF5IiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9InRyYW5zcGFyZW50Ij48L2NpcmNsZT48L3N2Zz4=");
background-position: bottom -3px right 0%;
background-repeat: no-repeat;
}
.list-search {
padding: 0;
list-style: none;
height: 134px;
font-size: 12px;
overflow-y: auto;
color: #333;
border: 1px solid #ccc;
border-top: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.list-search li {
padding: 0.2em 0.5em;
cursor: pointer;
text-indent: 0px;
overflow: hidden;
-webkit-transition: text-indent 0.5s;
transition: text-indent 0.5s;
}
.list-search li:hover {
background-color: rgba(110, 2, 38, 0.774);
text-indent: 15px;
color: white;
}
.combine {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.combine__label {
color: #333;
min-width: 85px;
}
.combine__input[disabled] {
color: lightgray;
background-color: #ededed;
}
.fix-table {
height: 215px;
overflow: auto;
border: 1px solid #ccc;
}
.fix-table__handler {
display: inline-block;
width: 12px;
position: relative;
background-color: #80808030;
border-radius: 3px;
cursor: row-resize;
}
.fix-table__handler::before,
.fix-table__handler::after {
content: " ";
position: absolute;
border-top: 1px solid #999;
border-bottom: 1px solid #999;
}
.fix-table__handler:hover::before,
.fix-table__handler:hover::after {
border-top-color: lightgreen;
border-bottom-color: lightgreen;
}
.fix-table__handler::before {
width: 40%;
height: 80%;
top: 10%;
left: 30%;
}
.fix-table__handler::after {
width: 64%;
height: 30%;
top: 35%;
left: 18%;
}
.fix-table__indicator {
display: none;
padding-left: 1em;
padding-right: 1em;
}
.fix-table__indicator td {
height: 20px;
outline: 1px solid yellow;
outline-style: dashed;
background-color: lightgray;
text-align: center;
}
.text-break {
position: relative;
max-width: 100px;
min-height: 35px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.text-break .btn-remove {
display: none;
}
.text-break:hover .btn-remove {
display: block;
}
.btn-remove {
position: absolute;
left: 0;
top: 0;
width: 24px;
height: 24px;
background-color: red;
color: white;
border: none;
border-radius: 12px;
line-height: 1.15;
}
.bg-gray {
background-color: #e4e9f1;
}
.bg-integer,
.bg-float,
.bg-string,
.bg-date {
background-repeat: no-repeat;
background-position: top right;
}
.bg-integer {
background-image: url("data:image/png;base64,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");
}
.bg-float {
background-image: url("data:image/png;base64,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");
}
.bg-string {
background-image: url("data:image/png;base64,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");
}
.bg-date {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABYUlEQVRYR+2XMU7EMBBF396Alp6OAu0JgJICKkqugYSggAKJhlvQ0iBRIrGLOMJuRY+4BWgWB1lZj/OdRBuKuIxnJn/+jL/HE/z1ChyE7RlwmLFNbUn+k0zQ79pezjYVRvIfARgDN8B+VO+KFY/C+vc6/aq/9dXcjBfAbhRFDeC1T4n/0ozVTL3AbRlY+eUAeMdILYHknwPgUawCkPxTAExwrEFSy4TJMsutIv8UgELB62b+rwDkqOuW5rr3XyljBkq1viuoVTOPANowUAmMooyNt20pgFgHNgrAbsy9cGNuhe7zsjsDHoBL4K5p4FEZMADXwoRk4F6AT+Ck4Zi0PgXVXZBi4Ba4Ao6B500DmALvwBtwJIhE7wxchJqfA/dDAHgKdd8GvoYA8AHsBHUV/v87iamnIA7oNWGuOVOAWgNQslNsRgBrDAw2kMTjs1K7Pm1m1dPstPY66vMnXqwl8PgDdABwnMZa/0gAAAAASUVORK5CYII=");
}
.roller {
width: 100px;
height: 28px;
overflow: hidden;
cursor: pointer;
}
.roller__child {
height: 28px;
line-height: 28px;
}
.roller-inner {
-webkit-transform: translate(0px, 0px);
transform: translate(0px, 0px);
-webkit-transition: -webkit-transform 0.5s;
transition: -webkit-transform 0.5s;
transition: transform 0.5s;
transition: transform 0.5s, -webkit-transform 0.5s;
}
.roller:hover .roller-inner {
-webkit-transform: translate(0px, -25px);
transform: translate(0px, -25px);
}
.roller__child-bottom {
background-color: tomato;
text-align: center;
color: white;
}
.svg {
display: block;
font-size: 12px;
}
.svg__bg {
fill: transparent;
pointer-events: all;
}
.svg__link {
stroke: white;
stroke-width: 1;
}
.svg__node {
stroke-width: 1;
cursor: pointer;
}
.svg__picked circle,
.svg__active circle {
stroke-width: 2;
stroke-dasharray: 2 6;
stroke: #e43939;
-webkit-filter: url(#packTreeBlur);
filter: url(#packTreeBlur);
}
.svg__active.svg__node-or circle,
.svg__active.svg__node-and circle {
-webkit-filter: unset;
filter: unset;
}
.svg__node-or,
.svg__node-and {
fill: transparent;
stroke-width: 1;
}
.svg__node-or {
stroke: #555;
}
.svg__node-and {
pointer-events: none;
}
.svg__text {
text-anchor: middle;
fill: white;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
@-webkit-keyframes bounceOutDown {
20% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
40%,
45% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(0, 2000px, 0);
transform: translate3d(0, 2000px, 0);
}
}
@keyframes bounceOutDown {
20% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
40%,
45% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(0, 2000px, 0);
transform: translate3d(0, 2000px, 0);
}
}
@-webkit-keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
-webkit-transform: translate3d(0, -3000px, 0);
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, 25px, 0);
transform: translate3d(0, 25px, 0);
}
75% {
-webkit-transform: translate3d(0, -10px, 0);
transform: translate3d(0, -10px, 0);
}
90% {
-webkit-transform: translate3d(0, 5px, 0);
transform: translate3d(0, 5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
-webkit-transform: translate3d(0, -3000px, 0);
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, 25px, 0);
transform: translate3d(0, 25px, 0);
}
75% {
-webkit-transform: translate3d(0, -10px, 0);
transform: translate3d(0, -10px, 0);
}
90% {
-webkit-transform: translate3d(0, 5px, 0);
transform: translate3d(0, 5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@-webkit-keyframes bounceInUp {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
-webkit-transform: translate3d(0, 3000px, 0);
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
75% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
90% {
-webkit-transform: translate3d(0, -5px, 0);
transform: translate3d(0, -5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceInUp {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
-webkit-transform: translate3d(0, 3000px, 0);
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
75% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
90% {
-webkit-transform: translate3d(0, -5px, 0);
transform: translate3d(0, -5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@-webkit-keyframes shake {
from,
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
10%,
30%,
50%,
70%,
90% {
-webkit-transform: translate3d(-10px, 0, 0);
transform: translate3d(-10px, 0, 0);
}
20%,
40%,
60%,
80% {
-webkit-transform: translate3d(10px, 0, 0);
transform: translate3d(10px, 0, 0);
}
}
@keyframes shake {
from,
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
10%,
30%,
50%,
70%,
90% {
-webkit-transform: translate3d(-10px, 0, 0);
transform: translate3d(-10px, 0, 0);
}
20%,
40%,
60%,
80% {
-webkit-transform: translate3d(10px, 0, 0);
transform: translate3d(10px, 0, 0);
}
}
.shake {
-webkit-animation-name: shake;
animation-name: shake;
}
.bounceInDown {
-webkit-animation-name: bounceInDown;
animation-name: bounceInDown;
}
.bounceInUp {
-webkit-animation-name: bounceInUp;
animation-name: bounceInUp;
}
.bounceOutDown {
-webkit-animation-name: bounceOutDown;
animation-name: bounceOutDown;
}
.bounce-enter-active {
-webkit-animation: bounceInDown 0.5s;
animation: bounceInDown 0.5s;
}
.bounce-leave-active {
-webkit-animation: bounceInUp 0.5s;
animation: bounceInUp 0.5s;
}
.body-list-enter-active {
-webkit-transition: all 0.5s;
transition: all 0.5s;
}
.body-list-leave-active {
-webkit-transition: all 0.3s;
transition: all 0.3s;
}
.body-list-enter, .body-list-leave-to {
opacity: 0;
-webkit-transform: translateY(30px);
transform: translateY(30px);
}
.app {
position: relative;
}
.controls {
position: absolute;
}
.controls__topLeft {
top: 0.5em;
left: 0.5em;
}
.controls__bottomRight {
bottom: 1em;
right: 1em;
}
.sence {
-webkit-transform: perspective(400px);
transform: perspective(400px);
width: 100%;
height: 100vh;
overflow: hidden;
}
.board {
position: relative;
width: 100%;
height: 100vh;
-webkit-transform-origin: -50% 50%;
transform-origin: -50% 50%;
-webkit-transition: -webkit-transform 1s;
transition: -webkit-transform 1s;
transition: transform 1s;
transition: transform 1s, -webkit-transform 1s;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
}
.board.rotate {
-webkit-transform: translateX(200%) rotateY(180deg);
transform: translateX(200%) rotateY(180deg);
}
.board__face {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
.board__face-back {
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
/*# sourceMappingURL=style.css.map */
{"name":"usa-today","items":[{"name":"3Lorem ipsum","type":"0"},{"name":"3dolor sit","type":"1"},{"name":"3amet consectetur","type":"3"},{"name":"3adipisicing","type":"2"},{"name":"3elit","type":"4"},{"name":"3maxime","type":"9"},{"name":"3odio","type":"0"},{"name":"3sequi","type":"1"},{"name":"3mollitia","type":"3"},{"name":"3magni","type":"2"},{"name":"3facilis","type":"4"},{"name":"3veniam","type":"9"},{"name":"3Nesciunt","type":"0"},{"name":"3ipsam veniam","type":"3"},{"name":"3consequuntur","type":"2"},{"name":"3soluta","type":"4"},{"name":"3animi","type":"9"},{"name":"3obcaati","type":"1"}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment