codicで変数名のコピーを補助するユーザースクリプト


# 概要
[codic](https://codic.jp/engine) で作成されたネーミングの左側にコピー用のボタンを設置します。
codicには大変お世話になっていて知人に紹介したのですが、コピーやケース変換の作業が手間だと言われたので作成しました。

動作確認はwindowsとfirefoxです。

# 機能
- `copy`をクリックするとネーミングをクリップボードにコピーします
- `snake_case` を選択している（初めて開く場合は選択されています）と `copy` をクリックしている間左側に変換用ボタンが表示されます  
マウスを押し込んだまま変換用ボタンにカーソルを移動してマウスを離すと、ケースを変換してコピーできます
  - `Pascal` でパスカルケース
  - `camel` でキャメルケース
  - `SNAKE` でアッパースネークケース
  - `ﾊｲﾌﾈｰｼｮﾝ` で単語間をハイフンに
  - `変換なし` で単語間をスペースで繋ぎます
- 単語をクリックした際、区切られた単語が選択されるようになります

```javascript
// ==UserScript==
// @name         codic コピー補助
// @version      0.1
// @match        https://codic.jp/engine
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const copyAreaId = 'copyArea';

    let resolve;
    const promise = new Promise(r => { resolve = r; });

    const mo = new MutationObserver(mrArr => {
        const translated = document.getElementById('translated');
        if (translated) {
            resolve(translated);
            mo.disconnect();
        }
    });
    mo.observe(document.body, {childList: true});

    document.head.insertAdjacentHTML('beforeend', `<style type="text/css">
[id="copyArea"] .bl_changeCase {
  text-align: right;
  white-space: nowrap;
  position: absolute;
  top: 0;
  left: -4px;
  transform: translate(-100%, -37.5%);
  display: none;
}
[id="copyArea"] .bl_changeCase > * + * {
  margin-top: 4px;
}
[id="copyArea"].is_caseSnake :active + .bl_changeCase {
  display: block;
}
</style>`);

    promise.then(translated => {
        const style = getComputedStyle(translated, '');
        translated.insertAdjacentHTML('beforebegin', `<div id="${copyAreaId}" style="position: absolute; top: ${style['padding-top']}; left: ${style['padding-left']}; transform: translateX(-100%); z-index: 3;"></div>`);

        const copyArea = document.getElementById(copyAreaId);
        copyArea.addEventListener('mouseup', e => {
            const btn = e.target;
            if (! btn.matches('button[data-case]')) { return; }

            translated.querySelectorAll('.dropdown-menu.with-arrow').forEach(n => n.remove());

            const type = btn.dataset.case;
            let str = btn.closest('[data-value]').dataset.value;
            if (type === 'usnake') {
                str = str.toUpperCase();
            } else if (type === 'hyphenation') {
                str = str.replace(/_/g, '-');
            } else if (type === 'no-converting') {
                str = str.replace(/_/g, ' ');
            } else if (type !== 'snake') {
                str = str.replace(/_.{1}/g, char => char[1].toUpperCase());
                if (type === 'pascal') {
                    str = str.replace(/^.{1}/, char => char.toUpperCase());
                }
            }

            navigator.clipboard.writeText(str);
        });

        translated.addEventListener('click', e => {
            if (! e.target.matches('.translated-text')) { return; }

            const range = new Range();
            range.selectNodeContents(e.target);

            const selection = document.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);
        }, true);

        (new MutationObserver(mrArr => {
            const isCaseSnake = document.querySelector('#header .casing-nav-item .case-option.active_ .title').innerText.trim() === 'snake_case';

            let html = '';
            translated.querySelectorAll('.node-sentence').forEach(n => {
                const height = n.closest('.wrapper').clientHeight;
                const str = Array.from(n.querySelectorAll('.translated-text-wrapper, .translated-delimiter')).map(n => n.innerText.trim()).join('');
                if (str) {
                    html += `<div style="height: ${height}px; position: relative;" data-value="${str}">
  <div><button type="button" data-case="snake">copy</button></div>
　<div class="bl_changeCase">
    <div><button type="button" data-case="pascal">Pascal</button></div>
    <div><button type="button" data-case="camel">camel</button></div>
    <div><button type="button" data-case="usnake">SNAKE</button></div>
    <div><button type="button" data-case="hyphenation">ﾊｲﾌﾈｰｼｮﾝ</button></div>
    <div><button type="button" data-case="no-converting">変換なし</button></div>
  </div>
</div>`;
                } else {
                    html += `<div style="height: ${height}px;">&nbsp;</div>`;
                }
            });

            copyArea.classList.toggle('is_caseSnake', isCaseSnake);
            copyArea.innerHTML = html;
        })).observe(translated, {childList: true, subtree: true});
    });
})();
```