JavaScript マウスをドラッグして描いた円の半径で数値を入力

![c210828.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/eb8c7f23-d19e-845a-6ea6-265845ef5775.gif)

#### [設置デモ](https://akebi.jp/temp/radius_input/) ####

(スマホには対応していません)

#### JavaScript ####

```js:radius_input.js
'use strict';
const radiusInput = (function() {

  const _p = { status: 0 };
  const _el = {};
  const _st = {};

  const defaultOptions = {
    scale: 1,
    decimalplaces: 0,
    offset: 0,
    max: Infinity,
    min: -Infinity,
  };

  window.addEventListener('DOMContentLoaded', function() {
    _el.body = document.querySelector('body');

    // 円表示用要素
    _el.body.insertAdjacentHTML('beforeend', '<div></div>');
    _el.circle = _el.body.lastChild;
    _st.cs = _el.circle.style;
    _st.cs.position = 'fixed';
    _st.cs.boxSizing = 'border-box';
    _st.cs.border = '1px #888 solid';
    _st.cs.backgroundColor = '#fff1';
    _st.cs.borderRadius = '50%';
    _st.cs.zIndex = '9999';
    _st.cs.display = 'none';

    // 始点ポインタ表示用要素
    _el.body.insertAdjacentHTML('beforeend', '<div></div>');
    _el.startPoint = _el.body.lastChild;
    _st.sps = _el.startPoint.style;
    _st.sps.position = 'absolute';
    _st.sps.boxSizing = 'border-box';
    _st.sps.width = _st.sps.height = '8px';
    _st.sps.backgroundColor = '#fff';
    _st.sps.border = '1px solid #000';
    _st.sps.zIndex = '9998';
    _st.sps.display = 'none';

    // 動作時用style マウスポインタ形状変更 他
    _el.body.insertAdjacentHTML('beforeend',
      '<style>*{cursor:crosshair;user-select:none;-webkit-user-select:none;-ms-user-select:none;}</style>');
    _el.cursor = _el.body.lastChild;
    _el.cursor.disabled = true; // 非動作時はdisabled
  });

  // ポインタ移動時
  window.addEventListener('mousemove', function(e) {
    if(_p.status < 2) return;
    // ドラッグ開始点からの半径をもとに円表示及び対象要素へ値を反映
    const r = Math.hypot(_p.x - e.pageX, _p.y - e.pageY);
    _st.cs.width = _st.cs.height = (r * 2) + 'px';
    _st.cs.left = (_p.x - 1 - _el.circle.clientWidth / 2 - window.pageXOffset) + 'px';
    _st.cs.top = (_p.y - 1 - _el.circle.clientHeight / 2 - window.pageYOffset) + 'px';
    _p.target.textContent = _p.target.value =
      (Math.max(Math.min(_p.offset + r * _p.scale, _p.max), _p.min)).toFixed(_p.decimalplaces);
  });

  // ドラッグ開始時
  window.addEventListener('mousedown', function(e) {
    if(_p.status !== 1) return;
    // 右クリック時は動作解除
    if(e.buttons & 2) {
      release();
      addDisableContextmenu();
      return;
    }

    // 始点保存及び始点ポインタ表示
    _st.cs.display = 'block';
    _p.status = 2;
    _p.x = e.pageX;
    _p.y = e.pageY;

    _st.sps.display = 'block';
    _st.sps.left = (_p.x - 1 - _el.startPoint.clientWidth / 2) + 'px';
    _st.sps.top = (_p.y - 1 - _el.startPoint.clientHeight / 2) + 'px';
  });

  // ドラッグ終了時
  window.addEventListener('mouseup', function() {
    removeDisableContextmenu();
    if(_p.status === 0) return;
    release();
  });

  // 動作終了処理
  function release() {
    _st.cs.display = 'none';
    _st.sps.display = 'none';
    _p.status = 0;
    delete _p.target;
    _el.cursor.disabled = true;
  }

  // コンテキストメニュー表示抑制
  function addDisableContextmenu() {
    if(_p.dcm === undefined) {
      window.addEventListener('contextmenu', _p.dcm = function(e) {
        e.preventDefault();
      });
    }
  }

  // コンテキストメニュー表示抑制解除
  function removeDisableContextmenu() {
    setTimeout(function() {
      if(_p.dcm !== undefined) {
        window.removeEventListener('contextmenu', _p.dcm);
        delete _p.dcm;
      }
    }, 100);
  }

  return {
    set: function(element, options) {
      // 対象要素クリック時
      element.addEventListener('click', function() {
        // 初期値設定
        _p.status = 1;
        _st.cs.width = _st.cs.height = 0;
        _st.cs.top = '-10px';
        _p.target = element;
        _el.cursor.disabled = false;

        for(let key in defaultOptions) {
          _p[key] = defaultOptions[key];
        }

        // オプション反映
        if(typeof options === 'object') {
          for(let key in options) {
            const lKey = key.toLowerCase();
            if(defaultOptions[lKey] !== undefined && !isNaN(options[key])) {
              _p[lKey] = + options[key];
            }
          }
          _p.decimalplaces = Math.min(20, Math.max(0, _p.decimalplaces));
        }
      });
    },
  }
}());
```

#### 使用例 ####

```html:index.html
<!DOCTYPE html>
<html lang='ja'>
  <head>
    <meta charset='utf-8'>
    <title>ドラッグ半径で数値入力</title>
    <script src='radius_input.js'></script>
  </head>
  <body>
    <input type='text' id='test1'><br>
    <input type='text' id='test2'><br>
    <div id='test3'>****</div>

    option指定例<br>
    <input type='text' id='test4'> scale:10<br>
    <input type='text' id='test5'> decimalPlaces:1<br>
    <input type='text' id='test6'> scale:0.1, decimalPlaces:2<br>
    <input type='text' id='test7'> offset:-100, max:0, scale:0.1<br>
    <input type='text' id='test8'> offset: new Date().getFullYear(), min: 1900, scale:-0.1<br>

    <script>
      radiusInput.set(document.getElementById('test1'));
      radiusInput.set(document.getElementById('test2'));
      radiusInput.set(document.getElementById('test3'));

      radiusInput.set(document.getElementById('test4'), {scale: 10});
      radiusInput.set(document.getElementById('test5'), {decimalPlaces: 1});
      radiusInput.set(document.getElementById('test6'), {scale: 0.1, decimalPlaces: 2});
      radiusInput.set(document.getElementById('test7'), {offset: -100, max: 0, scale: 0.1});
      radiusInput.set(document.getElementById('test8'), {offset: new Date().getFullYear(), min: 1900, scale:-0.1});
    </script>
  </body>
</html>
```

#### 設定方法 ####

**radiusInput.set( 対象要素 [,オプション] );**

オプションは以下のプロパティをオブジェクトで指定します。
**scale** ドラッグした長さを値として反映する際の倍率 (デフォルト: 1)
**decimalPlaces** 小数の桁数 (デフォルト: 0)
**offset** 開始値のオフセット (デフォルト: 0)
**max** 最大値 (デフォルト: 上限なし)
**min** 最小値 (デフォルト: 下限なし)

----