Как создать контекстное меню Konva-React

Насколько мне известно, с Konva непросто создать контекстное меню для щелчка правой кнопкой мыши по объектам. Я занят работой над проектом, который требует использования контекстных меню, поэтому решил создать свое собственное.

Излишне говорить, что я новичок в Konva, поэтому я надеялся, что у кого-то из SO может быть больше опыта, чтобы помочь мне преодолеть последние препятствия.

Я создал песочницу, расположенную ЗДЕСЬ

Требования:

  1. Объект должен быть перетаскиваемым. (Я скопировал рабочий пример из песочницы Konva.)
  2. При щелчке правой кнопкой мыши по объекту должно отображаться контекстное меню.
  3. Контекстное меню должно быть динамическим, что позволяет использовать несколько элементов, каждый из которых выполняет свой обратный вызов при нажатии.
  4. После того, как выбор сделан, контекстное меню должно быть закрыто.

Пока что я понял большую часть этого правильно, но вот с чем я борюсь:

  1. Я не могу понять, как навести курсор на один пункт контекстного меню, выделить его, а затем перейти к следующему, который должен быть выделен, а старый восстановлен до исходных настроек.
  2. При выходе из контекстного меню перерисовывается весь объект. Не понимаю почему.
  3. Щелчок по одному элементу вызывает обратные вызовы обоих элементов. Почему? Я нацелен на конкретный пункт меню, по которому был нажат, но получаю и то, и другое?
  4. Это не столько ошибка, сколько то, что я не уверен, что делать дальше: как мне предотвратить создание нескольких контекстных меню, если пользователь щелкнет правой кнопкой мыши несколько раз на объекте? Концептуально я понимаю, что могу найти любые элементы в слое (?) С именем контекстного меню и закрыть его, однако я понятия не имею, как это сделать.

Буду признателен за любую помощь. Заранее спасибо.

Интересный проект - попадется ряд полезных приемов. Если у вас нет готовых решений, вы можете разделить это на несколько небольших вопросов по каждой теме в вашем списке. Лучшая форма для потребления SO.

Vanquished Wombat 30.11.2018 09:03

Наверное, для создания контекстных меню будет лучше использовать внешнюю библиотеку. То, что он работает через обычный DOM, вместо рисования на холсте. Может быть проще управлять.

lavrton 30.11.2018 14:21

@lavrton Я думал об этом, но я совершенно не уверен, как это сделать, поскольку я не знаю, как смешивать холст с компонентом реакции на этом уровне. Проблема в том, что библиотеки, на которые я смотрел, используют поставщик контекста HOC, который должен обернуть элемент, в данном случае холст, где будет использоваться контекстное меню. Затем мне нужно отобразить контекстное меню для события. Не уверен, как это работает на холсте. Поэтому я прибег к рисованию собственного контекстного меню.

Ebbs 30.11.2018 15:11
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
3
2 767
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Боюсь, что не в React, а в простом JS, но он проливает свет на то, что вам придется делать.

Щелкните розовый кружок, затем выберите вариант 2 и выберите подвариант 2.

Области, требующие дополнительной работы:

  • доставить данные конфигурации меню через JSON
  • сделать добавление обратных вызовов методом внутри класса
  • добавить тайм-аут на шкуре, чтобы руки мыши дрожали
  • как обрабатывать скрытые подменю, когда пользователь наводит курсор мыши или щелкает другой параметр
  • добавить анимацию раскрытия и скрытия

// 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.info(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.info('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-konva: https://konvajs.github.io/docs/react/DOM_Portal.html

Я раздвоил вашу песочницу, как это сделать: https://codesandbox.io/s/km0n1x8367

Другие вопросы по теме