精确的拖放在一个contenteditable [英] Precise Drag and Drop within a contenteditable

查看:342
本文介绍了精确的拖放在一个contenteditable的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

设置



所以,我有一个contenteditable div - 我正在做一个WYSIWYG编辑器:粗体,斜体,格式化,

 < a class =fancyhref =i。 jpgtarget =_ blank> 
< img alt =src =i.jpg/>
可选标题在这里!
< / a>

用户通过对话框添加这些花哨的图片:他们填写详细信息,上传图像,然后很像其他编辑器功能,我使用 document.execCommand('insertHTML',false,fancy_image_html); 在用户的选择。 / p>

所需功能



所以,现在我的用户可以在一个花哨的图像中移动它。
用户需要能够点击和拖动图像(花式框和所有),将它放在任何地方,他们在可编辑内。



什么给我希望



请注意 - 在一个易于理解的简单旧 < img> 标签已经被用户祝福 - 具有这种可爱的拖放能力。默认情况下,您可以随时拖放 < img>



因此,考虑到这个默认行为已经在我们的上如此狡猾地工作, img> 好友 - 我只想扩展这个行为有点包括一些更多的HTML - 这似乎应该是容易可能的事情。



我的努力这么远



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

 < a class =fancy[...] draggable =truecontenteditable =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();
})
。 ',function(event){
//console.log('dragend');
});

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



问题是找不到正确的放置位置,或任何方式我希望找到某种'dropLocation'对象来转储我的花式框,像 dropEvent.dropLocation .content = myFancyBoxHTML; ,或者,至少,某种类型的放置位置值可以找到我自己的方式把内容放在那里?
我给了什么吗?



我完全错了吗?



我试图使用 document.execCommand('insertHTML',false,content) ; 就像我预期我应该能够,但不幸的是我没有在这里,因为选择插入位置不是精确的位置,我希望。

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



通过一些实验,我在drop事件中尝试了execCommand-insertHTML,事件 - 不插入drop-selection-caret的HTML,而是使用在拖动操作之前选择的任何位置。





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



这没有工作,当然:我无法获得选择标记插入在显式可见的选择插入符号计划 - 再次,execCommand-insertedHTML放置自己,无论选择插入符号,在拖动操作之前。



Huff 。那么我错过了什么?如何完成?



如何获取或插入拖放操作的确切位置?
我觉得这很明显是一个常见的拖放操作 - 当然,我必须忽略一些重要的和明确的细节吗?我甚至需要深入JavaScript,或者也许有一个方法来做这只是与属性,如draggable,droppable,contenteditable和一些fancydancy CSS3?



我仍然在寻找 - 仍然修改 - 我会发现我一直在失败的时候发布:)



非常感谢阅读;任何帮助都非常感谢:)



// Chase。





$ b b


Hunt继续(原始帖子编辑)






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

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

查看选择插入位置实际上在哪里。我将其拖入 dragover 事件,这是我在选择插入符号时可见的可编辑内容之间的可编辑内容。



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



这是一个勇敢的努力。非常感谢Farrukh。



这里发生了什么事?我得到的感觉是,小选择插入点看到跳跃,不是选择插入!我认为这是一个冒险者!



进一步检查!



结果,这是一个冒险者!在整个拖动操作过程中,真实选择插入符号保持原样!



我在阅读 MDN Drag and Drop Docs ,and found this:


自然,您可能需要将拖放区事件。 您可以像使用其他鼠标事件一样使用事件的clientX和clientY属性来确定鼠标指针的位置。


Yikes,这是否意味着我应该根据 clientX clientY ?使用鼠标坐标来确定选择插入符号的位置?恐怕!!



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



感谢大家。我明天更新。 // Chase。

解决方案

Dragon Drop



一个可笑的数量的fiddling。所以,这么多的jsfiddling。



这不是一个健壮或完整的解决方案;我可能永远不会想出一个。如果任何人有任何更好的解决方案,我都耳朵 - 我不想这样做,这是唯一的方式,我已经能够揭露到目前为止。以下jsFiddle和我即将呕吐的信息在我的特定实例中为我的特定版本的Firefox和Chrome在我的特定WAMP设置和计算机上工作。不要哭泣我当它不工作在你的网站。



jsFiddle:Chase Moskal's Dragon Drop



所以,我无聊的女朋友的大脑,她认为我一直说龙滴,当真的,我只是说拖放。它陷入了,所以这就是我为处理这些拖放情况而创建的我的小的JavaScript好友。



结果 - 位的噩梦。 HTML5 Drag-and-Drop API即使乍一看也是可怕的。然后,你几乎热身,当你开始理解并接受它的工作方式。 。然后你意识到一个可怕的噩梦是什么,因为你了解Firefox和Chrome如何以自己的特殊方式来处理这个规范,似乎完全忽略了你的所有需求。你会发现自己在问这样的问题:等待,什么元素甚至被拖动现在?我如何获取这些信息?我如何取消这个拖动操作?我如何停止这个浏览器的独特的默认处理这种情况? ...你的问题的答案:你是自己的,LOSER!保持黑客的东西,直到有事情!



完成在多个contenteditable之间,周围和之间准确地拖放任意HTML元素(注意:我不会全面深入每一个细节,你必须看看因为我的时间有限)



我的解决方案




  • 首先,我将CSS应用到draggables(fancybox) - 我们需要 user-select:none; user-drag:element; ,然后在花式框中的图像上特别地 user-drag:none; 其他元素,为什么不?)。不幸的是,这对于Firefox来说是不够的,Firefox需要在图片上显式设置属性 draggable =false,以防止它被拖动。

  • 接下来,我将属性 draggable =true dropzone =copy



对于拖曳事件(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,我绑定了 dragleave drop dragleave处理程序仅对Firefox存在,如在Firefox中,拖放会工作(Chrome默认拒绝)当您尝试将其拖到contenteditable外部时,它会对仅Firefox relatedTarget 执行快速检查。 Huff。



Chrome和Firefox有不同的获取Range对象的方法,在drop事件中为每个浏览器执行不同的操作。 Chrome会根据鼠标坐标 (右图)创建一个范围,但Firefox会在事件数据中提供此范围。 document.execCommand('insertHTML',false,blah)原来是我们如何处理drop。 OH,我忘了提及 - 我们不能使用 dataTransfer.getData()在Chrome上获取我们的dragstart设置HTML - 它似乎是某种奇怪的错误在本规范中。火狐浏览器调用它的规则,它提供了数据反正 - 但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 selector).get(0).focus(); // essential
document.execCommand('insertHTML',false,'< param name =dragonDropMarker/>'+DD.dropLoad);
sel.removeAllRanges();

//使用dragonDropMarker验证
var $ DDM = $('param [name =dragonDropMarker]');
var insertSuccess = $ DDM.length> 0;
if(insertSuccess){
$(DD。$ draggables.selector).filter('[dragged]')。
$ DDM.remove();
}

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

好了,我很生气。我写了所有我想知道的。



感谢大家。 // Chase。


The Setup

So, I have a contenteditable div -- I'm making a WYSIWYG editor: bold, italics, formatting, whatever, and most lately: inserting fancy images (in a fancy box, with a caption).

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

The user adds these fancy images with a dialog I present them with: they fill out the details, upload the image, and then much like the other editor functions, I use document.execCommand('insertHTML',false,fancy_image_html); to plop it in at the user's selection.

Desired Functionality

So, now that my user can plop in a fancy image -- they need to be able to move it around. The user needs to be able to click and drag the image (fancy box and all) to place it anywhere that they please within the contenteditable. They need to be able to move it between paragraphs, or even within paragraphs -- between two words if they want.

What Gives me Hope

Keep in mind -- in a contenteditable, plain old <img> tags are already blessed by the user-agent with this lovely drag-and-drop capability. By default, you can drag and drop <img> tags around wherever you please; the default drag-and-drop operation behaves as one would dream.

So, considering how this default behavior already works so smashingly on our <img> buddies -- and I only want to extend this behaviour a little bit to include a tad more HTML -- this seems like something that should be easily possible.

My Efforts Thus Far

First, I set up my fancy <a> tag with the draggable attribute, and disabled contenteditable (not sure if that's necessary, but it seems like it may as well be off):

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

Then, because the user could still drag the image out of the fancy <a> box, I had to do some CSS. I'm working in Chrome, so I'm only showing you the -webkit- prefixes, though I used the others too.

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

Now the user can drag the whole fancy box, and the little partially-faded click-drag representation image reflects this -- I can see that I'm picking up the entire box now :)

I've tried several combinations of different CSS properties, the above combo seems to make sense to me, and seems to work best.

I was hoping that this CSS alone would be enough for the browser to use the entire element as the draggable item, automagically granting the user the functionality I've been dreaming of... It does however, appear to be more complicated than that.

HTML5's JavaScript Drag and Drop API

This Drag and Drop stuff seems more complicated than it needs to be.

So, I started getting deep into DnD api docs, and now I'm stuck. So, here's what I've rigged up (yes, 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');
    });

So here's where I'm stuck: This almost completely works. Almost completely. I have everything working, up until the very end. In the drop event, I now have access to the fancy box's HTML content that I'm trying to have inserted at the drop location. All I need to do now, is insert it at the correct location!

The problem is I can't find the correct drop location, or any way to insert to it. I've been hoping to find some kind of 'dropLocation' object to dump my fancy box into, something like dropEvent.dropLocation.content=myFancyBoxHTML;, or perhaps, at least, some kind of drop location values with which to find my own way to put the content there? Am I given anything?

Am I doing it completely wrong? Am I completely missing something?

I tried to use document.execCommand('insertHTML',false,content); like I expected I should be able to, but it unfortunately fails me here, as the selection caret is not located at the precise drop location as I'd hope.

I discovered that if I comment out all of the event.preventDefault();'s, the selection caret becomes visible, and as one would hope, when the user prepares to drop, hovering their drag over the contenteditable, the little selection caret can be seen running along between characters following the user's cursor and drop operation -- indicating to the user that the selection caret represents the precise drop location. I need the location of this selection caret.

With some experiments, I tried execCommand-insertHTML'ing during the drop event, and the dragend event -- neither insert the HTML where the dropping-selection-caret was, instead it uses whatever location was selected prior to the drag operation.

Because the selection caret is visible during dragover, I hatched a plan.

For awhile, I was trying, in the dragover event, to insert a temporary marker, like <span class="selection-marker">|</span>, just after $('.selection-marker').remove();, in an attempt for the browser to constantly (during dragover) be deleting all selection markers and then adding one at the insertion point -- essentially leaving one marker wherever that insertion point is, at any moment. The plan of course, was to then replace this temporary marker with the dragged content which I have.

None of this worked, of course: I couldn't get the selection-marker to insert at the apparently visible selection caret as planned -- again, the execCommand-insertedHTML placed itself wherever the selection caret was, prior to the drag operation.

Huff. So what have I missed? How is it done?

How do I obtain, or insert into, the precise location of a drag-and-drop operation? I feel like this is, obviously, a common operation among drag-and-drops -- surely I must have overlooked an important and blatant detail of some kind? Did I even have to get deep into JavaScript, or maybe there's a way to do this just with attributes like draggable, droppable, contenteditable, and some fancydancy CSS3?

I'm still on the hunt -- still tinkering around -- I'll post back as soon as I find out what I've been failing at :)

Thanks so much for reading; any help at all is greatly appreciated :)

//Chase.



The Hunt Continues (edits after original post)


Farrukh posted a good suggestion -- use:

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

To see where the selection caret actually is. I plopped this into the dragover event, which is when I figure the selection caret is visibily hopping around between my editable content in the contenteditable.

Alas, the Range object that is returned, reports offset indices that belong to the selection caret prior to the drag-and-drop operation.

It was a valiant effort. Thanks Farrukh.

So what's going on here? I am getting the sensation that the little selection caret I see hopping around, isn't the selection caret at all! I think it's an imposter!

Upon Further Inspection!

Turns out, it is an imposter! The real selection caret remains in place during the entire drag operation! You can see the little bugger!

I was reading MDN Drag and Drop Docs, and found this:

Naturally, you may need to move the insertion marker around a dragover event as well. You can use the event's clientX and clientY properties as with other mouse events to determine the location of the mouse pointer.

Yikes, does this mean I'm supposed to figure it out for myself, based on clientX and clientY?? Using mouse coordinates to determine the location of the selection caret myself? Scary!!

I'll look into doing so tomorrow -- unless myself, or somebody else here reading this, can find a sane solution :)

Thanks everybody. I'll update tomorrow. //Chase.

解决方案

Dragon Drop

I've done a ridiculous amount of fiddling. So, so much jsFiddling.

This is not a robust, or complete solution; I may never quite come up with one. If anyone has any better solutions, I'm all ears -- I didn't want to have to do it this way, but it's the only way I've been able to uncover so far. The following jsFiddle, and the information I am about to vomit up, worked for me in this particular instance with my particular versions of Firefox and Chrome on my particular WAMP setup and computer. Don't come crying to me when it doesn't work on your website. This drag-and-drop crap is clearly every man for himself.

jsFiddle: Chase Moskal's Dragon Drop

So, I was boring my girlfriend's brains out, and she thought I kept saying "dragon drop" when really, I was just saying "drag-and-drop". It stuck, so that's what I call my little JavaScript buddy I've created for handling these drag-and-drop situations.

Turns out -- it's a bit of a nightmare. The HTML5 Drag-and-Drop API even at first glance, is horrible. Then, you almost warm up to it, as you start to understand and accept the way it's supposed to work.. Then you realize what a terrifying nightmare it actually is, as you learn how Firefox and Chrome go about this specification in their own special way, and seem to completely ignore all of your needs. You find yourself asking questions like: "Wait, what element is even being dragged right now? How to do I get that information? How do I cancel this drag operation? How can I stop this particular browser's unique default handling of this situation?"... The answers to your questions: "You're on your own, LOSER! Keep hacking things in, until something works!".

So, here's how I accomplished Precise Drag and Drop of Arbitrary HTML Elements within, around, and between multiple contenteditable's. (note: I'm not going fully in-depth with every detail, you'll have to look at the jsFiddle for that -- I'm just rambling off seemingly relevant details that I remember from the experience, as I have limited time)

My Solution

  • First, I applied CSS to the draggables (fancybox) -- we needed user-select:none; user-drag:element; on the fancy box, and then specifically user-drag:none; on the image within the fancy box (and any other elements, why not?). Unfortunately, this was not quite enough for Firefox, which required attribute draggable="false" to be explicitly set on the image to prevent it from being draggable.
  • Next, I applied attributes draggable="true" and dropzone="copy" onto the contenteditables.

To the draggables (fancyboxes), I bind a handler for dragstart. We set the dataTransfer to copy a blank string of HTML ' ' -- because we need to trick it into thinking we are going to drag HTML, but we are cancelling out any default behavior. Sometimes default behavior slips in somehow, and it results in a duplicate (as we do the insertion ourselves), so now the worst glitch is a ' ' (space) being inserted when a drag fails. We couldn't rely on the default behavior, as it would fail to often, so I found this to be the most versatile solution.

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

To the dropzones, I bind a handler for dragleave and drop. The dragleave handler exists only for Firefox, as in Firefox, the drag-drop would work (Chrome denies you by default) when you tried to drag it outside the contenteditable, so it performs a quick check against the Firefox-only relatedTarget. Huff.

Chrome and Firefox have different ways of acquiring the Range object, so effort had to be put in to do it differently for each browser in the drop event. Chrome builds a range based on mouse-coordinates (yup that's right), but Firefox provides it in the event data. document.execCommand('insertHTML',false,blah) turns out to be how we handle the drop. OH, I forgot to mention -- we can't use dataTransfer.getData() on Chrome to get our dragstart set HTML -- it appears to be some kind of weird bug in the specification. Firefox calls the spec out on it's bullcrap and gives us the data anyways -- but Chrome doesn't, so we bend over backwards and to set the content to a global, and go through hell to kill all the default behavior...

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

Okay, I'm sick of this. I've wrote all I want to about this. I'm calling it a day, and might update this if I think of anything important.

Thanks everybody. //Chase.

这篇关于精确的拖放在一个contenteditable的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆