#author("2020-05-31T18:00:09+09:00","","")
#author("2020-06-02T07:23:33+09:00","","")
** tab.inc.php [#f4e25ff9]
|RIGHT:100|LEFT:360|c
|~サマリ|複数ページのタブ表示|
|~リビジョン|1.1|
|~リビジョン|1.11|
|~対応バージョン|1.5.3|
|~投稿者|[[M.Taniguchi]]|
|~投稿日|&new{2020-04-20 (月) 13:31:33};|
**概要 [#ua2ede48]
複数のページをタブ表示するプラグイン。~

タブをクリックすると該当ページをロードして表示を差し替えます。~
タブをダブルクリックすると該当ページのURLに遷移します。

PukiWiki 1.5.3/PHP 7.4/UTF-8 で動作確認済み。旧バージョンでも動くかもしれませんが非推奨です。PHPは5.2以上が必須。古いブラウザーでは動作しない場合があります。

**使い方 [#k0eae189]

#tab([ラベル1>]ページ名[,[ラベル2>]ページ名2][,...])

ラベルとページ名の組をカンマで区切って必要なだけ羅列する。~
ページ名はタブ表示したいページの名前(Hoge、Fuga/Piyo等)。~
ラベルを省略するとページ名がタブのラベルとなる。

''使用例''

 #tab(プロフィール>Profile,履歴>History,連絡先>Contact)

''制約''
-本プラグインを挿入できるのは1ページにつき1箇所のみです。
-ループする恐れがあるため、タブの入れ子、つまりタブで読み込むページ内にさらにタブを表示することはできません(強制的に無効化される)。
-注釈表示領域にはタブで読み込まれたページの注釈が表示され、本プラグインを挿入したページの注釈は表示されません。
-JavaScriptが有効でないと動作しません。

**コード [#g8cc5e41]

tab.inc.php~
(下記のコードをコピーして、plugin ディレクトリに tab.inc.php というファイル名で保存してください)

 <?php
 /*
 PukiWiki - Yet another WikiWikiWeb clone.
 tab.inc.php, v1.1 2020 M.Taniguchi
 tab.inc.php, v1.11 2020 M.Taniguchi
 License: GPL v3 or (at your option) any later version
 
 ページをタブ表示するプラグイン。
 
 タブをクリックすると該当ページをロードして表示を差し替えます。
 タブをダブルクリックすると該当ページのURLに遷移します。
 
 【使い方】
 #tab([ラベル1>]ページ名[,[ラベル2>]ページ名2][,...])
 
 ラベルとページ名の組をカンマで区切って必要なだけ羅列する。
 ページ名はタブ表示したいページの名前(Hoge、Fuga/Piyo等)。
 ラベルを省略するとページ名がタブのラベルとなる。
 
 【使用例】
 #tab(プロフィール>Profile,履歴>History,連絡先>Contact)
 
 【制約】
 ・本プラグインを挿入できるのは1ページにつき1箇所のみです。
 ・ループする恐れがあるため、タブの入れ子、つまりタブで読み込むページ内にさらにタブを表示することはできません(強制的に無効化される)。
 ・注釈表示領域にはタブで読み込まれたページの注釈が表示され、本プラグインを挿入した元ページの注釈は表示されません。
 ・JavaScriptが有効でないと動作しません。
 */
 
 /////////////////////////////////////////////////
 // タブ表示プラグイン設定(tab.inc.php)
 if (!defined('PLUGIN_TAB_RESTRICT'))           define('PLUGIN_TAB_RESTRICT',           0);     // 本プラグインの実行を凍結/編集制限ページ内またはPKWK_READONLY下に制限する
 if (!defined('PLUGIN_TAB_ALLOW_DOUBLECLICK'))  define('PLUGIN_TAB_ALLOW_DOUBLECLICK',  1);     // 該当ページのURLに遷移するタブダブルクリック機能を許可
 if (!defined('PLUGIN_TAB_TIMEOUT'))            define('PLUGIN_TAB_TIMEOUT',            10000); // ページをロードする際のタイムアウト時間(ミリ秒)。0なら設定せず
 if (!defined('PLUGIN_TAB_ALLOW_DEFAULTSTYLE')) define('PLUGIN_TAB_ALLOW_DEFAULTSTYLE', 1);     // タブに既定のスタイルを適用
 if (!defined('PLUGIN_TAB_NOTEID'))             define('PLUGIN_TAB_NOTEID',            'note'); // 注釈表示ブロック要素のID
 if (!defined('PLUGIN_TAB_NOCACHE'))            define('PLUGIN_TAB_NOCACHE',            1);     // ロードするページ情報のブラウザーキャッシュを明示的にオフ
 
 
 function plugin_tab_convert() {
 	global	$vars, $defaultpage, $foot_explain;
 
 	// JavaScript無効なら中断
 	if (!PKWK_ALLOW_JAVASCRIPT) return '';
 
 	// 引数がなければ中断
 	$arg = func_get_args();
 	if (!$arg) return '';
 
 	// 二重起動なら中断
 	static	$included = false;
 	if ($included) return '';
 	$included = true;
 
 	// 制限あり?
 	if (PLUGIN_TAB_RESTRICT) {
 		global $auth_user;
 		$backup = $auth_user;
 		$auth_user = '';	// 非認証ユーザーのふり
 		$result = (PKWK_READONLY || !is_editable($vars['page']) || !is_page_writable($vars['page']));	// 制限付きページか判定
 		$auth_user = $backup;
 		if (!$result) return '';	// 誰でも編集可能なページなら中断
 	}
 
 	// 引数を走査してタブページ名を取得
 	$page = '';
 	$tabs = '';
 	foreach ($arg as $v) {
 		if (strpos($v, '>') === false) {
 			$v = trim($v);
 			$v = array($v, $v);
 		} else {
 			$v = explode('>', $v);
 			$v[0] = trim($v[0]);
 			$v[1] = trim($v[1]);
 		}
 		$id = urlencode($v[1]);
 		$tabs .= '<li id="PluginTab-' . $id . '" class="PluginTab" data-page="' . $id . '" onclick="__pluginTab__.change(this);"' . ((PLUGIN_TAB_ALLOW_DOUBLECLICK)? ' ondblclick="__pluginTab__.move(this);"' : '') . ((!$page)? ' data-active="1"' : '') . '>' . htmlsc(trim($v[0])) . '</li>';
 		if (!$page) $page = $v[1];
 	}
 	$tabs = '<ul id="PluginTabs">' . $tabs . '</ul>';
 
 	// デフォルトページ読み込み
 	$data = plugin_tab_getPage($page);
 	$data = str_replace('","explain":[', '`,"explain":[', str_replace('{"body":"', '{"body":`', $data));	// エラー対策
 	$page = urlencode($page);
 	$noteId = (PLUGIN_TAB_NOTEID)? PLUGIN_TAB_NOTEID : 'note';
 
 	// スタイル定義
 	$style = <<<EOT
 <style>
 /* タブ領域 */
 #PluginTabs {
 	display: block;
 	margin-bottom: 0;
 	padding: 0;
 	-webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;
 }
 /* タブ */
 .PluginTab {
 	list-style: none;
 	display: inline-block;
 	min-width: 5em;
 	padding: .3em .5em;
 	box-sizing: border-box;
 	border: 0 solid #808080;
 	border-width: 1px 1px 0;
 	border-radius: 8px 8px 0 0;
 	text-align: center;
 	cursor: pointer;
 }
 /* 選択中タブ */
 .PluginTab[data-active='1'] {
 	font-weight: bold;
 	cursor: auto;
 }
 /* ページ表示領域 */
 #PluginTabContent {
 	margin-top: 1.5em;
 }
 /* その他調整 */
 #${noteId} { display:none };
 </style>
 EOT;
 
 	// JavaScriptコード
 	$script = get_script_uri();
 	$method = (PLUGIN_TAB_NOCACHE)? 'POST' : 'GET';
 	$method = 'GET';
 	$timeout = PLUGIN_TAB_TIMEOUT;
 	$jscode = <<<EOT
 <script>
 'use strict';
 
 var	__PluginTab__ = function() {
 	const	self = this;
 	this.content = document.getElementById('PluginTabContent');	// ページ表示領域要素
 	this.tabs = document.getElementsByClassName('PluginTab');	// タブ要素
 	this.note = null;	// 注釈表示領域要素
 	this.data = [];		// ページ情報
 
 	// 最初のタブのページ情報はあらかじめ設定
 	this.data['${page}'] = ${data};
 
 	// URLに「#タブページ名」指定あり?
 	var path = location.href.split('#');
 	if (path[1] != undefined && path[1]) {
 		// ありならタブ切り替え
 		window.addEventListener('DOMContentLoaded', function(){
 			var	ele = document.getElementById('PluginTab-' + path[1]);
 			self.change(ele);
 
 			self.note = document.getElementById('${noteId}');
 			self.note.style.display = 'none';
 		});
 	} else {
 		// HTML表示
 		this.makeHTML(this.content, this.data['${page}']['body']);
 
 		// 注釈を設定
 		window.addEventListener('DOMContentLoaded', function(){
 			self.note = document.getElementById('${noteId}');
 			self.changeNote(self.data['${page}']['explain']);
 		});
 	}
 };
 
 // タブ切り替え(タブクリックハンドラ)
 __PluginTab__.prototype.change = function(ele) {
 	const	self = this;
 
 	if (ele.getAttribute('data-active')) return;	// すでに選択中のタブなら無視
 	var	page = ele.getAttribute('data-page');	// クリックされたタブに対応するページ名を取得
 
 	// URLに「#タブページ名」を設定
 	 window.location.href = '#' + ((page != '${page}')? page : '');
 
 	// タブに選択中属性を設定
 	for (var i = self.tabs.length - 1; i >= 0; --i) self.tabs[i].removeAttribute('data-active');
 	ele.setAttribute('data-active', '1');
 
 	// ロード済みのページか?
 	if (self.data[page] !== undefined) {
 		// ロード済みページ情報を表示
 		self.makeHTML(self.content, self.data[page]['body']);
 		self.changeNote(self.data[page]['explain']);
 	} else {
 		// ページ情報をロードして表示
 		var xhr = new XMLHttpRequest();
 		xhr.open('${method}', '${script}?plugin=tab&refer=' + page);	// plugin_tab_action()へ要求
 		xhr.responseType = 'json';
 		if (${timeout} > 0) xhr.timeout = Math.max(${timeout}, 1000);
 		xhr.onload = function() {
 			if (xhr.status == 200 && xhr.response) {
 				self.data[page] = xhr.response;	// ページ情報を記憶しておき、次回からロードを省略する
 				if (typeof self.data[page] === 'string') self.data[page] = JSON.parse(self.data[page]);	// IE対策
 				self.makeHTML(self.content, self.data[page]['body']);
 				self.changeNote(self.data[page]['explain']);
 			}
 		};
 		xhr.send();
 	}
 };
 
 // Script実行付きinnerHTML(注:document.write()には非対応)
 __PluginTab__.prototype.makeHTML = function(element, html) {
 	var regexp = /<script[^>]+?\/>|<script(.|\s)*?\/script>/gi;
 	var scripts = html.match(regexp);
 	if (scripts) {
 		element.innerHTML = html.replace(regexp, '');
 		scripts.forEach(function(script) {
 			var scriptElement = document.createElement('script');
 			var	src = script.match(/<script[^>]+src=['"]?([^'"\s]+)[\s'"]?/i);
 			if (src && src.length >= 1) {
 				scriptElement.src = src[1];
 				scriptElement.setAttribute('defer', 'defer');
 			} else {
 				scriptElement.text = script.replace(/<[\/]*?script>/gi, '');
 			}
 			element.appendChild(scriptElement);
 		});
 	} else {
 		element.innerHTML = html;
 	}
 };
 
 // ページ遷移(タブダブルクリックハンドラ)
 __PluginTab__.prototype.move = function(ele) {
 	var	page = ele.getAttribute('data-page');	// ダブルクリックされたタブに対応するページ名を取得
 	window.location.href = '${script}?' + page;	// 画面遷移
 };
 
 // 注釈書き換え
 __PluginTab__.prototype.changeNote = function(data) {
 	const	self = this;
 
 	if (self.note) {
 		var	explain = '';
 		if (data) data.forEach(function(v){ explain += v; });
 		if (explain) {
 			self.note.innerHTML = '<hr class="note_hr"/>' + explain;
 			self.note.style.display = 'block';
 		} else {
 			self.note.style.display = 'none';
 			self.note.innerHTML = '';
 		}
 	}
 };
 
 var __pluginTab__ = new __PluginTab__();
 </script>
 EOT;
 
 	$foot_explain = array(1 => '&#8203;');	// 注釈表示ブロックを生成させるためダミーの注釈を設定
 
 	return ((PLUGIN_TAB_ALLOW_DEFAULTSTYLE)? $style : '') . $tabs . '<section id="PluginTabContent"></section>' . $jscode;
 }
 
 
 // URL指定呼び出し(タブ切り替え時にクライアントから呼ばれる)
 function plugin_tab_action() {
 	global $vars;
 	header('Content-Type: application/json');
 	if (PLUGIN_TAB_NOCACHE) header('Cache-Control: no-cache');
 	echo plugin_tab_getPage($vars['refer']);
 	exit;
 }
 
 
 // ページ情報JSON取得
 function plugin_tab_getPage($page) {
 	global $vars, $defaultpage, $foot_explain, $auth_type, $auth_user;
 
 	$page = trim($page);
 	if (!$page) $page = &$defaultpage;	// ページ名が空ならトップページ
 
 	// 有効かつ権限があればページ内容を取得
 	$body = '';
 	if (is_page($page)) {
 		if (check_readable($page, false, false)) {
 			// 現在ページと取得ページが異なる?
 			if ($page != $vars['page']) {
 				$backup = unserialize(serialize($vars));	// HTTP引数をディープコピーで待避
 				$vars['page'] = $page;	// 現在ページ名を変更してシステムを騙す
 			}
 
 			$body = get_source($page);	// ソースを取得
 			foreach ($body as $i => $row) if (strpos($row, '#tab(') === 0) $body[$i] = '';	// ループ防止のため自プラグイン記述を探して無効化
 			$body = convert_html($body);	// HTMLに変換
 
 			if ($backup) $vars = $backup;	// HTTP引数を元に戻す
 		} else
 		if (exist_plugin_action('loginform') && (AUTH_TYPE_FORM === $auth_type || AUTH_TYPE_EXTERNAL === $auth_type || AUTH_TYPE_SAML === $auth_type) && !$auth_user) {
 			$body = '<a href="./?plugin=loginform&pcmd=login&page=' . $page . '">Login required</a>';
 		}
 	}
 
 	// JSONエンコード
 	$json = json_encode(array('body' => $body, 'explain' => array_values($foot_explain)));
 	return $json;
 }
 

**詳細 [#j4377614]

***ページの切り替え処理 [#k41be708]

タブを選択すると、対応するページをAjaxでロードします。~
そして既表示ページをDOMから削除し、代わりにロードしたページを追加することで表示を切り替えます。~
スタイルによる表示・非表示ではなく、わざわざこのようにDOMごと都度入れ替えるのは、要素IDの重複やページ内容の衝突による不具合を避けるためです。~

***動作設定 [#j8fd8718]

コード内の下記の定数で動作を制御することができます。

|~定数名|~値|~既定値|~意味|
|PLUGIN_TAB_RESTRICT|CENTER:0 or 1|CENTER:0|1なら本プラグインの実行を凍結/編集制限ページ内またはPKWK_READONLY下に制限する(※)|
|PLUGIN_TAB_ALLOW_DOUBLECLICK|CENTER:0 or 1|CENTER:1|1なら該当ページのURLに遷移するタブのダブルクリック機能を許可|
|PLUGIN_TAB_TIMEOUT|CENTER:任意の数値|CENTER:10000|タブページをロードする際のタイムアウト時間(ミリ秒)。0なら設定せず|
|PLUGIN_TAB_ALLOW_DEFAULTSTYLE|CENTER:0 or 1|CENTER:1|1ならタブに既定のスタイルを適用|
|PLUGIN_TAB_NOTEID|CENTER:任意の文字列|CENTER:'note'|注釈表示ブロック要素のID。非標準スキン使用時など、必要に応じて変更|
|PLUGIN_TAB_NOCACHE|CENTER:0 or 1|CENTER:1|1ならロードするページ情報のブラウザーキャッシュを明示的にオフ|

~
''※ 誰でも編集可能な公開ウィキにおいては、PLUGIN_TAB_RESTRICT を 1 にする(一般ユーザーには挿入できなくする)ことを勧めます。''

PukiWikiは原則として、ページを同時に複数表示したり、画面全体をリロードせずにページの削除・追加を繰り返される前提で作られていません。~
一般に、ユーザーもそのつもりでページを編集してはいないでしょう。~
(標準添付の include のようにサーバーサイドでページの入れ子を実現するプラグインは存在するし、PukiWiki側でもページの再帰ループ対策などある程度考慮してはいるようですが。)~
このプラグインはその原則を無理やり破るため、予期せぬ不具合が生じる可能性があります。~
結果はPukiWikiやPHPのバージョン・表示するページ内容・併用プラグイン・さらにはブラウザーによっても異なるため、何々を避ければ安全などとは一概にいえません。~

可能性があるだけで即致命的なわけではなく、導入してみたが動かないという誤解を避けるためにも PLUGIN_TAB_RESTRICT のデフォルトは 0 としていますが、公開ウィキ管理者の方は以上に留意していただければと思います。

***スキンCSSによるタブへのスタイル適用 [#d598780e]

コード内の定数 PLUGIN_TAB_ALLOW_DEFAULTSTYLE の値を 0 にして、下記の要素をCSSで修飾してください。

|~役割|~要素|~CSSセレクター|
|CENTER:タブ領域|<ul id="PluginTabs">タブ群</ul>|#PluginTabs|
|CENTER:タブ|<li class="PluginTab">ラベル</li>|.PluginTab|
|CENTER:選択中タブ|<li class="PluginTab" data-active="1">ラベル</li>|.PluginTab[data-active='1']|
|CENTER:ページ表示領域|<section id="PluginTabContent">ページ本文HTML</section>|#PluginTabContent|

***ページ情報取得API [#te17538a]

次のURL書式で指定ページの本文と注釈とをJSON形式で得ることができます。

 ?plugin=tab&page=ページ名

応答フォーマットは次の通り。

 {
     "body":"ページ本文のHTML",
     "explain":[
         "1つ目の注釈のHTML",
         "2つ目の注釈のHTML",
         ...
     ]
 }


**ライセンス [#p1472648]

GPL v3


**改版履歴 [#h661aa51]
-バージョン1.0
--初版
-バージョン1.04
--タブのダブルクリックによる該当ページへの遷移機能を追加
-バージョン1.1
--エイリアス書式に合わせ、引数の順序を「ページ名>ラベル」から「ラベル>ページ名」に変更
--リロードしても元に戻らないよう、選択したタブをURLに保持する
--ページに含まれるJavaScript(script要素)をできるかぎり実行する
--ページ内容により正常に表示されない不具合を修正
-バージョン1.11
--リソースを常にGETメソッドで取得するよう修正

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Site admin: PukiWiki Development Team

PukiWiki 1.5.3+ © 2001-2020 PukiWiki Development Team. Powered by PHP 5.6.40-0+deb8u12. HTML convert time: 0.063 sec.

OSDN