利用者:Sat.d.h./variant-character-selector.js

注意: 保存後、変更を確認するにはブラウザーのキャッシュを消去する必要がある場合があります。

  • Firefox / Safari: Shift を押しながら 再読み込み をクリックするか、Ctrl-F5 または Ctrl-R を押してください (Mac では ⌘-R)
  • Google Chrome: Ctrl-Shift-R を押してください (Mac では ⌘-Shift-R)
  • Internet Explorer / Microsoft Edge: Ctrl を押しながら 最新の情報に更新 をクリックするか、Ctrl-F5 を押してください
  • Opera: Ctrl-F5を押してください
/**
 * このスクリプトは[[s:ja:MediaWiki:Gadget-char-convert0.js]] (2015-10-24T06:17:57, UTC)、
 * および[[s:ja:MediaWiki:Gadget-to-old-char0.js]] (2015-12-21T14:02, UTC) をもとに作成しています。
 * ▽使い方:
 * 文字列を選択すると、辞書ファイルをもとに変換テーブルを作成し、異体字候補を表示します。
 * 文字をクリックすると、wikiPreviewおよびwpTextbox1において、該当文字が置換されます。
 */
(function() {
  'use strict';

  // 標準、利用者、ページ名前空間の編集ページのみに適用
  var namespaceNum = mw.config.get('wgNamespaceNumber');
  if ((namespaceNum !== 0 && namespaceNum !== 2
    && namespaceNum != 250) || !document.getElementById('editform')) {
      return;
  }

  /**
   * selected - 選択された文字列で置換対象(IVSを含む)
   * searched - 旧字等の異体字を検索する際の検索対象文字、
   *     かつ異体字セレクタを付加する基底文字(IVSを除外)
   * button - ボタン(button要素の場合は
   *     Google Chromeで正常に機能しないため、spanで代用)
   */
  var dicData = {};
  var flag = false;
  var selected = '';
  var searched = '';
  var wpTextbox = document.forms.editform.wpTextbox1;
  var previewText = document.getElementById('wikiPreview');
  var mouse_x = 0;
  var mouse_y = 0;
  var menu = {};
  var button = document.createElement('span');
  button.className = 'button';

  (function initialize() {
    // CSSファイル読み込み
    var style = document.createElement('link');
    style.href = '/w/index.php?title=User:Sat.d.h./for-variant-character-selector.css&action=raw&ctype=text/css';
    style.rel = 'stylesheet';
    style.type = 'text/css';
    document.getElementsByTagName('head')[0].appendChild(style);

    createDic();

    // 切替えスイッチ
    $('#p-namespaces > ul')
        .append($('<li><span><a>異体字OFF</a></span></li>').attr({'id':'var-swich'}));
    $('#var-swich').click(function() {
        if (flag) {
            flag = false;
            $('#var-swich a').text('異体字OFF');
        } else {
            flag = true;
            $('#var-swich a').text('異体字ON');
        }
    });

    // 選択文字列の取得およびメニューの表示
    wpTextbox.addEventListener('select', function(e) {
      if (!flag) return;
      var start = wpTextbox.selectionStart;
      selected = wpTextbox.value.substr(start, 8);
      if (start === wpTextbox.selectionEnd || selected.substr(0, 1).match(/[^\u3400-\uFAFF]/)) return;
      document.addEventListener('mousemove', function f(e) {
        mouse_x = e.clientX;
        mouse_y = e.clientY;
        document.removeEventListener('mousemove', f, false);
      }, false);
      charType ();
      showMenu ();
    }, false);
    previewText.addEventListener('mouseup', function(e) {
      if (!flag) return;
      if (document.getSelection().toString() === '' || selected.substr(0, 1).match(/[^\u3400-\uFAFF]/)) return;
      selected = document.getSelection().toString();
      mouse_x = e.clientX;
      mouse_y = e.clientY;
      charType ();
      showMenu ();
    }, false);
  }());

  // 辞書データの作成
  function createDic() {
    var dfd = new $.Deferred();

    /**
     * 辞書データの取得
     * 現在は[[s:ja:利用者:CES1596/辞書]]を利用しておりますが、
     * 自前の辞書データを作成する予定です。
     */
    (function getDic() {
      /**
       * pageids - 辞書のページID:'18270'
       * dAstr - データ中のマーカ
       */
      var pageids = '18270';
      var dAstr = '*';
      $.ajax({
        type: 'GET',
        scriptCharset: 'utf-8',
        dataType: 'jsonp',
        url: 'https://ja.wikisource.org/w/api.php?action=query&prop=revisions&rvprop=content&format=json&pageids='+pageids,
        timeout: 5000,
      })
      .done(function(json) {
        dicData = json.query.pages[pageids].revisions[0][dAstr];
        dfd.resolve();
      })
      .fail(function() {
          alert('辞書読取エラー');
      });
    }());

    // データの整形
    dfd.promise().done(function arrangeData() {
      // console.log('deferred done');
      var lines = dicData.split('\n');
      var strD =[];
      for (var i = 1; i < lines.length-1; i++) {
        if (lines[i].match(/\*\s*([\S]*)\s*[::→]\s*([\S]*)/)) {
          if (strD.length) strD += ', ';
          strD += '\"'+RegExp.$2+'\":\"'+RegExp.$1+'\"';
          }
      }
      dicData = JSON.parse('{' + strD + '}');
    });
  }

  // 文字種ごとにsearchedとselectedを決定
  function charType() {
    if (selected.substr(0,3).match('&#x')) {
      // 数値文字参照(主にCJK互換漢字)
      searched = unescape('%u' + selected.substr(3, 4));
      selected = selected.substr(0, 8) ;
    } else if (selected.substr(0, 4).match(/[\uD800-\uDBFF][\uDC00-\uDFFF]\uDB40[\uDD00-\uDDEF]/)) {
      // サロゲートペア+IVS
      searched = selected.substr(0, 2);
      selected = selected.substr(0, 4);
    } else if (selected.substr(1, 2).match(/\uDB40[\uDD00-\uDDEF]/)) {
      // IVS
      searched = selected.substr(0, 1);
      selected = selected.substr(0, 3);
    } else if (selected.substr(0, 1).match(/[\uD800-\uDBFF]/)) {
      // サロゲートペア
      searched = selected.substr(0, 2);
      selected = searched;
    } else {
      // その他の文字(一般的な文字)
      searched = selected.substr(0, 1);
      selected = searched;
    }
  }

  // メニュー本体
  function showMenu() {
    // リセット
    if (menu) {
      if (menu.parentNode) document.body.removeChild(menu);
    }

    createFrame();

    // 各種の文字一覧の作成
    cjk();
    ivs();
    otherChar();
  }

  // 外部レイアウトの設定
  function createFrame() {
    var content = {};
    var size = '';
    var close = {};
    menu = document.createElement('form');
    menu.className = 'variantchar-menu';
    menu.id = 'variantCharMenu';

    // メニュー表示位置
    content = document.body;
    size = document.defaultView.getComputedStyle(content, null).fontSize;
    size = parseFloat(size);
    switch (true) {
      case window.innerWidth <= size * 30 + 2:
        menu.style.left = 0;
        break;
      case window.innerWidth <= mouse_x + (size * 30 + 2):
        menu.style.right = 0;
        break;
      default:
        menu.style.left = mouse_x + 'px';
        break;
    }
    switch (true) {
      case window.innerHeight <= size * 30 + 2:
        menu.style.top = 0;
        break;
      case window.innerHeight <= mouse_y + (size * 30 + 2):
        menu.style.bottom = 0;
        break;
      default:
        menu.style.top = mouse_y + 'px';
        break;
    }

    // 終了処理(閉じるボタン・メニューのダブルクリック・Escキー)
    close = button.cloneNode(true);
    close.className += ' close';
    close.insertAdjacentHTML('afterbegin', '×');
    close.addEventListener('click', function() { document.body.removeChild(menu); }, false);
    menu.appendChild(close);
    menu.addEventListener('dblclick', function() { document.body.removeChild(menu); }, false);
    document.addEventListener('keydown', function(e) {
      if (e.keyCode === 27) document.body.removeChild(menu);
    }, false);
    document.body.appendChild(menu);
  }

  // CJK統合・互換漢字を表示
  function cjk() {
    var i = 0;
    var cjkMenu = {};
    var cjkSet = {};
    var cjkChar = '';
    var cjkCode = '';
    cjkMenu = createPanel (cjkMenu, 'CJK');
    cjkCode = getUnicode(searched);
    cjkSet[cjkCode] = searched;
    Object.keys(dicData).forEach(function(key) {
      var value = dicData[key];
      if (selected === key || value.indexOf(selected) !== -1) {
        // console.log('代替候補:' + key + ' → ' + value);
        cjkCode = getUnicode(key);
        cjkSet[cjkCode] = key;
        while (value.substr(i, 1)) {
          if (value.substr(i, 3).match('&#x')) {
            cjkChar = unescape('%u' + value.substr(i+3, 4));
            cjkCode = getUnicode(cjkChar);
            cjkSet[cjkCode] = cjkChar;
          } else if (!value.substr(i, 1).match(/[&#xA-Fa-f0-9;g]/)) {
            cjkChar = value.substr(i, 1);
            cjkCode = getUnicode(cjkChar);
            cjkSet[cjkCode] = cjkChar;
          }
          i++;
        }
      }
    });
    createItem (cjkMenu, cjkSet);
  }

  // 受け取った文字のユニコードを計算
  function getUnicode(char) {
      var surrogate1 = '';
      var surrogate2 = '';
      var unicode  = escape(char);
      if (char.match(/[\uD800-\uDBFF]/)) {
        surrogate1 = unicode.slice(2,6);
        surrogate1 = parseInt(surrogate1,16);
        surrogate2 = unicode.slice(-4);
        surrogate2 = parseInt(surrogate2,16);
        unicode = 0x10000 + (surrogate1 - 0xD800) * 0x400 + (surrogate2 - 0xDC00);
      } else {
        unicode = unicode.slice(-4);
      }
      unicode = 'U+' + unicode.toString(16).toUpperCase();
      return unicode;
  }

  // IVS文字を表示
  function ivs() {
    var i = 0;
    var ivsMenu = {};
    var ivsSet = {};
    var ivsName = '';
    var ivsChar = '';
    ivsMenu = createPanel (ivsMenu, 'IVS');
    for (i = 0; i < 32; i++) {
      ivsChar = i.toString(16);
      ivsChar = ('0' + ivsChar).slice(-2);
      ivsName = 'U+E01' + ivsChar.toUpperCase();
      ivsChar = '%uDB40%uDD' + ivsChar;
      ivsChar = searched + unescape(ivsChar);
      ivsSet[ivsName] = ivsChar;
    }
    createItem (ivsMenu, ivsSet);
  }

  // その他の文字用入力フォームを表示
  function otherChar() {
    var otherMenu = {};
    var charInput = {};
    var startReplace = {};
    otherMenu = createPanel (otherMenu, 'その他');
    charInput = document.createElement('input');
    charInput.id = 'charInput';
    charInput.type = 'search';
    otherMenu.appendChild(charInput);
    startReplace = button.cloneNode(true);
    startReplace.insertAdjacentHTML('afterbegin', '置換');
    startReplace.addEventListener('click', replaceChar, false);
    otherMenu.appendChild(startReplace);
  }

  // 文字種ごとにパネルを作成
  function createPanel(panel, type) {
    var heading = {};
    panel = document.createElement('div');
    heading = document.createElement('h2');
    heading.insertAdjacentHTML('afterbegin', type);
    panel.appendChild(heading);
    menu.appendChild(panel);
    return panel;
  }

  // 各文字の入力ボタンを作成
  function createItem(panel, charSet) {
    var itemFrame = {};
    var variantCaption = {};
    var variantButton = {};
    var item = {};
    itemFrame = document.createElement('span');
    itemFrame.className = 'item-frame';
    variantCaption = document.createElement('span');
    variantCaption.className = 'variant-caption';
    variantButton = button.cloneNode(true);
    variantButton.className += ' variant-button';
    itemFrame.appendChild(variantCaption);
    itemFrame.appendChild(variantButton);
    Object.keys(charSet).forEach(function(key) {
      item = itemFrame.cloneNode(true);

      // variantCaption
      item.children[0].insertAdjacentHTML('afterbegin', key);

      // variantButton
      item.children[1].insertAdjacentHTML('afterbegin', charSet[key]);
      item.children[1].addEventListener('click', replaceChar, false);
      panel.appendChild(item);
    });
  }

  // 置換・メニュー終了
  function replaceChar(e) {
    var newChar = '';
    if (e.target.textContent === '置換') {
      newChar = document.forms.variantCharMenu.charInput.value;
    } else {
      newChar = e.target.textContent;
    }
    previewText.innerHTML = previewText.innerHTML.replace(new RegExp(selected, 'g'), newChar);
    wpTextbox.value = wpTextbox.value.replace(new RegExp(selected, 'g'), newChar);
    document.body.removeChild(menu);
  }
}());