在 contenteditable 中精确拖放

2022-01-11 00:00:00 drag-and-drop draggable javascript html css

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

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>

用户通过我向他们展示的对话框添加这些精美的图像:他们填写详细信息,上传图像,然后就像其他编辑器功能一样,我使用 document.execCommand('insertHTML',false,fancy_image_html); 在用户选择时将其放入.

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.

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

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.

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

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.

所以,考虑到这种默认行为如何在我们的 <img> 伙伴中如此出色地发挥作用——我只想稍微扩展这种行为以包含更多 HTML——这看起来应该很容易实现.

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.

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

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">

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

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 :)

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

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

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

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.

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

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

所以,我开始深入研究 DnD api 文档,但现在我陷入了困境.所以,这就是我编造的(是的,jQuery):

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

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

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!

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

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?

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

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

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.

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

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.

通过一些实验,我在 drop 事件和 dragend 事件期间尝试了 execCommand-insertHTML'ing - 既不将 HTML 插入到 drop-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.

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

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

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.

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

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.

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

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 :)

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

Farrukh posted a good suggestion -- use:

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

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

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.

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

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

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!

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

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

我在阅读 MDN 拖放文档,发现这个:

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

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

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.

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

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 :)

推荐答案

Dragon Drop

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

Dragon Drop

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

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

jsFiddle: Chase Moskal 的龙滴

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

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.

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

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!".

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

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)

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

对于可拖动对象(花式框),我为 dragstart 绑定了一个处理程序. 我们将 dataTransfer 设置为复制 HTML ' ' 的空白字符串——因为我们需要欺骗它认为我们要拖动 HTML,但我们正在取消任何默认行为.有时默认行为会以某种方式滑入,并导致重复(就像我们自己进行插入一样),所以现在最糟糕的故障是在拖动失败时插入''(空格).我们不能依赖默认行为,因为它经常会失败,所以我发现这是最通用的解决方案.

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

到 dropzones,我为 dragleavedrop 绑定了一个处理程序. Dragleave 处理程序仅适用于 Firefox,就像在 Firefox 中一样,当您尝试将其拖到 contenteditable 之外时,拖放将起作用(Chrome 默认拒绝您),因此它会针对 Firefox-only relatedTarget 执行快速检查.呵呵.

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

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

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

谢谢大家.//追逐.

相关文章