【问题标题】:Precise Drag and Drop within a contenteditable在 contenteditable 中精确拖放
【发布时间】:2013-01-18 15:55:08
【问题描述】:

设置

所以,我有一个内容可编辑的 div —— 我正在制作一个所见即所得的编辑器:粗体、斜体、格式等,以及最近的:插入精美的图像(在精美的框中,带有标题)。

<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); 在用户选择时将其放入。

所需的功能

所以,现在我的用户可以放入精美的图片——他们需要能够四处移动它。 用户需要能够单击并拖动图像(花哨的框和所有)以将其放置在 contenteditable 中他们喜欢的任何位置。他们需要能够在段落之间移动它,甚至在段落内——如果他们愿意的话,可以在两个词之间移动。

是什么给了我希望

请记住——在一个内容可编辑的、普通的旧 &lt;img&gt; 标签中,用户代理已经为它提供了这种可爱的拖放功能。默认情况下,您可以随意拖放&lt;img&gt;标签;默认的拖放操作表现得如人所愿。

所以,考虑到这种默认行为如何在我们的&lt;img&gt; 好友中如此出色地发挥作用——我只想稍微扩展这种行为以包含更多 HTML——这似乎应该很容易实现.

到目前为止我的努力

首先,我用可拖动属性设置了我喜欢的&lt;a&gt; 标签,并禁用了 contenteditable(不确定是否有必要,但似乎也可以关闭):

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

然后,因为用户仍然可以将图像拖出花哨的&lt;a&gt; 框,所以我不得不做一些 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');
    });

这就是我卡住的地方:这几乎完全有效。 几乎完全。我一切正常,直到最后。在放置事件中,我现在可以访问我试图在放置位置插入的精美盒子的 HTML 内容。我现在需要做的就是将它插入正确的位置!

问题是我找不到正确的放置位置,或任何插入它的方法。我一直希望找到某种'dropLocation' 将我的精美盒子转储到的对象,例如 dropEvent.dropLocation.content=myFancyBoxHTML;,或者至少是某种放置位置值,我可以用它找到自己的方式将内容放在那里? 我得到了什么吗?

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

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

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

通过一些实验,我在 drop 事件和 dragend 事件期间尝试了 execCommand-insertHTML'ing - 既不将 HTML 插入到 drop-selection-caret 所在的位置,而是使用在拖动操作之前选择的任何位置.

因为选择插入符号在拖动过程中是可见的,所以我制定了一个计划。

有一段时间,我试图在拖动事件中插入一个临时标记,例如 &lt;span class="selection-marker"&gt;|&lt;/span&gt;,就在 $('.selection-marker').remove(); 之后,试图让浏览器不断(在拖动期间)删除所有选择标记和然后在插入点添加一个 - 本质上是在任何时候在插入点所在的任何位置留下一个标记。当然,计划是用我拥有的拖动内容替换这个临时标记。

当然,这些都不起作用:我无法按计划将选择标记插入到明显可见的选择插入符处——同样,execCommand-insertedHTML 在拖动之前将自身放置在选择插入符所在的任何位置操作。

呵呵。那么我错过了什么?它是怎么做的?

如何获取或插入拖放操作的精确位置? 我觉得这显然是拖放操作中的常见操作——我肯定忽略了某种重要且明显的细节吗?我什至必须深入了解 JavaScript,或者也许有一种方法可以只使用可拖动、可放置、可内容编辑和一些花哨的 CSS3 等属性?

我仍在寻找——仍在修补——我会在发现我一直失败的地方后立即回复:)


狩猎继续(原帖后编辑)


Farrukh 提出了一个很好的建议——使用:

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

查看选择插入符号的实际位置。我将它放入 dragover 事件中,此时我发现选择插入符号明显在 contenteditable 中的可编辑内容之间跳跃。

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

这是一次勇敢的努力。谢谢法鲁克。

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

经进一步检查!

原来,这是一个冒名顶替者! 真正的选择插入符号在整个拖动操作期间保持原位!你可以看到那个小虫子!

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

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

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

我明天会考虑这样做 -- 除非我自己或其他阅读本文的人能找到一个理智的解决方案 :)

【问题讨论】:

  • +1 问题问得好!
  • 当图像在该点下降时,选择插入符号就是您要插入的位置。不是吗?如果是则使用此stackoverflow.com/questions/1891444/…
  • @FarrukhSubhani:看起来很有希望。我在 dragover 期间 console.log'd window.getSelection().getRangeAt(0); (这是选择插入符号可见的位置),它返回的 Range 对象显示 选择插入符号最初所在的索引值,在拖放操作之前。 谢谢,这是个好主意。

标签: javascript html css drag-and-drop draggable


【解决方案1】:

龙滴

我做了很多可笑的摆弄。所以,这么多的jsFiddleing。

这不是一个稳健或完整的解决方案;我可能永远也想不出一个。如果有人有更好的解决方案,我会全力以赴——我不想这样做,但这是迄今为止我能够发现的唯一方法。下面的 jsFiddle 以及我即将吐出的信息,在我的特定 WAMP 设置和计算机上使用特定版本的 Firefox 和 Chrome 在这个特定实例中为我工作。 别哭了当它在您的网站上不起作用时我。这种拖放式的废话显然是每个人都为自己。

jsFiddle: Chase Moskal's Dragon Drop

所以,我把我女朋友的脑子弄得无聊了,她以为我一直在说“dragon drop”,而实际上,我只是在说“drag-and-drop”。它卡住了,所以这就是我为处理这些拖放情况而创建的 JavaScript 小伙伴。

事实证明,这有点像噩梦。 HTML5 Drag-and-Drop API 乍一看很糟糕。然后,当您开始理解并接受它应该的工作方式时,您几乎对它产生了兴趣。 . 然后你会意识到这实际上是一场多么可怕的噩梦,因为你了解了 Firefox 和 Chrome 如何以它们自己的特殊方式执行此规范,并且似乎完全忽略了你的所有需求。你会发现自己在问这样的问题:“等等,什么元素现在正在被拖动?如何获取该信息?如何取消此拖动操作?如何停止此特定浏览器对这种情况的独特默认处理?” ...您的问题的答案:“您只能靠自己了,失败者!继续破解,直到成功!”。

所以,这就是我如何在多个 contenteditable 内部、周围和之间实现 任意 HTML 元素的精确拖放。 (注意:我不会完全深入每个详细信息,您必须查看 jsFiddle ——我只是在漫无边际地谈论我从经验中记得的看似相关的细节,因为我的时间有限)

我的解决方案

  • 首先,我将 CSS 应用到可拖动对象(花式框)——我们需要花式框上的 user-select:none; user-drag:element;,然后特别是花式框内的图像上的 user-drag:none;(以及任何其他元素,为什么不呢?)。不幸的是,这对于 Firefox 来说还不够,它需要在图像上显式设置属性 draggable="false" 以防止它被拖动。
  • 接下来,我将属性 draggable="true"dropzone="copy" 应用到 contenteditables。

对于可拖动对象(花式框),我为 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 拒绝你通过默认)当您尝试将其拖到 contenteditable 之外时,它会对仅限 Firefox 的relatedTarget 执行快速检查。 呵呵。

Chrome 和 Firefox 获取 Range 对象的方式不同,因此必须努力为每个浏览器在 drop 事件中采取不同的方式。 Chrome 基于鼠标坐标 (是的,没错) 构建了一个范围,但 Firefox 在事件数据中提供了它。 document.execCommand('insertHTML',false,blah) 原来是我们处理跌落的方式。哦,我忘了提——我们不能在 Chrome 上使用dataTransfer.getData() 来获取我们的 dragstart 设置 HTML——这似乎是规范中的某种奇怪的错误。 Firefox 将规范称为废话,并给我们提供了数据——但 Chrome 没有,所以我们向后弯腰,将内容设置为全局,并通过地狱杀死所有默认行为......

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();
});

好吧,我受够了。我已经写了我想写的所有内容。我正在收工,如果我想到任何重要的事情,可能会更新此内容。

谢谢大家。 //追逐。

【讨论】:

  • 您是否在任何许可下发布此解决方案?我注意到你的 jsfiddle 只说它可以免费“学习”,但我想将它合并到我正在从事的(专有)项目中。
  • 这太棒了。谢谢你写得很好的问题和答案。顺便说一句 - Safari 使用与 Chrome 相同的代码:document.caretRangeFromPoint,而 IE 使用oSelection.getRangeAt(0)“正常工作”
  • 我在 IE10 中尝试了 jsFiddle,但它不起作用。我错过了什么吗?
  • 我知道这是一篇较旧的帖子,但是如果您使用 dragStart、dragEnd 来切换 contenteditable div 可编辑属性怎么办。禁用该属性应该允许您像使用任何其他 dnd 上传器一样使用该 div。如果它仍然是一个问题,我希望这会有所帮助:)
  • 我知道那也是一篇旧帖子,但我不明白演示的重点?可拖动的图像但不是文本?为什么不只是jsfiddle.net/crl/haxo1boo/6
【解决方案2】:

因为我想在原生 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();

下面是运行的sn-p

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>

【讨论】:

    【解决方案3】:
    1. 事件拖动开始; dataTransfer.setData("text/html", "&lt;div class='whatever'&gt;&lt;/div&gt;");
    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);

    【讨论】:

      【解决方案4】:

      总结上面的答案,关键是event.parentNodeevent.rangeOffset(firefox)和caretRangeFromPoint(event.clientX, event.clientY)(chrome)。 这是一个最小的例子:

      span {
          border: 1px solid red;
      }
         
      span:before {
         content: "grab ";
         background-color: #0f0;
      }
      <p contenteditable="true"
          ondrop="sel = window.getSelection();
              if (document.caretRangeFromPoint)
                  range = document.caretRangeFromPoint(event.clientX, event.clientY)
              else {
                  sel.collapse(event.rangeParent,event.rangeOffset)
                  range = sel.getRangeAt(0)
              }
              range.insertNode(sp1)"
          ondragover="
              return false"
      >This is a contenteditable paragraph. Grab the green field in the following span 
       <span draggable="True" id="sp1" ondragstart=" 
          event.dataTransfer.setData('text/plain', this.innerText)">span</span>
       and drag it inside this paragraph.
      </p>

      【讨论】:

        猜你喜欢
        • 2012-02-22
        • 2011-03-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-12-23
        • 1970-01-01
        相关资源
        最近更新 更多