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

多くの WindowsLinux のブラウザ

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

Mac における Safari

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

Mac における ChromeFirefox

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

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

/*
 * vpTagHelper.js
 * 井戸端タグ支援ツール
 *
 * 井戸端サブページの編集画面に、タグ検索・編集機能を追加する
 */

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?$/)) //(導入部除き)節編集モードではない
) {
  mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.template.mustache']).then(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 = [];

      var api = new mw.Api();

      /** APIのクエリ結果から全タグの配列を作成、allTagsに格納する */
      function loadAllTagsFromAPI(callback, continueInfo) {
        var apiParams = {
          format: 'json',
          action: 'query',
          list: 'embeddedin',
          einamespace: 14,
          eilimit: SEARCH_LIMIT,
          eititle: TEMPLATE_EMBEDDED_FOR_SEARCH,
          continue: '',
        };
        $.extend(apiParams, continueInfo);

        api.get(apiParams).done(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 =
        '{{#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 Mustache.render(tagButtonsTemplate, 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 () {
      VpTagUI.setup();
      TagKeeper.initialize(PageUI.getWikiText(), VpTagUI.refreshRecommend);

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