tab.inc.php

サマリ複数ページのタブ表示
リビジョン1.11
対応バージョン1.5.3
投稿者M.Taniguchi
投稿日2020-04-20 (月) 13:31:33

概要

複数のページをタブ表示するプラグイン。

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

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

使い方

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

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

使用例

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

制約

コード

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

<?php
/*
PukiWiki - Yet another WikiWikiWeb clone.
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 = '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;
}

詳細

ページの切り替え処理

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

動作設定

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

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


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

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

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

スキンCSSによるタブへのスタイル適用

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

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

ページ情報取得API

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

?plugin=tab&page=ページ名

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

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

ライセンス

GPL v3

改版履歴


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-06-02 (火) 07:23:33
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.184 sec.

OSDN