精确的拖放在一个令人满意的

时间:2013-02-03 23:09:38

标签: javascript html5 css3 drag-and-drop draggable

设置

所以,我有一个令人满意的div - 我正在制作一个WYSIWYG编辑器:粗体,斜体,格式化,等等,最近:插入花哨的图像(在一个花哨的盒子里,带有标题)。

<a class="fancy" href="i.jpg" target="_blank">
    <img alt="" src="i.jpg" />
    Optional Caption goes Here!
</a>

用户使用我给他们提供的对话框添加这些花哨的图像:他们填写详细信息,上传图像,然后很像其他编辑器功能,我使用document.execCommand('insertHTML',false,fancy_image_html);将其填入用户和#39;选择。

期望的功能

所以,既然我的用户可以拍摄一个奇特的图像 - 他们需要能够移动它。 用户需要能够单击并拖动图像(花式框和所有图像),以便将其放置在满足其中的任何位置。他们需要能够在段落之间移动,甚至在段落之间 - 如果他们想要的话,可以在两个单词之间移动。

什么给了我希望

请记住 - 在一个令人满意的,简单的旧 <img> 标签已经被用户代理祝福,具有这种可爱的拖放功能。默认情况下,您可以随时随地拖放 <img> 标签;默认的拖放操作就像人们梦寐以求的那样。

所以,考虑一下这个默认行为如何在我们的<img>好友中如此蠢蠢 - 我只想稍微扩展这种行为以包含更多的HTML - 这似乎应该是很容易。

我的努力到目前为止

首先,我使用draggable属性设置了我的花哨<a>标记,并禁用了contenteditable(不确定是否必要,但似乎它可能会关闭):

<a class="fancy" [...] draggable="true" contenteditable="false">

然后,因为用户仍然可以将图像拖出花哨的<a>框,我不得不做一些CSS。我在Chrome上工作,所以我只向您展示-webkit-前缀,但我也使用了其他的。

.fancy {
    -webkit-user-select:none;
    -webkit-user-drag:element; }
    .fancy>img {
        -webkit-user-drag:none; }

现在,用户可以拖动整个花哨的盒子,而小部分褪色的点击拖动表示图像反映了这一点 - 我可以看到我现在正在拾取整个盒子:)

我尝试了几种不同CSS属性的组合,上面的组合似乎对我有意义,并且似乎效果最好。

我希望只有这个CSS足以让浏览器将整个元素用作可拖动项目,自动向用户授予我梦寐以求的功能...... 然而,它确实如此似乎比这更复杂。

HTML5的JavaScript拖放API

这个拖放的东西似乎比它需要的更复杂。

所以,我开始深入研究DnD api文档,现在我被卡住了。所以,这就是我所操作的(是的,jQuery):

$('.fancy')
    .bind('dragstart',function(event){
        //console.log('dragstart');
        var dt=event.originalEvent.dataTransfer;
        dt.effectAllowed = 'all';
        dt.setData('text/html',event.target.outerHTML);
    });

$('.myContentEditable')
    .bind('dragenter',function(event){
        //console.log('dragenter');
        event.preventDefault();
    })
    .bind('dragleave',function(event){
        //console.log('dragleave');
    })
    .bind('dragover',function(event){
        //console.log('dragover');
        event.preventDefault();
    })
    .bind('drop',function(event){
        //console.log('drop');      
        var dt = event.originalEvent.dataTransfer;
        var content = dt.getData('text/html');
        document.execCommand('insertHTML',false,content);
        event.preventDefault();
    })
    .bind('dragend',function(event){ 
        //console.log('dragend');
    });

所以我在这里被困:这几乎完全有效。 几乎完全。我一切正常,直到最后。在drop事件中,我现在可以访问我想要在放置位置插入的奇特框的HTML内容。我现在需要做的就是将它插入正确的位置!

问题是我无法找到正确的放置位置,或以任何方式插入其中。我一直希望找到某种&# 39; dropLocation&#39; 对象将我的花哨框转储到,例如 dropEvent.dropLocation.content=myFancyBoxHTML; ,或者至少是某种丢弃位置值,用于查找我的自己的方式将内容放在那里? 我给了什么吗?

我完全错了吗?我完全错过了什么吗?

我尝试使用 document.execCommand('insertHTML',false,content); ,就像我预期的那样,但是不幸的是我在这里失败了,因为选择插入符号并不位于精确的放置位置,因为我&# 39; d希望。

我发现如果我注释掉所有 event.preventDefault(); ,那么选择插入符号就会变得可见,并且正如人们所希望的那样,当用户准备放弃时,将它们的拖动悬停在可信的上,可以看到在用户的光标和放下操作之后在字符之间运行的小选择插入符号 - 向用户指示选择插入符表示精确的放置位置。 我需要此选择插入符号的位置。

通过一些实验,我在drop事件期间尝试了execCommand-insertHTML,并且dragend事件 - 既没有插入drop-selection-caret所在的HTML,而是使用了之前选择的任何位置。拖动操作。

因为选择插入符号在dragover中可见,所以我制定了一个计划。

有一段时间,我在dragover事件中尝试在<span class="selection-marker">|</span>之后插入一个临时标记,如$('.selection-marker').remove();,以便浏览器不断(在dragover期间)删除所有选择标记然后在插入点添加一个标记 - 在任何时刻基本上留下一个标记,无论该插入点在哪里。当然,计划是用我所拖动的内容替换这个临时标记。

当然,这些都不起作用:我无法按照计划将选择标记插入到明显可见的选择插入符号中 - 再次,execCommand-insertedHTML将自己放置在选择插入符所在的位置,之前拖动操作。

哈夫。那么我错过了什么?怎么做?

如何获取或插入拖放操作的精确位置? 我觉得这显然是拖放操作中常见的操作 - 当然我一定忽略了某种重要且明显的细节?我是否还要深入了解JavaScript,或者有一种方法可以做到这一点,只有像draggable,droppable,contenteditable和一些奇特的CSS3?

我还在狩猎 - 仍在修修补补 - 我一发现我失败了就会发回信息:)


亨特继续(原帖后编辑)


Farrukh发表了一个很好的建议 - 使用:

console.log( window.getSelection().getRangeAt(0) );

查看选择插入符的实际位置。我把它放到了 dragover 事件中,当我认为选择插入符号在我满意的可编辑内容之间可见时。

唉,返回的Range对象会在拖放操作之前报告属于选择插入符的偏移索引。

这是一种勇敢的努力。谢谢Farrukh。

那么这里发生了什么?我感觉到我看到的那个小小的选择插入符号,根本不是选择插入符号!我认为这是一个冒名顶替者!

进一步检查!

原来,这是一个冒名顶替者!在整个拖动操作期间,真实选择插入符号保持不变!你可以看到小家伙!

我正在阅读MDN Drag and Drop Docs,并发现了这个:

  

当然,您可能还需要在dragover事件周围移动插入标记。 您可以像使用其他鼠标事件一样使用事件的clientX和clientY属性来确定鼠标指针的位置。

Yikes,这是否意味着我应该根据 clientX clientY 来解决这个问题?使用鼠标坐标自己确定选择插入符的位置?吓人!!

我明天会这样做 - 除非我自己或其他人在这里阅读本文,否则可以找到一个理智的解决方案:)

3 个答案:

答案 0 :(得分:39)

Dragon Drop

我做了一个荒谬的摆弄。所以,这么多jsFiddling。

这不是一个强大的或完整的解决方案;我可能永远不会想出一个。如果有人有任何更好的解决方案,我会全力以赴 - 我不想这样做,但这是我迄今为止唯一能够发现的方式。下面的jsFiddle,以及我即将呕吐的信息,在我特定的WAMP设置和计算机上使用我的特定版本的Firefox和Chrome在这个特定的实例中为我工作。 不要来哭我在你的网站上不起作用的时候。对于每个男人来说,这种拖放式垃圾显然都非常强大。

<强> jsFiddle: Chase Moskal's Dragon Drop

所以,我的女朋友的脑子很无聊,她觉得我真的说“龙滴”,我只是说“拖放”。它卡住了,所以我称之为我为处理这些拖放情况而创建的JavaScript小伙伴。

原来 - 这有点像噩梦。即使乍一看,HTML5拖放API也很糟糕。然后,当你开始理解并接受所谓的工作方式时,你几乎热身了。然后你意识到它实际上是一个多么可怕的噩梦,因为你了解Firefox和Chrome如何以他们自己的特殊方式处理这个规范,并且似乎完全忽略了你的所有需求。你会发现自己会问这样的问题:“等等,现在甚至被拖动了什么元素?如何获取该信息?如何取消此拖动操作?如何阻止此特定浏览器对此情况的唯一默认处理?” ...你的问题的答案:“你是独立的,失败!继续黑客入侵,直到有效!”

所以,这就是我如何完成精确拖放任意HTML元素的内部,周围和之间以及多个可信任的内容。 (注意:我不会完全深入到每一个细节,你将不得不看看jsFiddle - 我只是漫无目的地从我的经验中记得看似相关的细节,因为我的时间有限)

我的解决方案

  • 首先,我将CSS应用于draggables(fancybox) - 我们在花式框上需要user-select:none; user-drag:element;,然后在花式框中的图像上特别user-drag:none;(以及任何其他元素,为什么不?)。不幸的是,这对于Firefox来说已经不够了,它需要在图像上明确设置属性draggable="false"以防止它被拖动。
  • 接下来,我将属性draggable="true"dropzone="copy"应用于了满足要求的人。

对于draggables(fancyboxes),我为dragstart绑定了一个处理程序。我们设置dataTransfer来复制一个空白的HTML字符串'' - 因为我们需要把它变成我们认为我们要拖动HTML,但我们正在取消任何默认行为。有时默认行为会以某种方式滑落,并导致重复(因为我们自己进行插入),所以现在最糟糕的故障是在拖动失败时插入'(空格)。我们无法依赖默认行为,因为它经常失败,所以我发现这是最通用的解决方案。

DD.$draggables.off('dragstart').on('dragstart',function(event){
    var e=event.originalEvent;
    $(e.target).removeAttr('dragged');
    var dt=e.dataTransfer,
        content=e.target.outerHTML;
    var is_draggable = DD.$draggables.is(e.target);
    if (is_draggable) {
        dt.effectAllowed = 'copy';
        dt.setData('text/plain',' ');
        DD.dropLoad=content;
        $(e.target).attr('dragged','dragged');
    }
});

对于dropzones,我绑定了dragleavedrop的处理程序。 dragleave处理程序仅适用于Firefox,因为在Firefox中,拖放功能可以使用(当您尝试将其拖到可信的范围之外时,Chrome会默认拒绝您,因此它会对仅限Firefox的relatedTarget执行快速检查。的哈夫。

Chrome和Firefox有不同的方式来获取Range对象,因此必须为drop事件中的每个浏览器进行不同的操作。 Chrome根据鼠标坐标 (是的,这是正确的)构建范围,但Firefox会在事件数据中提供该范围。 document.execCommand('insertHTML',false,blah)原来是我们处理掉落的方式。 OH,我忘了提及 - 我们不能在Chrome上使用dataTransfer.getData()来获取我们的dragstart集HTML - 它似乎是规范中的某种奇怪的错误。 Firefox在它的标记上调用了规范并且反过来给了我们数据 - 但Chrome没有,所以我们向后弯腰并将内容设置为全局,并通过地狱来杀死所有默认行为...... < / p>

DD.$dropzones.off('dragleave').on('dragleave',function(event){
    var e=event.originalEvent;

    var dt=e.dataTransfer;
    var relatedTarget_is_dropzone = DD.$dropzones.is(e.relatedTarget);
    var relatedTarget_within_dropzone = DD.$dropzones.has(e.relatedTarget).length>0;
    var acceptable = relatedTarget_is_dropzone||relatedTarget_within_dropzone;
    if (!acceptable) {
        dt.dropEffect='none';
        dt.effectAllowed='null';
    }
});
DD.$dropzones.off('drop').on('drop',function(event){
    var e=event.originalEvent;

    if (!DD.dropLoad) return false;
    var range=null;
    if (document.caretRangeFromPoint) { // Chrome
        range=document.caretRangeFromPoint(e.clientX,e.clientY);
    }
    else if (e.rangeParent) { // Firefox
        range=document.createRange(); range.setStart(e.rangeParent,e.rangeOffset);
    }
    var sel = window.getSelection();
    sel.removeAllRanges(); sel.addRange(range);

    $(sel.anchorNode).closest(DD.$dropzones.selector).get(0).focus(); // essential
    document.execCommand('insertHTML',false,'<param name="dragonDropMarker" />'+DD.dropLoad);
    sel.removeAllRanges();

    // verification with dragonDropMarker
    var $DDM=$('param[name="dragonDropMarker"]');
    var insertSuccess = $DDM.length>0;
    if (insertSuccess) {
        $(DD.$draggables.selector).filter('[dragged]').remove();
        $DDM.remove();
    }

    DD.dropLoad=null;
    DD.bindDraggables();
    e.preventDefault();
});

好的,我厌倦了这一点。我已经写了所有我想要的内容。我把它称为一天,如果我想到任何重要的事情,可能会更新它。

谢谢大家。 //大通。

答案 1 :(得分:1)

由于我想在本机JS解决方案中看到这一点,因此我做了一些工作以删除所有jQuery依赖项。希望它可以帮助某人。

首先标记

    <div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
      WAITING  FOR STUFF
    </div>
    <div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
      <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
        Block 1
      </span>
      <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
        Second Blk
      </span>
    </div>

然后是一些帮手

    function addClass( elem, className ){
        var classNames = elem.className.split( " " )
        if( classNames.indexOf( className ) === -1 ){
            classNames.push( className )
        }
        elem.className = classNames.join( " " )
    }
    function selectElem( selector ){
        return document.querySelector( selector )
    }
    function selectAllElems( selector ){
        return document.querySelectorAll( selector )
    }
    function removeElem( elem ){
         return elem ? elem.parentNode.removeChild( elem ) : false
    }

然后是实际方法

    function nativeBindDraggable( elems = false ){
        elems = elems || selectAllElems( '.native_drag' );
        if( !elems ){
            // No element exists, abort
            return false;
        }else if( elems.outerHTML ){
            // if only a single element, put in array
            elems = [ elems ];
        }
        // else it is html-collection already (as good as array)

        for( let i = 0 ; i < elems.length ; i++ ){
            // For every elem in list, attach or re-attach event handling
            elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
            elems[i].ondragstart = function(e){
                if (!e.target.id){
                    e.target.id = (new Date()).getTime();
                }

                window.inTransferMarkup = e.target.outerHTML;
                window.transferreference = elems[i].dataset.transferreference;
                addClass( e.target, 'dragged');
            };
        };
    }

    function nativeBindWriteRegion( elems = false ){
        elems = elems || selectAllElems( '.native_receiver' );
        if( !elems ){
            // No element exists, abort
            return false;
        }else if( elems.outerHTML ){
            // if only a single element, put in array
            elems = [ elems ];
        }
        // else it is html-collection

        for( let i = 0 ; i < elems.length ; i++ ){
            elems[i].ondragover = function(e){
                e.preventDefault();
                return false;
            };
            elems[i].ondrop = function(e){
                receiveBlock(e);
            };
        }
    }

    function receiveBlock(e){
        e.preventDefault();
        let content = window.inTransferMarkup;

        window.inTransferMarkup = "";

        let range = null;
        if (document.caretRangeFromPoint) { // Chrome
            range = document.caretRangeFromPoint(e.clientX, e.clientY);
        }else if (e.rangeParent) { // Firefox
            range = document.createRange();
            range.setStart(e.rangeParent, e.rangeOffset);
        }
        let sel = window.getSelection();
        sel.removeAllRanges(); 
        sel.addRange( range );
        e.target.focus();

        document.execCommand('insertHTML',false, content);
        sel.removeAllRanges();

        // reset draggable on all blocks, esp the recently created
        nativeBindDraggable(
          document.querySelector(
            `[data-transferreference='${window.transferreference}']`
          )
        );
        removeElem( selectElem( '.dragged' ) );
        return false;
    }

最后实例化

nativeBindDraggable();
nativeBindWriteRegion();

下面是起作用的代码段

function addClass( elem, className ){
            var classNames = elem.className.split( " " )
            if( classNames.indexOf( className ) === -1 ){
                classNames.push( className )
            }
            elem.className = classNames.join( " " )
        }
        function selectElem( selector ){
            return document.querySelector( selector )
        }
        function selectAllElems( selector ){
            return document.querySelectorAll( selector )
        }
        function removeElem( elem ){
             return elem ? elem.parentNode.removeChild( elem ) : false
        }
        
      
    	function nativeBindDraggable( elems = false ){
    		elems = elems || selectAllElems( '.native_drag' );
    		if( !elems ){
    			// No element exists, abort
    			return false;
    		}else if( elems.outerHTML ){
    			// if only a single element, put in array
    			elems = [ elems ];
    		}
    		// else it is html-collection already (as good as array)
            
    		for( let i = 0 ; i < elems.length ; i++ ){
    			// For every elem in list, attach or re-attach event handling
    			elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
    			elems[i].ondragstart = function(e){
    				if (!e.target.id){
    					e.target.id = (new Date()).getTime();
    				}

    				window.inTransferMarkup = e.target.outerHTML;
    				window.transferreference = elems[i].dataset.transferreference;
    				addClass( e.target, 'dragged');
    			};
    		};
    	}
        
    	function nativeBindWriteRegion( elems = false ){
    		elems = elems || selectAllElems( '.native_receiver' );
    		if( !elems ){
    			// No element exists, abort
    			return false;
    		}else if( elems.outerHTML ){
    			// if only a single element, put in array
    			elems = [ elems ];
    		}
    		// else it is html-collection
    		
    		for( let i = 0 ; i < elems.length ; i++ ){
    			elems[i].ondragover = function(e){
    				e.preventDefault();
    				return false;
    			};
    			elems[i].ondrop = function(e){
    				receiveBlock(e);
    			};
    		}
    	}
        
        function receiveBlock(e){
    		e.preventDefault();
    		let content = window.inTransferMarkup;
    		
    		window.inTransferMarkup = "";
    		
    		let range = null;
    		if (document.caretRangeFromPoint) { // Chrome
    			range = document.caretRangeFromPoint(e.clientX, e.clientY);
    		}else if (e.rangeParent) { // Firefox
    			range = document.createRange();
    			range.setStart(e.rangeParent, e.rangeOffset);
    		}
    		let sel = window.getSelection();
    		sel.removeAllRanges(); 
    		sel.addRange( range );
    		e.target.focus();
    		
    		document.execCommand('insertHTML',false, content);
    		sel.removeAllRanges();
    		
            // reset draggable on all blocks, esp the recently created
    		nativeBindDraggable(
              document.querySelector(
                `[data-transferreference='${window.transferreference}']`
              )
            );
    		removeElem( selectElem( '.dragged' ) );
    		return false;
    	}


    nativeBindDraggable();
    nativeBindWriteRegion();
        <div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
          WAITING  FOR STUFF
        </div>
        <div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
          <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
            Block 1
          </span>
          <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
            Second Blk
          </span>
        </div>

答案 2 :(得分:0)

  1. event dragstart; dataTransfer.setData("text/html", "<div class='whatever'></div>");
  2. 事件掉落: var me = this; setTimeout(function () { var el = me.element.getElementsByClassName("whatever")[0]; if (el) { //do stuff here, el is your location for the fancy img } }, 0);