如何创建 Konva-React 上下文菜单
How to create a Konva-React context menu
据我所知,Konva 没有 easy/built 方法来创建右键单击对象的上下文菜单。我正忙于一个需要使用上下文菜单的项目,所以我想我应该创建自己的菜单。
不用说我对 Konva 还很陌生,所以我希望 SO 上的人可能有更多经验来帮助我克服最后的障碍。
我已经创建了一个沙箱,位于 HERE
要求是:
- 对象应该是可拖动的。 (我从 Konva 沙盒中复制了一个工作示例。)
- 右键单击对象时应显示上下文菜单。
- 上下文菜单应该是动态的,因此允许多个项目,每个项目在被点击时执行自己的回调。
- 选择完成后,应关闭上下文菜单。
到目前为止,我大部分都做对了,但我遇到的问题是:
- 我不知道如何将鼠标悬停在一个上下文菜单项上,使其突出显示,然后移动到下一个应突出显示的项,并将旧的恢复为原始设置。
- 移出上下文菜单会重新绘制整个对象。我不明白为什么。
- 点击一项会触发两项的回调。为什么?我定位了被点击的特定菜单项,但同时获得了两者?
- 这一点与其说是一个错误,不如说是我不确定如何继续:如果用户在对象上多次右键单击,我将如何防止创建多个上下文菜单?从概念上讲,我知道我可以使用上下文菜单的名称搜索图层(?)中的任何项目并将其关闭,但是我不知道如何执行此操作。
如有任何帮助,我将不胜感激。提前致谢。
恐怕不是在 React 中,而是在纯 JS 中,但它照亮了您必须做的一些事情。
点击粉色圆圈,然后选择选项2,点击子选项2。
需要更多工作的领域:
- 通过JSON
传送菜单配置数据
- 使添加回调成为 class
中的一个方法
- 在隐藏上添加超时以允许鼠标手抖动
- 如何在用户鼠标移出或单击另一个选项时处理隐藏子菜单
- 添加显示和隐藏动画
// Set up the canvas / stage
var stage = new Konva.Stage({container: 'container1', width: 600, height: 300});
// Add a layer some sample shapes
var layer = new Konva.Layer({draggable: false});
stage.add(layer);
// draw some shapes.
var circle = new Konva.Circle({ x: 80, y: 80, radius: 30, fill: 'Magenta'});
layer.add(circle);
var rect = new Konva.Rect({ x: 80, y: 80, width: 60, height: 40, fill: 'Cyan'});
layer.add(rect);
stage.draw();
// that is the boring bit over - now menu fun
// I decided to set up a plain JS object to define my menu structure - could easily receive from async in JSON format. [Homework #1]
var menuData = { options: [
{key: 'opt1', text: 'Option 1', callBack: null},
{key: 'opt2', text: 'Option 2', callBack: null,
options: [
{key: 'opt2-1', text: 'Sub 1', callBack: null},
{key: 'opt2-2', text: 'Sub 2', callBack: null}
]
},
{key: 'opt3', text: 'Option 3', callBack: null},
{key: 'opt4', text: 'Option 4', callBack: null}
]};
// Define a menu 'class' object.
var menu = function(menuData) {
var optHeight = 20; // couple of dimension constants.
var optWidth = 100;
var colors = ['white','gold'];
this.options = {}; // prepare an associative list accessible by key - will put key into the shape as the name so we can can get from click event to this entry
this.menuGroup = new Konva.Group({}); // prepare a canvas group to hold the option rects for this level. Make it accessible externally by this-prefix
var _this = this; // put a ref for this-this to overcome this-confusion later.
// recursive func to add a menu level and assign its option components.
var addHost = function(menuData, hostGroup, level, pos){ // params are the data for the level, the parent group, the level counter, and an offset position counter
var menuHost = new Konva.Group({ visible: false}); // make a canvas group to contain new options
hostGroup.add(menuHost); // add to the parent group
// for every option at this level
for (var i = 0; i < menuData.options.length; i = i + 1 ){
var option = menuData.options[i]; // get the option into a var for readability
// Add a rect as the background for the visible option in the menu.
option.optionRect = new Konva.Rect({x: (level * optWidth), y: (pos + i) * optHeight, width: optWidth, height: optHeight, fill: colors[0], stroke: 'silver', name: option.key });
option.optionText = new Konva.Text({x: (level * optWidth), y: (pos + i) * optHeight, width: optWidth, height: optHeight, text: ' ' + option.text, listening: false, verticalAlign: 'middle'})
console.log(option.optionText.height())
option.optionRect
.on('mouseover', function(){
this.fill(colors[1])
layer.draw();
})
.on('mouseleave', function(){
this.fill(colors[0])
layer.draw();
})
// click event listener for the menu option
option.optionRect.on('click', function(e){
var key = this.name(); // get back the key we stashed in the rect so we can get the options object from the lookup list
if (_this.options[key] && (typeof _this.options[key].callback == 'function')){ // is we found an option and it has a real function as a callback then call it.
_this.options[key].callback();
}
else {
console.log('No callback for ' + key)
}
})
menuHost.add(option.optionRect); // better add the rect and text to the canvas or we will not see it
menuHost.add(option.optionText);
_this.options[option.key] = option; // stash the option in the lookup list for later retrieval in click handlers.
// pay attention Bond - if this menu level has a sub-level then we call into this function again.
if (option.options){
var optionGroup = addHost(option, menuHost, level + 1, i) // params 3 & 4 are menu depth and popout depth for positioning the rects.
// make an onclick listener to show the sub-options
option.callback = function(e){
optionGroup.visible(true);
layer.draw();
}
}
}
return menuHost; // return the konva group
}
// so - now we can call out addHost function for the top level of the menu and it will recurse as needed down the sub-options.
var mainGroup = addHost(menuData, this.menuGroup, 0, 0);
// lets be nice and make a show() method that takes a position x,y too.
this.show = function(location){
location.x = location.x - 10; // little offset to get the group under the mouse
location.y = location.y - 10;
mainGroup.position(location);
mainGroup.show(); // notice we do not draw the layer here - leave that to the caller to avoid too much redraw.
}
// and if we have a show we better have a hide.
this.hide = function(){
mainGroup.hide();
}
// and a top-level group listener for mouse-out to hide the menu. You might want to put a timer on this [Homework #3]
mainGroup.on('mouseleave', function(){
this.hide();
layer.draw();
})
// end of the menu class object.
return this;
}
// ok - now we can get our menu data turned into a menu
var theMenu = new menu(menuData);
layer.add(theMenu.menuGroup); // add the returned canvas group to the layer
layer.draw(); // and never forget to draw the layer when it is time!
//
// now we can add some arbitrary callbacks to some of the options.
//
// make a trivial function to pop a message when we click option 1
var helloFunc = function(){
alert('hello')
}
// make this the callback for opt1 - you can move this inside the menu class object as a setCallback(name, function()) method if you prefer [homework #2]
theMenu.options['opt1'].callback = helloFunc;
// put a function on sub2 just to show it works.
theMenu.options['opt2-2'].callback = function(){ alert('click on sub-2') };
// and the original reason for this - make it a context menu on a shape.
circle.on('click', function(e){
theMenu.show({x: e.evt.offsetX, y: e.evt.offsetY});
layer.draw();
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.min.js"></script>
<div id='container1' style="width: 300px, height: 200px; background-color: silver;"></div>
不确定我是否迟到了,但我会使用 React Portals, theres a example about it on the react-konva page: https://konvajs.github.io/docs/react/DOM_Portal.html
我分叉了你的沙盒如何完成:https://codesandbox.io/s/km0n1x8367
据我所知,Konva 没有 easy/built 方法来创建右键单击对象的上下文菜单。我正忙于一个需要使用上下文菜单的项目,所以我想我应该创建自己的菜单。
不用说我对 Konva 还很陌生,所以我希望 SO 上的人可能有更多经验来帮助我克服最后的障碍。
我已经创建了一个沙箱,位于 HERE
要求是:
- 对象应该是可拖动的。 (我从 Konva 沙盒中复制了一个工作示例。)
- 右键单击对象时应显示上下文菜单。
- 上下文菜单应该是动态的,因此允许多个项目,每个项目在被点击时执行自己的回调。
- 选择完成后,应关闭上下文菜单。
到目前为止,我大部分都做对了,但我遇到的问题是:
- 我不知道如何将鼠标悬停在一个上下文菜单项上,使其突出显示,然后移动到下一个应突出显示的项,并将旧的恢复为原始设置。
- 移出上下文菜单会重新绘制整个对象。我不明白为什么。
- 点击一项会触发两项的回调。为什么?我定位了被点击的特定菜单项,但同时获得了两者?
- 这一点与其说是一个错误,不如说是我不确定如何继续:如果用户在对象上多次右键单击,我将如何防止创建多个上下文菜单?从概念上讲,我知道我可以使用上下文菜单的名称搜索图层(?)中的任何项目并将其关闭,但是我不知道如何执行此操作。
如有任何帮助,我将不胜感激。提前致谢。
恐怕不是在 React 中,而是在纯 JS 中,但它照亮了您必须做的一些事情。
点击粉色圆圈,然后选择选项2,点击子选项2。
需要更多工作的领域:
- 通过JSON 传送菜单配置数据
- 使添加回调成为 class 中的一个方法
- 在隐藏上添加超时以允许鼠标手抖动
- 如何在用户鼠标移出或单击另一个选项时处理隐藏子菜单
- 添加显示和隐藏动画
// Set up the canvas / stage
var stage = new Konva.Stage({container: 'container1', width: 600, height: 300});
// Add a layer some sample shapes
var layer = new Konva.Layer({draggable: false});
stage.add(layer);
// draw some shapes.
var circle = new Konva.Circle({ x: 80, y: 80, radius: 30, fill: 'Magenta'});
layer.add(circle);
var rect = new Konva.Rect({ x: 80, y: 80, width: 60, height: 40, fill: 'Cyan'});
layer.add(rect);
stage.draw();
// that is the boring bit over - now menu fun
// I decided to set up a plain JS object to define my menu structure - could easily receive from async in JSON format. [Homework #1]
var menuData = { options: [
{key: 'opt1', text: 'Option 1', callBack: null},
{key: 'opt2', text: 'Option 2', callBack: null,
options: [
{key: 'opt2-1', text: 'Sub 1', callBack: null},
{key: 'opt2-2', text: 'Sub 2', callBack: null}
]
},
{key: 'opt3', text: 'Option 3', callBack: null},
{key: 'opt4', text: 'Option 4', callBack: null}
]};
// Define a menu 'class' object.
var menu = function(menuData) {
var optHeight = 20; // couple of dimension constants.
var optWidth = 100;
var colors = ['white','gold'];
this.options = {}; // prepare an associative list accessible by key - will put key into the shape as the name so we can can get from click event to this entry
this.menuGroup = new Konva.Group({}); // prepare a canvas group to hold the option rects for this level. Make it accessible externally by this-prefix
var _this = this; // put a ref for this-this to overcome this-confusion later.
// recursive func to add a menu level and assign its option components.
var addHost = function(menuData, hostGroup, level, pos){ // params are the data for the level, the parent group, the level counter, and an offset position counter
var menuHost = new Konva.Group({ visible: false}); // make a canvas group to contain new options
hostGroup.add(menuHost); // add to the parent group
// for every option at this level
for (var i = 0; i < menuData.options.length; i = i + 1 ){
var option = menuData.options[i]; // get the option into a var for readability
// Add a rect as the background for the visible option in the menu.
option.optionRect = new Konva.Rect({x: (level * optWidth), y: (pos + i) * optHeight, width: optWidth, height: optHeight, fill: colors[0], stroke: 'silver', name: option.key });
option.optionText = new Konva.Text({x: (level * optWidth), y: (pos + i) * optHeight, width: optWidth, height: optHeight, text: ' ' + option.text, listening: false, verticalAlign: 'middle'})
console.log(option.optionText.height())
option.optionRect
.on('mouseover', function(){
this.fill(colors[1])
layer.draw();
})
.on('mouseleave', function(){
this.fill(colors[0])
layer.draw();
})
// click event listener for the menu option
option.optionRect.on('click', function(e){
var key = this.name(); // get back the key we stashed in the rect so we can get the options object from the lookup list
if (_this.options[key] && (typeof _this.options[key].callback == 'function')){ // is we found an option and it has a real function as a callback then call it.
_this.options[key].callback();
}
else {
console.log('No callback for ' + key)
}
})
menuHost.add(option.optionRect); // better add the rect and text to the canvas or we will not see it
menuHost.add(option.optionText);
_this.options[option.key] = option; // stash the option in the lookup list for later retrieval in click handlers.
// pay attention Bond - if this menu level has a sub-level then we call into this function again.
if (option.options){
var optionGroup = addHost(option, menuHost, level + 1, i) // params 3 & 4 are menu depth and popout depth for positioning the rects.
// make an onclick listener to show the sub-options
option.callback = function(e){
optionGroup.visible(true);
layer.draw();
}
}
}
return menuHost; // return the konva group
}
// so - now we can call out addHost function for the top level of the menu and it will recurse as needed down the sub-options.
var mainGroup = addHost(menuData, this.menuGroup, 0, 0);
// lets be nice and make a show() method that takes a position x,y too.
this.show = function(location){
location.x = location.x - 10; // little offset to get the group under the mouse
location.y = location.y - 10;
mainGroup.position(location);
mainGroup.show(); // notice we do not draw the layer here - leave that to the caller to avoid too much redraw.
}
// and if we have a show we better have a hide.
this.hide = function(){
mainGroup.hide();
}
// and a top-level group listener for mouse-out to hide the menu. You might want to put a timer on this [Homework #3]
mainGroup.on('mouseleave', function(){
this.hide();
layer.draw();
})
// end of the menu class object.
return this;
}
// ok - now we can get our menu data turned into a menu
var theMenu = new menu(menuData);
layer.add(theMenu.menuGroup); // add the returned canvas group to the layer
layer.draw(); // and never forget to draw the layer when it is time!
//
// now we can add some arbitrary callbacks to some of the options.
//
// make a trivial function to pop a message when we click option 1
var helloFunc = function(){
alert('hello')
}
// make this the callback for opt1 - you can move this inside the menu class object as a setCallback(name, function()) method if you prefer [homework #2]
theMenu.options['opt1'].callback = helloFunc;
// put a function on sub2 just to show it works.
theMenu.options['opt2-2'].callback = function(){ alert('click on sub-2') };
// and the original reason for this - make it a context menu on a shape.
circle.on('click', function(e){
theMenu.show({x: e.evt.offsetX, y: e.evt.offsetY});
layer.draw();
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.min.js"></script>
<div id='container1' style="width: 300px, height: 200px; background-color: silver;"></div>
不确定我是否迟到了,但我会使用 React Portals, theres a example about it on the react-konva page: https://konvajs.github.io/docs/react/DOM_Portal.html
我分叉了你的沙盒如何完成:https://codesandbox.io/s/km0n1x8367