jQuery: Cytoscape.js お試せた9(コンテキストメニューな extension)

文脈依存お品書きんぐる。

今回も前回からの続き物。

「コンテキストメニューな UI extension」としてCytoscape.js 公式から2つ紹介されてる。かっちょいいヤツふつうのヤツの2つ。ドキュメントをざざっと眺めつつデモも動かしつつ軽くあんばいを見てたんだけど、どっちもちょっとずつ不十分。

そもそもかっちょいい方はメニューアイテムが3つくらいまでなら実用になるだろうが、4つ目以降くらいから気分は相当に微妙だろう。ふつうの方は、残念ながらまだサブメニューが使えない。かっちょいい方は CDN にいるがふつうの方はいない。

でどっちの不十分さが「マシか」といえばこれは「圧倒的にふつうのヤツ」。かっちょいい方の構造は、セレクタの位置がおかしい。つまり「コンテキストメニューを出せるのはノードの方である」とか「コンテキストメニューを出せるのはノードのうち class=”a” である」は出来ても、「ノード全体ではこのコンテキストメニューを」「ノードのうち class=”a” にはさらにこのメニューを」が出来ない、はず。サンプルを読んだだけの理解なのでほんとかどうかはわからんけれど、これが本当なら全然使い物にならない(場合が多い)だろう。

結局かっちょいい方は「項目が多いとびみょーだ」が既に気分的には相当致命傷な上にこうなので、まぁこっちはいいかなと。ふつうのヤツだけでいいや。

出来たのよねこんなで:

/cytoscape-context-menus.js、/cytoscape-context-menus.css はワタシのサーバに置いたヤツ
 1 <link rel="stylesheet" href="/cytoscape-context-menus.css">
 2 <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
 3 <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
 4 <!--...-->
 5 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.2.7/cytoscape.min.js"></script>
 6 <!-- if you want to use 'dagre' layout
 7 <script type="text/javascript" src="https://cdn.rawgit.com/cpettitt/dagre/v0.7.4/dist/dagre.min.js"></script>
 8 <script type="text/javascript" src="https://cdn.rawgit.com/cytoscape/cytoscape.js-dagre/1.5.0/cytoscape-dagre.js"></script>
 9 -->
10 <script type="text/javascript" src="/cytoscape-context-menus.js"></script>
11 
12 <!-- ... -->
13 <div id="cy"></div>
14 <!-- ... -->
15 <script>
16 /* ... cy は初期化済みとして ... */
17 
18     cy.contextMenus({
19         menuItems: [
20             {
21                 "id": "jump_to_actor_page",
22                 "content": "jump to actor page",
23                 "selector": "node.actor",
24                 "onClickFunction": function (event) {
25                     var actor = event.target.data("actor");
26                     var url = (new MalUrlBuilder(false)).get_person(actor["malid"], actor["canonical_name"]);
27                     try { // your browser may block popups
28                         window.open(url);
29                     } catch(e) { // fall back on url change
30                         window.location.href = url;
31                     }
32                 },
33             },
34             {
35                 "id": "jump_to_character_page",
36                 "content": "jump to character page",
37                 "selector": "node.actor",
38                 "onClickFunction": function (event) {
39                     var character = event.target.data("character");
40                     var url = (new MalUrlBuilder(false)).get_character(character["malid"], character["canonical_name"]);
41                     try { // your browser may block popups
42                         window.open(url);
43                     } catch(e) { // fall back on url change
44                         window.location.href = url;
45                     }
46                 },
47                 "hasTrailingDivider": true,
48             },
49             {
50                 "id": "jump_to_anime_page",
51                 "content": "jump to anime page",
52                 "selector": "node",
53                 "onClickFunction": function (event) {
54                     var anime = event.target.data("anime");
55                     var url = (new MalUrlBuilder(false)).get_characters(anime["malid"], anime["canonical_name"]);
56                     try { // your browser may block popups
57                         window.open(url);
58                     } catch(e) { // fall back on url change
59                         window.location.href = url;
60                     }
61                 },
62             },
63         ]});
64 
65 </script>

最初ディバイダなしで始めたが、すぐに hasTrailingDivider の存在に気付いた。あぁいいね、さすが。event.target.dataは cytoscape 使ってれば「お決まり」。これでノードのデータをほじくってお好きにどうぞ、てこと。今回のは単にデータを使うだけの例だけれど、こういうメニューを使うなら、「ノードを隠したりとか色々」な例をばやりたいよね、てことだけど、それは後日。実際それをやりたいのですよ、「見たいノードだけ見たい」からね。

てわけでいつものように実際に動くやつ:


アニメのノードの場合は MAL のアニメページに飛ぶメニュー、キャラクターのノードの場合は、「声優ページ」「キャラクターページ」それぞれに飛ぶメニュー「も」追加されてる、てことね。(セレクタが柔軟なんでこういうことが簡単に出来るのがありがたいわよね。)

なお、「cytoscape.js ネタ」とは無関係にこのアプリケーションは地味に進化し続けてます。


2018-02-02追記:
cytoscape.js-context-menus に関して「サブメニューが使えない」以外のことで後から色々気付いたことがあるので、まとめて追記しておく。

まず一つ目が、「ノード・エッジ以外の領域でコンテキストメニューをトリガーする」のは実は「滅茶苦茶簡単」なのだが、ドキュメントからはひっじょーにわかりずらい。これは coreAsWell を真にするだけ。「as well」でなく「core only」にしたければ、つまり「ノード・エッジ上以外でのみ」としたければ selector を指定しないだけ。言われればめっちゃ簡単だが、なぜかドキュメントが異様に説明下手。

二つ目。「メニュー項目を選択しないで戻る」手段がわかりにくい。出来ないのではなくて結構気付きにくくて、ノード・エッジや以外を「タップ」すればメニューは消えてくれる。ただし、「戻れねー」と騒ぎ立てながら閉じないまま右クリックを続けると無限増殖する。これはまぁバグみたいなもんだと思うが、回避のしようがないんじゃないかと思う。

三つ目。これが非常に悩ましいのだが、非常にカジュアルに作ってあるというか。真剣味が若干欠けてるというか。普通この手のはこれは「やってくれて当然」と思うんだけどね、何かというと「いつなんどきでもマウス位置を左上にしてメニューを描画してくれる」ことしかしてくれない。何が起きるかというと、右端やら下端やらでコンテキストメニューを起こすと「恐怖のスクロールバーが突如挨拶してくる」。あー、もう、普通そんくらいの回り込み処理は入れるよなぁ…。これへの措置はもう「苦肉」るしか術はなく、「苦肉」にはいくらかバリエーションはあるとは思うけれど、ワタシはもう「だったらスクロールバーを常に出しとくしかないわなぁ」という措置にしてしまった:

 1 <head>
 2 <style>
 3 body {
 4   overflow-x: scroll;
 5   overflow-y: scroll;
 6 }
 7 #cy {
 8   width: 100%;  /* MUST */
 9   height: 90%;  /* MUST */
10 }
11 /* ... */
12 </style>
13 </head>
14 <body>
15 <!-- ... -->
16 <div id="cy"></div>
17 <div id="status-bar-text" style="height: 10pt; font-size: 9pt;"></div>
18 <div id="progress-bar" style="height: 10pt;"></div>
19 <script>
20 </script>
21 </body>

cy を absolute みたいな固定配置にワタシはしてない(公式のデモの類では全部でそうしてるがワタシはこれは嫌いなので)のでだいたいこんなんで措置になるが、そうでないならちょっと違ったことになるかもしれないね。