62

设置

所以,我有一个内容可编辑的 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 中他们喜欢的任何位置。他们需要能够在段落之间移动它,甚至在段落内——如果他们愿意的话,可以在两个词之间移动。

是什么给了我希望

请记住——在内容可编辑中,普通的旧<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');
    });

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

问题是我找不到正确的放置位置,或任何插入它的方法。我一直希望找到某种“dropLocation”对象来将我的花哨的盒子倾倒到dropEvent.dropLocation.content=myFancyBoxHTML;其中,或者至少是某种放置位置值,以便找到我自己的方式将内容放在那里? 我得到了什么吗?

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

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

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

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

因为选择插入符号在拖动期间可见,所以我制定了一个计划。

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

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

呵呵。那么我错过了什么?它是如何完成的?

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

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


狩猎继续(原帖后编辑)


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

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

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

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

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

那么这里发生了什么?我感觉到我看到的小选择插入符号根本不是选择插入符号!我觉得是骗子!

经进一步检查!

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

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

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

哎呀,这是否意味着我应该根据clientXclientY自己弄清楚?自己使用鼠标坐标确定选择插入符号的位置?可怕的!!

明天我会考虑这样做——除非我自己,或者这里的其他人读到这个,可以找到一个理智的解决方案:)

4

4 回答 4

41

龙滴

我做了很多荒谬的摆弄。所以,这么多 jsFiddleing。

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

jsFiddle:追逐 Moskal 的 Dragon Drop

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

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

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

我的解决方案

  • 首先,我将 CSS 应用于可拖动对象(fancybox)——我们需要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,我为dragleaveand绑定了一个处理程序dropdragleave 处理程序仅适用于 Firefox,因为在 Firefox 中,当您尝试将其拖动到 contenteditable 之外时,拖放将起作用(Chrome 默认拒绝您),因此它会针对 Firefox-only 执行快速检查relatedTarget呵呵。

Chrome 和 Firefox 获取 Range 对象的方式不同,因此必须努力为每个浏览器在 drop 事件中做不同的事情。Chrome 基于鼠标坐标构建范围 (是的,没错),但 Firefox 在事件数据中提供了它。document.execCommand('insertHTML',false,blah)原来是我们如何处理下降。哦,我忘了提——我们不能dataTransfer.getData()在 Chrome 上使用来获取我们的 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();
});

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

谢谢大家。//追赶。

于 2013-02-05T04:54:49.550 回答
3

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

于 2018-10-12T00:05:02.403 回答
0
  1. 事件拖动开始;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);
于 2014-08-10T09:28:57.460 回答
0

总结上面的答案,关键是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>

于 2021-10-24T01:40:45.837 回答