Corredor

ウェブ、プログラミングの勉強メモ。

アンケートサイトで使える!ドラッグ・アンド・ドロップで選択した範囲を一括でクリックするブックマークレット

ドラッグ・アンド・ドロップで選択した範囲を一括でクリックするブックマークレットを作った。

アンケートサイトなんかで、大量のラジオボタンやチェックボックスをクリックするのが面倒になった時は、コチラをドウゾ。

サンプル

CodePen のサンプルをドウゾ。

See the Pen Click Selected Range by Neos21 (@Neos21) on CodePen.

画面内をドラッグすると水色の選択領域が見えるので、そのままドラッグしてチェックボックスをいくつか囲んでみよう。ドラッグ・アンド・ドロップを終了した領域にある要素に対して click() イベントを発火させるので、チェックボックスが一括でセットできるだろう。

ブックマークレット用コード

ブックマークレットに使えるコードは以下。

javascript:((e,t,o,a,l,n,p,s,i,d,r,h,c,g,m,u)=>{r=(d=(e=>(e=t.createElement("i"),t.body.appendChild(e),e)))(),h=d(),t[o]("mousedown",t=>{p=!0,r[l].cssText=i+"border:1px solid rgba(0,0,255,.2);background:rgba(99,255,255,.2)",h[l].cssText=i,c=t.pageX,g=t.pageY,m=e.scrollX,u=e.scrollY}),t[o]("mousemove",(e,t,o)=>{p&&(s=!0,t=e.pageX-c,r[l].left=(t<0?c+t:c)+n,o=e.pageY-g,r[l].top=(o<0?g+o:g)+n,r[l].width=Math.abs(t)+n,r[l].height=Math.abs(o)+n)}),t[o]("mouseup",(o,i,d,r,b,f,x,M)=>{if(p=!1,s)for(s=!1,i=o.pageX,d=o.pageY,r=[],f=Math.min(g,d);f<Math.max(g,d);f+=9)for(e.scrollTo(m,u),b=Math.min(c,i);b<Math.max(c,i);b+=9)h[l].top=f+n,h[l].left=b+n,x=h[a](),e.scrollTo(x.left,x.top),x=h[a](),(M=t.elementFromPoint(x.left,x.top))&&!r.includes(M)&&(M.click(),r.push(M))})})(window,document,"addEventListener","getBoundingClientRect","style","px",!1,!1,"position:absolute;top:0;left:0;width:1px;height:1px;pointer-events:none;");

ソースコード

ソースコードは以下。

((win, doc, addEvent, getRect, sty, px, isMouseDown, isDragging, defaultStyle, createElement, rangeElem, pointElem, beginX, beginY, beginScrollX, beginScrollY) => {
  // 要素を生成して返す関数を作る
  createElement = (elem) => {
    elem = doc.createElement('i');
    doc.body.appendChild(elem);
    return elem;
  };
  // 選択範囲を示す要素
  rangeElem = createElement();
  // ポインタ要素
  pointElem = createElement();
  
  // マウスボタン押下
  doc[addEvent]('mousedown', (event) => {
    // マウスボタン押下中
    isMouseDown = true;
    // 選択範囲を消すため初期値を再指定する
    rangeElem[sty].cssText = defaultStyle + 'border:1px solid rgba(0,0,255,.2);background:rgba(99,255,255,.2)';
    pointElem[sty].cssText = defaultStyle;
    // 選択開始位置を控えておく
    beginX = event.pageX;
    beginY = event.pageY;
    // 選択開始時のスクロール位置を控えておく
    beginScrollX = win.scrollX;
    beginScrollY = win.scrollY;
  });
  
  // マウス移動中
  doc[addEvent]('mousemove', (event, x, y) => {
    // マウスボタン押下中の移動なら処理する
    if(isMouseDown) {
      // ドラッグ中
      isDragging = true;
      // 始点より左に移動する場合を考慮して left を制御する
      x = event.pageX - beginX;
      rangeElem[sty].left = (x < 0 ? beginX + x : beginX) + px;
      // 始点より上に移動する場合を考慮して top を制御する
      y = event.pageY - beginY;
      rangeElem[sty].top = (y < 0 ? beginY + y : beginY) + px;
      // 始点と現在の位置の差を絶対値にして幅・高さとして設定する
      rangeElem[sty].width  = Math.abs(x) + px;
      rangeElem[sty].height = Math.abs(y) + px;
    }
  });
  
  // マウスボタンを離した時
  doc[addEvent]('mouseup', (event, endX, endY, clickedElems, x, y, position, elem) => {
    // マウスボタンを離した
    isMouseDown = false;
    // ドラッグ中なら処理する
    if(isDragging) {
      // ドラッグ終了
      isDragging = false;
      // 終了位置を控えておく
      endX = event.pageX;
      endY = event.pageY;
      // クリックした要素を入れておく配列
      clickedElems = [];
      // 左上から 9px ずつズラす (圧縮時に1桁の数値で済ませたいので…)
      // elementFrompoint はスクロールして画面上に要素が見えていないと null になってしまうので、スクロールして対象要素が画面内に表示されている状態にしている
      for(  y = Math.min(beginY, endY); y < Math.max(beginY, endY); y += 9) {
        // 開始位置にスクロールし直す
        win.scrollTo(beginScrollX, beginScrollY);
        for(x = Math.min(beginX, endX); x < Math.max(beginX, endX); x += 9) {
          pointElem[sty].top  = y + px;
          pointElem[sty].left = x + px;
          // 絶対配置したポインタ要素の位置にスクロールする
          position = pointElem[getRect]();
          win.scrollTo(position.left, position.top);
          // ポインタ要素の座標を再度拾い、その位置にある要素を取得する
          position = pointElem[getRect]();
          elem = doc.elementFromPoint(position.left, position.top);
          // クリック済の要素は無視してクリックする
          if(elem && !clickedElems.includes(elem)) {
            elem.click();
            clickedElems.push(elem);
          }
        }
      }
    }
  });
})(
  window,                   // win
  document,                 // doc
  'addEventListener',       // addEvent
  'getBoundingClientRect',  // getRect
  'style',                  // sty
  'px',                     // px
  false,                    // isMouseDown
  false,                    // isDragging
  'position:absolute;top:0;left:0;width:1px;height:1px;pointer-events:none;'  // defaultStyle
  // createElem
  // rangeElem
  // pointElem
  // beginX
  // beginY
  // beginScrollX
  // beginScrollY
);
  • ブックマークレットとして利用するため即時関数の形にしておき、引数を利用してグローバル領域を汚さないようにしている。
  • 予めドラッグ・アンド・ドロップ領域を示すための要素 rangeElem と、クリックする座標を特定するために使用する要素 pointElem を生成しておく。
  • mousedownmousemovemouseup でドラッグ・アンド・ドロップした領域を拾うための処理を管理している。rangeElemposition: absolute で絶対配置し、その topleft が画面左上の始点となる座標、widthheight で終点となる右下の座標を特定できるようにする。
  • mouseup 時に、rangeElem で囲った領域に対して click() イベントを発火させるための処理を行っている。
  • 座標を指定して要素を特定するには elementFromPoint() というメソッドを使う。
    neos21.hatenablog.com
  • だが、elementFromPoint()スクロールが発生すると要素が取得できなくなる。どうもスクロールに対するオフセットを考慮しないといけないようだが、それでも画面内に表示されていないと上手く要素が取得できないようであった。
  • また、画面外の要素をなんとか座標で特定しても、click() イベントを上手く発火させられないことがあった。
  • コレを解消するため、以下の順に処理した。
    1. クリックしたい座標に pointElem 要素を配置する。
    2. 配置した pointElem 要素の座標を getBoundingClientRect() で取得する。この座標値は、ドラッグ中のスクロールを伴い、画面外になっている場合がある。
    3. この座標が画面内に表示されるよう scrollTo() でスクロール移動する。
    4. そのうえで再度 pointElem 要素の座標を getBoundingClientRect() で取得する。
    5. こうして取得した座標に対して elementFromPoint() で要素を拾えば、正しく要素が拾える。
  • ドラッグ領域を示す rangeElempointElem には pointer-events: none を指定しておき、この要素が選択されないようにしておいた。
  • このような処理を、選択領域の左上から右に 9px ずつズラしていき、右端に行ったら下に 9px ズラして…を繰り返す。9px というオフセット値は「大体」なので適当。
  • 一度クリックした要素は配列に控えておき、再度クリックしないようにしておく。

「ブラウザで見えている範囲の要素でないとうまく操作できない」というところが面倒臭かった。

スクロールを伴うドラッグ・アンド・ドロップをサポートしないのであれば、スクロールし直したりする必要もなく、rangeElemtopleft の座標から widthheight の値まで elementFromPoint() を繰り返せばいいだけ。もう少し楽に実装できるのだが…。

とりあえずコレで、大量に領域を選択して、一気にクリックできるようになったので、雑にアンケートサイトの回答ができるようになった。w

成功するポイントサービス

成功するポイントサービス