正在为无限词典制作单词思维导图功能,实现无限单词导图,无限思维画布。目前制作到第三步,实现节点移动与编辑:
节点移动与编辑
Details
第一步,搜索 github。
一个是比较完善的,基于普通dom,用canvas做小预览图,值得借鉴。而另一个是vue-mindmap,用的是svg结合foreignobject嵌套普通dom,虽然功能不完整、在移动端没有惯性滑动且卡顿,但css外观上更符合预期,于是基于其h5产物,改造。
第二步,简化
简化是为了理清dom结构,理解代码原理。
保留两三个节点即可。连线是svg的q指令,即指令两点连线,然后用另一个点,或数个点控制曲线弯曲。节点旁边还有竖线关联一些细节。
第三步,古法手搓html
h5其实很简单 ——
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="MDict JS">
<meta name="robots" content="noindex">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes, maximum-scale=10">
<style>
.mindmap-svg {
height: 100%;
width: 100%;
}
.mindmap-svg:focus {
outline: none;
}
.mindmap-node>a, .mindmap-node>wordnode {
background: #f5f5f5;
border-radius: 10px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
color: #212121;
display: inline-block;
font-family: 'Raleway';
font-size: 22px;
margin: 0 auto;
padding: 15px;
text-align: center;
text-decoration: none;
transition: background-color .2s, color .2s ease-out;
}
.mindmap-node>a[href]:hover {
background-color: #f57c00;
color: #fff;
cursor: pointer;
}
.mindmap-node--editable>a {
pointer-events: none;
}
.mindmap-subnode-group {
align-items: center;
border-left: 4px solid #9e9e9e;
display: flex;
margin-left: 15px;
padding: 5px;
}
.mindmap-subnode-group a {
color: #212121;
font-family: 'Raleway';
font-size: 16px;
padding: 2px 5px;
}
.mindmap-connection {
fill: transparent;
stroke: #9e9e9e;
stroke-dasharray: 10px 4px;
stroke-width: 3px;
}
.mindmap-emoji {
height: 24px;
vertical-align: bottom;
width: 24px;
}
.reddit-emoji {
border-radius: 50%;
}
mark{
background: orange;
color: black;
}
html,body{
padding:0;
margin:0;
}
body{
overflow: scroll;
}
foreignObject { overflow: visible; }
.items-container{
font-size: 12px;
color: rgb(255, 255, 255);
display: flex;
flex-grow: 1;
user-select:none;
}
.statusbar-item{
color: rgb(255, 255, 255);
display: inline-block;
line-height: 22px;
height: 100%;
vertical-align: top;
max-width: 40vw;
padding-top:5px;
padding-bottom:5px;
}
.statusbar-item>a{
line-height: 22px;
text-decoration: none;
cursor: pointer;
display: flex;
height: 100%;
padding: 0 5px;
white-space: pre;
align-items: center;
text-overflow: ellipsis;
overflow: hidden;
outline-width: 0;
margin-right: 3px;
margin-left: 3px;
color: #000;
}
.statusbar-item>a>span{
text-align: center;
font-size: 14px;
padding-top:10px;
padding-bottom:10px;
}
</style>
<!--script src='https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js'> </script-->
</head>
<body style='background:#fff'>
<svg id="map" class="mindmap-svg" viewBox="0 0 1500 2500">
<g transform="translate(0,0) scale(1)">
<!--path class="mindmap-connection" d="M 487 -259 Q 443 241 , 402 540">
</path>
<path class="mindmap-connection" d="M 402 540 Q 298 590 , 154 639">
</path>
<path class="mindmap-connection" d="M 154 639 Q 135 696 , 183 753">
</path>
<path class="mindmap-connection" d="M 955 417 Q 717 362 , 746 138">
</path-->
<foreignObject class="mindmap-node mindmap-node--editable" width="125" height="55" x="426" y="14">
<wordnode>
<a id="node-undefined">python<img class="mindmap-emoji" title="wiki" ></a>
<title></title>
</wordnode>
</foreignObject>
<foreignObject class="mindmap-node mindmap-node--editable" width="1" height="1" x="0" y="0">
<wordnode>
<div style="whitespace:nowrap;">basics
asdsa asd
asdsaddddd
asdsadads
</div>
<video preload="none" src='https://www.runoob.com/try/demo_source/movie.mp4' controls>
</wordnode>
</foreignObject>
<!--foreignObject class="mindmap-subnodes" width="113" height="176" x="546" y="3">
<div class="mindmap-subnode-group" style="border-left-color: rgba(255, 189, 10, 1.0)">
<a><img class="mindmap-emoji reddit-emoji" title="reddit"></a>
<div></div>
</div>
<div class="mindmap-subnode-group" style="border-left-color: rgba(255, 189, 10, 1.0)">
<a>source <img class="mindmap-emoji" title="github"></a>
<div></div>
</div>
<div class="mindmap-subnode-group" style="border-left-color: rgba(255, 189, 10, 1.0)">
<a>source <img class="mindmap-emoji" title="github"></a>
<div></div>
</div>
</foreignObject-->
<path id="location" transform="translate(0,0) scale(1)" d="M0,-7c-3.87,0 -7,3.13 -7,7s3.13,7 7,7 7,-3.13 7,-7 -3.13,-7 -7,-7zM15.65,-1.75c-0.81,-7.3 -6.6,-13.09 -13.9,-13.9L1.75,-19.25h-3.5v3.61C-9.05,-14.84 -14.84,-9.05 -15.65,-1.75L-19.25,-1.75v3.5h3.61c0.81,7.3 6.6,13.09 13.9,13.9L-1.75,19.25h3.5v-3.61c7.3,-0.81 13.09,-6.6 13.9,-13.9L19.25,1.75v-3.5h-3.61zM0,12.25c-6.77,0 -12.25,-5.48 -12.25,-12.25s5.48,-12.25 12.25,-12.25 12.25,5.48 12.25,12.25 -5.48,12.25 -12.25,12.25z">
</path>
<foreignObject id="editbar" class="mindmap-node mindmap-node--editable" width="1" height="1" x="500" y="500" >
<wordnode style="transform:scale(1.5);padding-top:0;padding-bottom:0;">
<div class="items-container" style="width:auto;">
<div class="statusbar-item" id="move">
<a tabindex="-1" role="button" style="cursor:all-scroll">
<span class="codicon codicon-bookmark">✥ 移动</span>
</a>
</div>
<div class="statusbar-item" >
<a tabindex="-1" role="button" style="">
<span class="codicon codicon-bookmark" id="edit">🖊 编辑</span>
</a>
</div>
<div class="statusbar-item" >
<a tabindex="-1" role="button" style="">
<span class="codicon codicon-bookmark">🗑️ 删除</span>
</a>
</div>
</div>
</wordnode>
</foreignObject>
</g>
</svg>
<script>
function ge(e,p){return (p||document).getElementById(e)};
window.debug = function(a,b,c,d,e){var t=[a,b,c,d,e];for(var i=5;i>=0;i--){if(t[i]===undefined)t[i]='';else break}console.log("%c wordmap ","color:#333!important;background:#0FF;",t[0],t[1],t[2],t[3],t[4])}
var ua=navigator.userAgent, mobile=(/Android|webOS|iPhone|iPod|BlackBerry/i.test(ua));
var map = ge("map");
var loca = ge("location");
var editbar = ge("editbar");
var move = ge("move");
var focaNode;
var mapX = 1500, mapY = 2500;
map.style.background="#0000ff22"
loca.style.display='none';
editbar.style.display='none';
var ff = mapX / document.documentElement.clientWidth;
function placeEditbar(node) {
var obj = node.parentNode;
var x = parseInt(obj.getAttribute("x"));
//x+=node.offsetWidth/3;
x+=55;
var y = parseInt(obj.getAttribute("y"));
y+=node.offsetHeight;
y+=19;
editbar.setAttribute("x", x);
editbar.setAttribute("y", y);
}
map.addEventListener('click', function(e){
var p=e.path,node=0;
if(!p && e.composedPath) p=e.composedPath();
debug(p);
if(p) for(var i=0;(t=p[i])&&i++<99;)
{
if(t.tagName==='WORDNODE') {
node = t;
break;
}
if(t.tagName==='FOREIGNOBJECT' && t.childNodes[0].tagName==='WORDNODE') {
node = t.childNodes[0];
break;
}
}
if(node) {
loca.style.display='none';
if(node.parentNode==editbar) return;
focaNode = node;
debug(node);
placeEditbar(node);
editbar.style.display='';
return;
}
var x = xy(e).clientX, y=e.clientY;
debug("mousedown", x, y, e);
var xx = (x+document.documentElement.scrollLeft) * ff;
var yy = (y+document.documentElement.scrollTop) * ff;
loca.style.display='';
editbar.style.display='none';
loca.setAttribute("transform", "translate("+parseInt(xx)+","+parseInt(yy)+")");
if(focaNode)
focaNode.setAttribute("contenteditable", "false")
});
function xy(e){
if(e.clientX==undefined)
e.clientX=e.changedTouches[0].clientX;
if(e.clientY==undefined)
e.clientY=e.changedTouches[0].clientY;
return e;
};
var moving = 0, orgX, orgY, nodeX, nodeY, stopScroll;
function dragMove(e){
var x = xy(e).clientX, y=e.clientY;
//debug('touchmove', x, y);
if(moving) {
moving.setAttribute("x", nodeX+(x-orgX)*ff);
moving.setAttribute("y", nodeY+(y-orgY)*ff);
placeEditbar(focaNode);
}
if(mobile && !stopScroll) {
document.documentElement.style.overflow='hidden';
stopScroll=1;
}
e.preventDefault();
e.stopPropagation();
}
function dragAbort(e){
var x = xy(e).clientX, y=e.clientY;
//debug('touchend', x, y);
moving = 0;
document.removeEventListener(mobile?'touchmove':'mousemove', dragMove, true);
document.removeEventListener(mobile?'touchend':'mouseup', dragAbort, true);
if(mobile)
{
document.documentElement.style.overflow='scroll';
stopScroll=0;
}
}
move.addEventListener(mobile?'touchstart':'mousedown', function(e){
var x = xy(e).clientX, y=e.clientY;
orgX = x;
orgY = y;
moving = focaNode?focaNode.parentNode:0;
document.addEventListener(mobile?'touchend':'mouseup', dragAbort, true);
document.addEventListener(mobile?'touchmove':'mousemove', dragMove, true);
if(moving) {
nodeX = parseInt(moving.getAttribute("x"));
nodeY = parseInt(moving.getAttribute("y"));
}
//debug('touchstart', x, y, nodeX, nodeY);
});
ge("edit").onclick = function(){
debug(focaNode);
focaNode.setAttribute("contenteditable", "true");
focaNode.focus();
}
</script>
</body>
</html>