当前位置: 首页 > 知识库问答 >
问题:

javascript - 如何在旋转后的父元素中使用draggable拖动元素?

卢出野
2023-10-12

编辑2:在仔细阅读了jquery-ui的源代码,并充分理解了其原理后,我制作出了能够符合我的需求的代码:能够在旋转后的父元素中使用jquery-ui的draggable功能以实现拖动子元素的效果。我希望能分享给大家,或许有其他人有着和我类似的需求,可以提供给您作为参考。

需要写在最前面强调的是,这个功能有一定的局限性,例如旋转的父元素和拖拽的子元素必须都是position:absolute,并且一部分draggable的功能例如snap将可能无法正确工作。另外,由于涉及到了更多计算,这会影响到代码的运行效率。如果这些限制可以满足您的需求,请往下看。

为了完成这个功能,我们首先需要明确一点:jquery中的offset功能对于旋转后的元素而言,只会得到错误的结果,无一例外。如果您想要移动一个旋转后的元素,必须在这个元素外面嵌套一个父元素用于定位offset,并且移动的draggable绑定的对象也必须是这个父元素。不过,只要我们将其高度和宽度设定为0px,位置设定为absolute的话,就不需要担心这个多出来的父元素会影响到我们的元素结构。wrapper的用处是获取元素在旋转前左上角的offset,并结合角度等计算出旋转后的元素的offset,并以这个旋转后的offset来计算元素的位移值
对于我而言,我将其设置为:

HTML:

<div class="wrapper">       <div id="转动的父元素">          <div class="wrapper>              <div id="移动的子元素"></div>            </div>      </div></div>

CSS:

.wrapper{    height:0px;    width:0px;    position:absolute;}

js:

$(旋转的父元素).css({    "transform" : "rotate(45deg)",//注意这里,我将父元素的中心设置为元素的中心,如果您需要以其他位置为中心进行旋转,请注意修改下方的return_playground_wrapperOffset()函数中有关中心点坐标和偏移量的部分    "transform-origin" : "center" )$(移动的子元素).parent(".wrapper").draggable()

随后,我们还需要一些准备工作,例如:
1.我们需要能够获取到“转动的父元素”所转动的角度,在这里我将其记录在父元素内: $("#转动的父元素").attr("angle",转动的角度),并通过return_angle()函数获取这个值

2.我们需要一个额外的函数,用来求得“旋转的父元素”在旋转后的左上角的位置,我的函数如下:

function return_playground_wrapperOffset(){  //父元素wrapper的坐标:  var wrapper = $("#旋转的父元素").parent(".wrapper")  var wrapper_offset = $(wrapper).offset()  //中心点的坐标  var center_x = wrapper_offset.left + $(huabu).width()/2 * return_scale()  var center_y = wrapper_offset.top + $(huabu).height()/2 * return_scale()  //wrapper相对于中心点的偏移量  var delta_x = - $(huabu).width()/2 * return_scale()  var delta_y = - $(huabu).height()/2 * return_scale()  //旋转弧度  var radian = return_angle() * Math.PI / 180  //计算旋转后的相对坐标  var new_delta_x = delta_x * Math.cos(radian) + delta_y * Math.sin(radian)  var new_delta_y = - delta_x * Math.sin(radian) + delta_y * Math.cos(radian)  //将相对坐标加上中心点的坐标,得到旋转后的左上角顶点的坐标  var new_x = center_x + new_delta_y  var new_y = center_y + new_delta_x  return {left:new_x,top:new_y}

}

随后,我修改了jquery-ui的源代码,也就是jquery-ui.js的内容,修改的内容包括两个部分,首先是第约10006行的_refreshOffsets: function( event ) {……}
我将其修改为:

_refreshOffsets: function( event ) {    this.offset = {        top: this.positionAbs.top - this.margins.top,        left: this.positionAbs.left - this.margins.left,        scroll: false,        parent: this._getParentOffset(),        relative: this._getRelativeOffset()    };    //获取父元素转动的角度,并转化为弧度以供三角函数使用    var radian = return_angle() * Math.PI / 180    //旧的偏移量表示的是这个鼠标点击位置与元素左上角的距离,但由于其原理使用到了offset,因此其无法在旋转后正确工作    var old_left = event.pageX - this.offset.left    var old_top = event.pageY - this.offset.top    //不过,我们可以对错误的值进行偏转,依次得到鼠标相对于旋转后的子元素的偏移量,并将其以旋转后的子元素的Left和top的方式进行表示,之所以使用left和top的方式,是因为之后我们在移动元素时,实际操作的是元素的top和left值,这也是为什么在offset无法正确工作的情况下,我们能够通过一些修复来使得draggable能够正确工作,因为其实际的工作原理与offset无关,只是一部分值涉及到了offset的使用    var new_top = old_top * Math.cos(radian) - old_left * Math.sin(radian)     var new_left = old_top * Math.sin(radian) + old_left * Math.cos(radian)     this.offset.click = {        top: new_top,        left: new_left    };},

随后我们还需要修改第约10380行的_generatePosition: function( event, constrainPosition ) {……}函数,修改后的函数如下所示:

_generatePosition: function( event, constrainPosition ) {    //一直到下一个注解前的内容都可以忽略    var containment, co, top, left,        o = this.options,        scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ),        pageX = event.pageX,        pageY = event.pageY;    if ( !scrollIsRootNode || !this.offset.scroll ) {        this.offset.scroll = {            top: this.scrollParent.scrollTop(),            left: this.scrollParent.scrollLeft()        };    }    if ( constrainPosition ) {        if ( this.containment ) {            if ( this.relativeContainer ) {                co = this.relativeContainer.offset();                containment = [                    this.containment[ 0 ] + co.left,                    this.containment[ 1 ] + co.top,                    this.containment[ 2 ] + co.left,                    this.containment[ 3 ] + co.top                ];            } else {                containment = this.containment;            }            if ( event.pageX - this.offset.click.left < containment[ 0 ] ) {                pageX = containment[ 0 ] + this.offset.click.left;            }            if ( event.pageY - this.offset.click.top < containment[ 1 ] ) {                pageY = containment[ 1 ] + this.offset.click.top;            }            if ( event.pageX - this.offset.click.left > containment[ 2 ] ) {                pageX = containment[ 2 ] + this.offset.click.left;            }            if ( event.pageY - this.offset.click.top > containment[ 3 ] ) {                pageY = containment[ 3 ] + this.offset.click.top;            }        }        if ( o.grid ) {            top = o.grid[ 1 ] ? this.originalPageY + Math.round( ( pageY -                this.originalPageY ) / o.grid[ 1 ] ) * o.grid[ 1 ] : this.originalPageY;            pageY = containment ? ( ( top - this.offset.click.top >= containment[ 1 ] ||                top - this.offset.click.top > containment[ 3 ] ) ?                    top :                    ( ( top - this.offset.click.top >= containment[ 1 ] ) ?                        top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : top;            left = o.grid[ 0 ] ? this.originalPageX +                Math.round( ( pageX - this.originalPageX ) / o.grid[ 0 ] ) * o.grid[ 0 ] :                this.originalPageX;            pageX = containment ? ( ( left - this.offset.click.left >= containment[ 0 ] ||                left - this.offset.click.left > containment[ 2 ] ) ?                    left :                    ( ( left - this.offset.click.left >= containment[ 0 ] ) ?                        left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : left;        }        if ( o.axis === "y" ) {            pageX = this.originalPageX;        }        if ( o.axis === "x" ) {            pageY = this.originalPageY;        }    }    //此处开始我们的修正,首先仍然是获取旋转的父元素的旋转角度并转化为弧度    var radian = return_angle() * Math.PI / 180    //通过上面提到的函数,获取旋转后父元素的左上角此时的位置    var wrapper_offset = return_playground_wrapperOffset()    var old_top = pageY - wrapper_offset.top    var old_left = pageX - wrapper_offset.left    //进行修正,请不要忘记“- this.offset.click.left/top”    var new_left = old_left * Math.cos(radian) + old_top * Math.sin(radian)                    - this.offset.click.left    var new_top =  - old_left * Math.sin(radian) + old_top * Math.cos(radian)                    - this.offset.click.top    //将这个函数返回的值修改为如下    return {        top: new_top ,        left: new_left     };},

至此,我们完成了所有的修改,理论上无论您的父元素如何旋转,其都能正确地进行定位,另外,我建议您在进行修改之前将您的jquery-ui.js文件进行备份,以免错误或误差导致您的修改前的内容丢失。最后,如果我的代码能够帮助到你,或是给您启发,这是我的荣幸!

以下为原问题内容,其中提出的猜想没有参考价值,仅作为存档所用

首先我需要道歉,标题中的问题,实际上我已经解决了一部分,我希望得到回答的并不完全是我的题目。

我尝试在一个经过了transform:rotate(45deg) 的旋转的父元素中,使用jquery-ui的draggable拖动其中的子元素,然而jquery-ui的draggable并不支持transform属性,直接使用draggable是不可能的。

事实上,在这之前我也遇到过类似的问题,并且最后通过修改jquery-ui的源文件解决,这一次我也想进行同样的尝试。

我通过修改jquery-ui.js文件中,约10382行的内容,也就是

$.widget( "ui.draggable", $.ui.mouse, {  ……_generatePosition: function( event, constrainPosition ) {……}

中的内容,成功做到了鼠标的移动与对象的移动“同步”,也就是说是鼠标向左移动100px的距离,拖拽的对象也会向左移动100px的距离。只不过我遇到了一些难以解决的bug,因此我需要您的帮助。

我遇到的问题是:当拖拽开始时,对象会“瞬移”一段距离,我必须为position增添一些固定的值才能解决这个问题,然而我并不知道这些固定的值是如何产生的,以及为什么对象会“瞬移”,我希望能解决这些问题

以下是我的代码,其中#playground是我的容器元素,#ball是容器中的可拖拽元素

CSS:

#playground{    width:1000px;    height:1000px;    positition:absolute;    transform:rotate(45deg);    transform-origin:center center;    background-color:white;}#ball{    width:100px;    height:100px;    position:absolute;}

JS:

//下面的是我修改jquery-ui后的代码,修改内容仅限_generatePosition最后的return内容,其余内容均未有任何改动    var old_top = pageY-                  this.offset.click.top -                  this.offset.relative.top -                  this.offset.parent.top +                  (this.cssPosition === "fixed" ?                        -this.offset.scroll.top :                      (scrollIsRootNode ? 0 : this.offset.scroll.top ))    var old_left = pageX-                   this.offset.click.left -                   this.offset.relative.left -                   this.offset.parent.left +                   (this.cssPosition === "fixed" ?                      -this.offset.scroll.left :                      (scrollIsRootNode ? 0 : this.offset.scroll.left ))//以上两个值为原本函数中的返回值,我将其进行了一定的修改,以适配旋转后的主元素    var angle = 45    var radian = angle * Math.PI / 180    var new_left = old_top * Math.sin(radian) + old_left * Math.cos(radian)    var new_top = old_top * Math.cos(radian) - old_left * Math.sin(radian)//这里我必须增添两个值才能使得#ball元素不会瞬移//最终本函数会返回一个位置的值,并使用这个位置的值来修改被拖拽元素的位置,从而使得被拖拽元素能够与鼠标一同移动    return {        top: new_top + 450,        left: new_left - 450    };

我个人的猜想是:瞬移是因为position的初值不正确,最有可能是父元素角度变化导致了原本的old_left和old_top的值出现异常,但是我并没有那么深刻的理解,我不明白为什么这样的事情会发生我也不明白为什么最终我需要增加和减少“450”这个偏差值,我想知道这个偏差值是为什么会产生,如何获得的,因为我希望在后续的功能中增加其他的旋转角度

目前已知的信息是:这个偏差值会随着角度的变化而变化,也会随着#ball的height变化而变化,当角度在0-90°之间时,随着角度增大,或者height增大时,这个偏差值的数值也需要随之增大。有趣的是,width的变化似乎不会产生影响。

感谢你的任何建议,如果我没能描述清楚我的问题,我可以提供更多,谢谢您!

编辑1:经过多次的实验后,我大致得出了45°角时的偏差值规律为(1000 - 100)/2 ,也就是:(父元素的高度 - 子元素的高度)/ 2的值,这个结果让我感到很疑惑,为什么只有高度参与了这个偏差值的变化,宽度为什么不会参与?为什么要除以1/2?我正在尝试其他角度规律,有任何进展都愿意分享给大家,但我仍然希望能获得为什么会产生这样的变化的原因。

编辑2:我想我得到了top的偏差值的计算公式:

var offset_top = ($("#playground").height() - $(this.bindings).height()) * Math.pow(Math.sin(radian),2)

在任何角度下,这个top的偏差值都能准确奏效,但问题是left的偏差值却仍然是一个谜团,无论是 Math.pow(Math.sin(radian),2) 还是 Math.pow(Math.cos(radian),2) 都不是一个完美的值,我感到十分困惑,并且我并不了解为什么这个偏差值会是这样生成的,尤其是将三角函数开平方这件事完全在我的意料之外,无论如何,我仍然需求帮助,谢谢您!

共有1个答案

华知
2023-10-12

下面的例子在 Chrome118 和 FireFox118 中运行良好,复制到一个新的 html 文件再用浏览器打开就可以体验:

<!DOCTYPE html><html lang="zh-CN" charset="utf-8">  <head>    <title>Drag in rotated</title>    <style>      .stage {        position: absolute;        width: 800px;        height: 800px;        outline: solid 1px #2f89ff;        transform-origin: center;      }      .dancer {        position: absolute;        width: 16px;        height: 16px;        cursor: move;        background-color: red;        border-radius: 8px;      }      .inputs {        position: relative;        z-index: 10;      }    </style>  </head>  <body>    <div class="inputs">      <div>        <label>          <span>旋转:</span>          <input            type="number"            name="angle"            id="angle"            max="360"            min="-360"            step="1"            value="0"          />        </label>      </div>      <div>        <label>          <span>缩小:</span>          <input            type="number"            name="scale"            id="scale"            max="1"            min="0.6"            step="0.05"            value="1"          />        </label>      </div>    </div>    <div class="wrapper">      <div id="stage" class="stage">        <div id="dancer" class="dancer" style="top: 120px; left: 120px"></div>      </div>    </div>  </body>  <script>    const metaMatrix = [1, 0, 0, 1, 0, 0];    let actualAngle = 0;    let actualScale = 1;    function applyMatrixToStage() {      const matrix = new DOMMatrixReadOnly(metaMatrix)        .rotate(actualAngle)        .scale(actualScale)        .toString();      stage.style.transform = matrix;    }    function toNormalMatrix({ a, b, c, d, e, f }) {      return {        _value: new Float32Array([a, c, e, b, d, f]),        point([x, y]) {          const [m11, m12, m13, m21, m22, m23] = this._value;          return [m11 * x + m12 * y + m13, m21 * x + m22 * y + m23];        },      };    }    angle.addEventListener("change", ({ target: { value } }) => {      actualAngle = +value;      applyMatrixToStage();    });    scale.addEventListener("change", ({ target: { value } }) => {      actualScale = +value;      applyMatrixToStage();    });    stage.addEventListener("mousedown", ({ target, clientX, clientY }) => {      if (target === dancer) {        const { style } = dancer;        const matrix = new DOMMatrixReadOnly(metaMatrix)          .rotate(actualAngle)          .scale(actualScale);        const startAt = [parseFloat(style.left), parseFloat(style.top)];        const dragCallback = ({          target,          clientX: _clientX,          clientY: _clientY,        }) => {          // 把逆变换应用到拖动的差量即可          const offset = toNormalMatrix(matrix.inverse().toJSON()).point([            _clientX - clientX,            _clientY - clientY,          ]);          style.left = startAt[0] + offset[0] + "px";          style.top = startAt[1] + offset[1] + "px";        };        const cancelDrag = () => {          stage.removeEventListener("mousemove", dragCallback);        };        stage.addEventListener("mousemove", dragCallback);        stage.addEventListener("mouseup", cancelDrag);        stage.addEventListener("mouseleave", cancelDrag);      }    });  </script></html>
 类似资料:
  • 我有以下HTML结构,并将dragenter和dragleave事件附加到

  • 问题内容: 我想知道旋转JavaScript数组的最有效方法是什么。 我想出了这个解决方案,其中一个正数将数组向右旋转,而一个负数向左(): 然后可以使用这种方式: 在下面的评论中指出的那样,我上面的原始版本有一个缺陷,那就是正确的版本(附加返回值允许链接): 是否有可能在JavaScript框架中更紧凑和/或更快速的解决方案?(以下任何一种建议的版本都不会更紧凑或更快速) 有没有内置数组旋转的J

  • 问题内容: 总览 我具有以下HTML结构,并且将和事件附加到了元素上。 问题 当我将文件拖到时,事件将按预期触发。但是,当我将鼠标移到子元素(例如)上时,会为该元素触发该事件,然后为该元素触发该事件。 如果我再次将鼠标悬停在该元素上,则再次触发该事件,这很酷,但是随后为刚刚剩下的子元素触发了该事件,因此执行了该指令,这并不酷。 此行为有问题的原因有两个: 我只附加&,所以我不明白为什么子元素也要附

  • 本文向大家介绍如何使用JavaScript和CSS创建可拖动的HTML元素?,包括了如何使用JavaScript和CSS创建可拖动的HTML元素?的使用技巧和注意事项,需要的朋友参考一下 要使用JavaScript和CSS创建可拖动的HTML元素,代码如下- 示例 输出结果 上面的代码将产生以下输出- 通过拖动来移动div时-

  • 在使用 vue-draggable-next的时候,如何获取到拖动过程中,拖到了哪个元素上? 问题是这样的,我有两个组件,通过一个父组件进行挂载,左侧的drag组件可以拖入到右侧组件,右侧组件有两个draggable的div。我想分别拖入到不同的div中,现在的问题是无法知道我到底拖到了哪个div元素上,导致数据添加不正确,如何解决这个问题?尝试过 add、move事件,但是move事件获取到的是

  • 我使用jqui中的draggable小部件。 我希望当我的draggable元素拖拽到某个区域后,能产生一个新元素“#block”,并且这个新元素能够被draggable的"snap"所识别 我的代码如下: 我发现#block元素能够正确被创建,并且没有任何拼写错误,但是"#ball"元素仍然无法识别#block元素,也无法吸附上去。我尝试使用refreshPoistion:true来更新drag