利用者:Cpro/vpTagHelper.js/sandbox.js

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

/*
 * 井戸端タグ支援ツール
 * [[利用者:Cpro|cpro]]
 * 井戸端サブページの編集画面に、タグ検索・編集機能を追加する
 */
if(
	mw.config.get('wgPageName').indexOf('Wikipedia:井戸端/subj/') === 0 &&                  //井戸端サブページ、かつ
	(mw.config.get('wgAction') == 'edit' || mw.config.get('wgAction') == 'submit') &&      //編集・プレビュー・差分確認時のみで
	(!jQuery('[name=wpSection]').length || jQuery('[name=wpSection]').val().match(/^0?$/)) //(導入部除き)節編集モードではない
) {

(function($) {
	/** タグ一覧のロード・保持・抽出を管理するオブジェクト */
	var TagKeeper = (function() {
		var TEMPLATE_EMBEDDED_FOR_SEARCH = 'Template:井戸端タグカテゴリ'; //このテンプレートの呼出元が完全リストとなる
		var CATEGORY_PREFIX = 'Category:井戸端の話題/'; //見つかったタグカテゴリから取り除くべきプレフィクス
		var SEARCH_LIMIT = 500; //1回の検索数上限

		var allTags = [];
		var currentTags = [];
		var recommendTags = [];

		/** APIのクエリ結果から全タグの配列を作成、allTagsに格納する */
		function loadAllTagsFromAPI(callback, continueInfo) {
			var apiParams = {
				format: 'json',
				action: 'query',
				list: 'embeddedin',
				einamespace: 14,
				eilimit: SEARCH_LIMIT,
				eititle: TEMPLATE_EMBEDDED_FOR_SEARCH
			};
			continueInfo = continueInfo || {'continue': ''};
			$.extend(apiParams, continueInfo);
			
			$.getJSON(mw.util.wikiScript('api'), apiParams, function(data) {
				if(!data.query || !data.query.embeddedin)
					return;
				
				var tags = data.query.embeddedin.map(function(ent) {
					return ent.title.substring(CATEGORY_PREFIX.length);
				});
				allTags = allTags.concat(tags);
				
				//1回のロード件数 (SEARCH_LIMIT) より多ければ続きをロードする
				if(data['continue']) {
					loadAllTagsFromAPI(callback, data['continue']);
				} else {
					//ロード終了後処理
					callback();
				}
			});
		}

		return {
			/** 全タグのロードとページ解析 完了後コールバック実行 */
			initialize: function(pageText, callback) {
				loadAllTagsFromAPI(function() {
					TagKeeper.parse(pageText);
					callback();
				});
			},
			/** タグの選択状態切り替え */
			toggle: function(tag) {
				var i = currentTags.indexOf(tag);
				var toggledOn = i < 0;
				if(toggledOn)
					currentTags.push(tag);
				else
					currentTags.splice(i, 1);
				
				return toggledOn;
			},
			/** 現在のタグ */
			current: function() {
				return currentTags;
			},
			/** 使用可能なすべてのタグ */
			all: function() {
				return allTags;
			},
			/** 編集エリア内で見つかったタグ候補 */
			recommend: function() {
				return recommendTags;
			},
			/** クエリ文字列に中間一致するタグの配列を返す */
			search: function(query) {
				return allTags.filter(function(tag) {
					return (tag.toUpperCase().indexOf(query.toUpperCase()) >= 0);
				});
			},
			/** 編集エリアのウィキテキストを解析する */
			parse: function(pageText) {
				//現在記入済みのタグを取得
				var m = pageText.match(/\{\{vptag(?:\s*\|\s*([^{}]*?)\s*)?\}\}/i);
				if(m && m[1] && m[1].length > 0) {
					currentTags = m[1].split(/\s*\|\s*/);
				}

				pageText = pageText.toUpperCase()
					.replace(/\(UTC\)/g, '') //署名の(UTC)は拾わないように除去
					.replace(/<!--.+?-->/g, ''); //コメント内を拾わない
				
				//井戸端タグ以降を検索対象とする
				var searchStart = pageText.indexOf('{{VPTAG');
				if(searchStart < 0) searchStart = 0;
				
				recommendTags = allTags.filter(function(tag) {
					return (pageText.indexOf(tag.toUpperCase(), searchStart) >= 0);
				});
			}
		};
	})();

	/** タグの追加/削除履歴を保持するオブジェクト(要約欄記載用) */
	var TagHistory = (function() {
		var added = [],
			removed = [];
		var SUMMARY_HEAD = '[[WP:VPTAG|井戸端タグ]]';
		var SUMMARY_TAIL = '(vpTagHelper)';
		
		function toggleTag(tag, fromArray, toArray) {
			var i = fromArray.indexOf(tag);
			if(i >= 0) {
				fromArray.splice(i, 1);
				return;
			}
			if(toArray.indexOf(tag) < 0) {
				toArray.push(tag);
			}
		}

		return { //メソッド
			/** タグ追加履歴を追加する */
			add: function(tag) {
				toggleTag(tag, removed, added);
			},
			/** タグ削除履歴を追加する */
			remove: function(tag) {
				toggleTag(tag, added, removed);
			},
			/** 要約欄テキストを生成する */
			generateSummary: function() {
				if(added.length === 0 && removed.length === 0) {
					return '';
				}
				return [
					SUMMARY_HEAD,
					added.map(function(tag) { return '+' + tag; }).join(' '),
					removed.map(function(tag) { return '-' + tag; }).join(' '),
					SUMMARY_TAIL
				].join(' ').replace(/ {2,}/g, ' ');
			},
			/** 要約欄テキストを解析し履歴を作成する */
			parseSummary: function(summary) {
				added = [];
				removed = [];
				var summaryTailIndex = summary.length - SUMMARY_TAIL.length;
				
				if(summary.indexOf(SUMMARY_HEAD) !== 0 ||
				   summary.indexOf(SUMMARY_TAIL, summaryTailIndex) == -1) {
					return;
				}
				var tags = summary.substring(SUMMARY_HEAD.length + 1, summaryTailIndex - 1).split(' ');
				tags.forEach(function(i, tag) {
					if(!/^[+-]/.exec(tag)) return;
					var hist = (RegExp.lastMatch == '+') ? added : removed;
					hist.push(tag.substring(1));
				});
			}
		};
	})();

	/** MediaWikiページの要素にアクセスするオブジェクト */
	var PageUI = (function() {
		//MediaWikiの使用するid
		var ID_TEXTAREA = 'wpTextbox1'; //編集エリア
		var ID_SUMMARY = 'wpSummary'; //要約欄
		var ID_INSERT_BEFORE = 'editform'; //ツール配置用。このidを持つ要素の直前にツールを挿入する
		var _$textarea;

		//ページロード後のイベント設定
		$(function() {
			//手動でテキスト変更した場合にタグ再読み込み
			$textarea().on('change', function() {
				TagKeeper.parse(PageUI.getWikiText());
			});
		});

		function $textarea() {
			_$textarea = _$textarea || $('#' + ID_TEXTAREA);
			return _$textarea;
		}

		return {
			/** 編集エリアのウィキテキストを取得する */
			getWikiText: function() {
				return $textarea().val();
			},
			/** 編集エリアにタグリストを反映する */
			applyTags: function(tags) {
				var wikiText = $textarea().val();
				var reg = /\{\{vptag(?:\s*\|\s*([^{}]*?)\s*)?\}\}/i;
				if(!reg.test(wikiText)) {
					//テンプレートが記述されていなければ </noinclude> の直前にタグ追加
					reg = /\s*(?=<\/noinclude>)/i;
					if(!reg.test(wikiText)) return;
				}
				var dest = '{' + '{vptag|\n' + tags.join(' | ') + '\n}}';
				$textarea().val(wikiText.replace(reg, dest));
			},
			/** 要約欄テキストを取得する */
			getSummary: function() {
				return $('#' + ID_SUMMARY).val();
			},
			/** 要約欄にテキストを適用する */
			applySummary: function(summary) {
				$('#' + ID_SUMMARY).val(summary);
			},
			/** 所定の位置にタグ操作UIを挿入する */
			appendConsole: function(console) {
				$('#' + ID_INSERT_BEFORE).before(console);
			}
		};
	})();
	
	/** タグ操作UI */
	var VpTagUI = (function() {
		//このツールで定義するid/class
		var ID_RECOMMEND_CONTAINER = 'vptag-recommend'; //タグ候補表示部のid
		var ID_SEARCH_RESULT_CONTAINER = 'vptag-searchresult'; //検索結果表示部のid
		var ID_SEARCH_INPUT = 'vptag-searchinput'; //検索文字入力欄のid
		var ID_APPEND_BUTTON = 'vptag-append'; //タグ追加ボタンのid
		var CLASS_TOGGLED = 'vptag-toggled'; //ボタントグル状態のclass

		var CONTAINER_HTML = //ツール本体HTML
			'<div id="vptag-edittools">\n' +
				'<form action="#"><p>\n' +
					'<label for="' + ID_SEARCH_INPUT + '" class="vptag-container-label">井戸端タグ検索:</label>\n' +
					'<input id="' + ID_SEARCH_INPUT + '" type="text" />\n' +
					'<input type="text" style="position:absolute; visibility:hidden" />\n' + //dummy 誤送信防止
					'<button id="' + ID_APPEND_BUTTON + '" type="button">追加</button>\n' +
				'</p></form>\n' +
				'<p id="' + ID_SEARCH_RESULT_CONTAINER + '" class="vptag-container"></p>\n' +
				'<p class="vptag-container">\n' +
					'<span class="vptag-container-label">タグ候補:</span>\n' +
					'<span id="' + ID_RECOMMEND_CONTAINER + '"></span>\n' +
				'</p>\n' +
			'</div>';
		
		var tagButtonsTemplate = mw.template.add('module.with.templates', 'tagButtons.mustache', 
			'{' + '{#tags}}' +
				'<a class="vptag-button' +
				'{' + '{#toggled}} ' + CLASS_TOGGLED + '{' + '{/toggled}}">' +
					'{' + '{name}}' +
				'</a> ' +
			'{' + '{/tags}}');
		
		/** タグボタンの生成 */
		function createTagButtons(tags) {
			var data = {}
			data.tags = tags.map(function(tag) {
				return {
					name: tag,
					toggled: (TagKeeper.current().indexOf(tag) >= 0)
				};
			});
			return tagButtonsTemplate.render(data);
		}

		/** タグボタンクリック時処理 */
		function tagButtonOnclick(e) {
			var tag = $(this).text();
			var toggledOn = TagKeeper.toggle(tag);
			PageUI.applyTags(TagKeeper.current());

			$(this).toggleClass(CLASS_TOGGLED, toggledOn);
			if(toggledOn) {
				TagHistory.add(tag);
			} else {
				TagHistory.remove(tag);
			}
			PageUI.applySummary(TagHistory.generateSummary());
		}

		/** 検索結果を表示する */
		function searchTags(query) {
			$('#' + ID_SEARCH_RESULT_CONTAINER).html('');
			if(query === '') return;
			
			$('#' + ID_SEARCH_RESULT_CONTAINER)
				.empty()
				.append(createTagButtons(TagKeeper.search(query)));
		}

		return {
			/** 操作UIの生成・イベント設定 */
			setup: function() {
				//ツールフォームの生成・挿入
				PageUI.appendConsole($(CONTAINER_HTML));

				//イベント設定
				$('#' + ID_SEARCH_INPUT).keyup(function(e) {
					searchTags($(this).val());
				});
				$('#' + ID_APPEND_BUTTON).click(function(e) {
					TagKeeper.toggle($('#' + ID_SEARCH_INPUT).val());
					PageUI.applyTags(TagKeeper.current());
				});
				$('#' + ID_RECOMMEND_CONTAINER + ', #' + ID_SEARCH_RESULT_CONTAINER).on('click', 'a', tagButtonOnclick);
			},
			/** タグ候補を更新する */
			refreshRecommend: function() {
				$('#' + ID_RECOMMEND_CONTAINER)
					.empty()
					.append(createTagButtons(TagKeeper.recommend()));
			}
		};
	})();

	/* ページロード後処理 */
	$(function() {
		//CSSをロード
		importStylesheet('利用者:Cpro/vpTagHelper.css');
		
		VpTagUI.setup();
		TagKeeper.initialize(PageUI.getWikiText(), VpTagUI.refreshRecommend);

		//プレビュー時、要約欄に記載済みの操作履歴を取得
		TagHistory.parseSummary(PageUI.getSummary());
	});

})(jQuery);

} //end if