Нажатие на элемент SVG останавливает запуск ключевых событий

#javascript #svg #events

Вопрос:

Я создаю игру, используя PIXI для графики и Мифрил для некоторых элементов пользовательского интерфейса. Я создаю прослушиватели событий для различных входов (WASD для движения, щелчки мыши для действия). Я визуализирую SVG как часть пользовательского интерфейса (с тегом <объект>), и когда я нажимаю на этот SVG, в браузере должна быть некоторая «переориентация», так как ключевые входные данные прекращают запуск событий. В частности, я даже не могу использовать клавишу, которая должна включать/выключать SVG!

Я не могу просто отключить взаимодействие с элементом, так как мне нужно иметь возможность подключать прослушиватели событий к определенным элементам в SVG. Как мне исправить это поведение «переориентации»? Как ни странно, я также не могу запустить onclick при нажатии svg (после передачи функции onclick элементу <объект>).

Ответ №1:

Ваш SVG, загружаемый в элемент <object>, как и в <object><iframe>, имеет свой собственный документ, который изолирован от документа владельца.

События, происходящие в одном контексте, не будут видны другому. Это специально, вы бы не хотели, чтобы случайное объявление на странице могло читать все, что напечатано в его встраивателе, и аналогично другим способом.

Поэтому здесь простое решение состоит в том, чтобы добавить ваш SVG в качестве встроенного элемента непосредственно в основной документ:

 onkeydown = (evt) => {
  evt.preventDefault();
  console.log( "pressed", evt.key );
} 
 Press any key or click on the rect<br>
<svg viewBox="0 0 100 100" width="100">
  <rect fill="green" width="100" height="100"/>
  <script>
    document.querySelector("rect")
      .addEventListener("click", (evt) =>
        evt.target.setAttribute("fill", "#"   ((Math.random()*0xFFFFF)|0) )
      );
  </script>
</svg> 

Если ваше svg-изображение и ваш документ имеют одинаковое происхождение, вы можете получить доступ к <объекту><объекту> .contentDocument и прикрепить события туда даже из основного документа.

 document.querySelector("object").addEventListener("load", (evt) => {
  const doc = evt.target.contentDocument;
  // add your event here
  doc.addEventListener("keydown", handler);
});
 

Пример аутсорсинга, потому что iframes StackSnippet с нулевым происхождением не могут быть одного и того же происхождения…


Для документов перекрестного происхождения более сложным способом было бы создать узел между обоими контекстами и сообщить другой стороне, когда произошло событие.

Есть предложение, которое должно позволить это, хотя оно уже давно не обновлялось, и оно скорее нацелено на использование OffscreenCanvas в веб-работниках в качестве варианта использования. Но то, как все происходило в последнем обновлении, позволяло надеяться, что с этой ситуацией тоже можно будет справиться.

Я написал хакерскую реализацию, основанную на этом последнем предложении, вот пример использования:

 const svg_content = `
<svg viewBox="0 0 100 100" width="100" xmlns="http://www.w3.org/2000/svg">
  <rect fill="green" width="100" height="100"/>
  <!-- load the script from both sides
    note that here we load the "cross-origin" version,
    because StackSnippet's null-origined iframes make this object cross-origin...
  -->
  <script href="https://cdn.jsdelivr.net/gh/Kaiido/EventPort.js@cross-origin/EventPort.js"/>
  <script>
    document.querySelector("rect")
      .addEventListener("click", (evt) =>
        evt.target.setAttribute("fill", "#"   ((Math.random()*0xFFFFF)|0) )
      );
    // "parent" will listen to Events firing on "window"
    const eventPort = window.createEventPort();
    parent.postMessage( eventPort, "*", [eventPort] );
  </script>
</svg>
`;

document.querySelector("object")
  .data = URL.createObjectURL( new Blob( [ svg_content ], { type: "image/svg xml" } ) );

// we wait for the SVG document to send our EventPort object
window.addEventListener("message", (evt) => {
  const eventPort = evt.eventPorts[ 0 ];
  // now we can listen to the events occuring there
  eventPort.addEventListener( "keydown", (evt) => {
    console.log( "pressed in svg document", evt.key );
  });
  eventPort.addEventListener( "click", (evt) => {
    console.log( "clicked in svg document" );
  });
});
addEventListener("keydown", (evt) => {
  console.log( "pressed in main document", evt.key );
}) 
 <!-- load the script from both sides -->
<script src="https://kaiido.github.io/EventPort.js/EventPort.js"></script>
Press any key or click on the rect<br>
<object></object> 

Но на самом деле мой сценарий является хакерским, я написал это только в качестве доказательства концепции, чтобы помочь лучше разработать возможный API, и поскольку он перезаписывает множество глобальных файлов, он вполне может сломать другие материалы на вашей странице.
Так что, если вы можете, идите простым путем: встроите свой svg.

Комментарии:

1. Поскольку я использую Мифрил для управления состоянием пользовательского интерфейса, я не хочу много встраивать в основной документ. Благодаря вашему ответу мне удалось частично решить свои проблемы, вызвав document.body.focus() в прослушивателе событий щелчка на элементе svg, что приводит к немедленному переключению фокуса при щелчке, однако мне придется регистрировать аналогичные обработчики для каждого события, которое меняет фокус. Знаете ли вы, можно ли в первую очередь предотвратить изменение фокуса, но при этом разрешить обработку щелчков и так далее? Я полагаю, что нет, и в этом случае, знаете ли вы более простой способ сделать то, что я описал?

2. Вам абсолютно необходимо, чтобы событие click в svg находилось на заполненной части элементов или где-либо в svg было бы нормально? Если последнее, то используйте <img> для визуализации вашего svg.

3. Я использую svg специально, потому что хотел иметь возможность прикреплять разных слушателей к разным частям изображения.

4. У вас все еще есть элемент <карта><карта> , в противном случае, я думаю, я дал все, что мог придумать в своем ответе.