ポップアップしたウィンドウに要素の参照(DOMノード)を送りたかったので、この記事を書いた。
Web MessagingはDOMノードを送れない
JavaScriptでは、window.postMessageを使うことで、ポップアップやiframeなどの別ウィンドウとWeb Messaging(HTML5)を介して通信することができる。
子ウィンドウ、親ウィンドウへの参照はそれぞれwindow.open()
とwindow.opener
で持つことができるから、window.postMessage
と併せてあらゆるデータのやり取りが自由にできそうなものである。
しかし、window.postMessage
では送ることができないデータがある。
MDN web docsには、message
について
他のウィンドウに送られるデータ。データは the structured clone algorithm に従ってシリアル化されます。つまり、手動でシリアル化することなく様々なデータオブジェクトを渡すことができます。
(window.postMessage - Web API | MDN)
と書かれている。
この「the structured clone algorithm」(日本語「構造化複製アルゴリズム」)という部分が重要で、この中で「構造化複製で動作しないもの」というのが示されいる。
- Function オブジェクトは構造化複製アルゴリズムでは複製されません。複製しようとすると DATA_CLONE_ERR 例外が送出されます。
- DOM ノードを複製しようとしても同様に DATA_CLONE_ERR 例外が送出されます。
つまり、例えばdocument.querySelector()
などを使えば要素の参照を取得できるが、このような参照はWeb Messagingで送ることができない。
実際にwindow.postMessage
でDOMノードを送ろうとすると、
Uncaught DOMException: Failed to execute 'postMessage' on 'Window': HTMLButtonElement object could not be cloned.
のようなエラーが発生する。
ウィンドウ間で互いのDOMの参照はできるのだから、DOMノードも送れるべきである。
そこで、住所のように、テキストでDOMツリーにおけるノードの位置を表現できる方法を探していると、「Xpath」という言語構文を見つけた。
Xpathとは
Xpathとは、XMLやHTMLのようなツリー状の階層構造を持つ文書で、様々なノードの位置や情報を表すことができる記法のことである。URLのようなパス表記ができることが特徴。
Introduction to using XPath in JavaScript | MDN
記法についてはこちらの記事が詳しいが、ざっくり言うと、例えばbody直下の<h1>
にアクセスするためのXpathは
/html/body/h1
となる。
また、2番目の<div>
の3番目の<span>
にアクセスするためのXpathは
/html/body/div[2]/span[3]
といったように表すことができる。
要素のXpathを取得して送信する
まず、送るためにはDOMノードのXpathを取得する必要がある。
以下の記事のコードを使用して送信側のスクリプトを書いた。
ブラウザ上のクリックした要素のXpathを取得する - Qiita
let childWindow = window.open('child.html', 'child', 'width=300,height=400,scrollbars');
// クリックされたらその要素のXpathを子ウィンドウにpostMessageする
window.addEventListener('click', (e) => {
childWindow.postMessage(getXpath(e.target), 'http://localhost');
});
/* https://qiita.com/narikei/items/fb62b543ca386fcee211 */
function getXpath(element) {
if(element && element.parentNode) {
var xpath = getXpath(element.parentNode) + '/' + element.tagName;
var s = [];
for(var i = 0; i < element.parentNode.childNodes.length; i++) {
var e = element.parentNode.childNodes[i];
if(e.tagName == element.tagName) {
s.push(e);
}
}
if(1 < s.length) {
for(var i = 0; i < s.length; i++) {
if(s[i] === element) {
xpath += '[' + (i+1) + ']';
break;
}
}
}
return xpath.toLowerCase();
} else {
return '';
}
}
要素のXpathを受け取って参照する
受信側は以下のようになる。
let parent = window.opener.document;
function receiveMessage(e) {
if (e.origin !== "http://localhost") {return}
parent.evaluate(e.data, parent, null, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue.innerHTML = 'ok!';
}
window.addEventListener("message", receiveMessage);
Introduction to using XPath in JavaScript | MDN
成功すると、親ウィンドウでクリックした要素に「ok!」と表示されるはず。
これで、親ウィンドウから子ウィンドウに送られたXpathを使って、子ウィンドウが親ウィンドウのDOMを参照し、当該要素にアクセスすることが可能になった。もちろんその逆も可能である。