确定从dragenter和dragover事件中拖动的内容是什么。

85

我正在尝试使用HTML5可拖动API(虽然我意识到它存在问题)。到目前为止,我遇到的唯一障碍是我无法找到一种确定在dragoverdragenter事件触发时正在被拖动的内容的方法:

el.addEventListener('dragenter', function(e) {
  // what is the draggable element?
});

我明白可以假设最后一个元素触发了dragstart事件,但是……多点触控的情况就无法使用了。我还试过在 dragstart 中使用 e.dataTransfer.setData 绑定一个唯一标识符,但显然该数据在 dragover/dragenter 中是无法访问的

此数据只有在放置事件期间发生拖放时才可用。

那么,有什么好的想法吗?

更新:截至本文编写时,HTML5 拖放似乎在任何主要移动浏览器中都没有实现,使得多点触控成为一种实际上无效的方式。然而,我希望找到一种在规范的任何实现中都能保证正常工作的解决方案,该规范似乎不排除同时拖动多个元素的可能性。

我在下面发布了一个可行的解决方案,但这只是一个不好的 hack。我仍然希望有更好的答案。


1
通常情况下,您应该能够在函数内部使用“this”属性,该属性链接到触发事件的对象。 - lgomezma
2
@lgomezma 不是的,在dragenter/dragover事件处理程序中,this指向被拖动的元素,而不是正在拖动的元素。它等同于e.target。例如:http://jsfiddle.net/TrevorBurnham/jWCSB/ - Trevor Burnham
我认为微软在最初为IE 5设计和实现拖放功能时,并没有考虑到多点触控。 - Jeffery To
@JefferyTo 我知道这一点,但现在他们的设计已经被编码为标准,我想通过针对WHATWG规范而不仅仅是现有的实现来未雨绸缪。 - Trevor Burnham
已针对规范文件提交了此错误报告 - https://www.w3.org/Bugs/Public/show_bug.cgi?id=23486 - broofa
10个回答

82

我想在这里提供一个非常清晰的答案,以便于所有路过这里的人都能够明确。虽然其他回答已经说过几次,但这是我能够做到的最清楚明了的解释:

dragover 没有权限查看拖放事件中的数据。

这些信息仅在 DRAG_START 和 DRAG_END (drop) 期间可用。

问题在于这并不明显,且在规范或像这样的地方深入阅读之前,会让人感到十分恼火。

解决方法:

作为一种可能的解决方法,我向 DataTransfer 对象添加了特殊的键,并对其进行了测试。例如,为了提高效率,我想在拖动开始时查找一些“放置目标”规则,而不是每次“拖动悬停”发生时都这样做。为此,我将每个规则的标识键添加到 dataTransfer 对象中,并使用 "contains" 测试它们。

ev.originalEvent.dataTransfer.types.includes("allow_drop_in_non_folders")

还有类似的事情。要明确的是,那个“包括”不是一个万能的解决方案,它本身可能成为性能问题。请注意了解您的使用情况和场景。


1
如果您没有支持IE/Edge,这是一个好建议。 IE/Edge仅允许两个类型的值:“文本”或“URL”。 - wawka
这种解决方法甚至可以使用 JSON.stringifyJSON.parse 来传递更多的数据(poc)。尽管非常不优雅。 - Ulysse BN
4
我喜欢直截了当的回答,让人一下子就明白了。这样容易记住 :D - Wilt
1
我理解的没错,这个答案已经过时了,至少部分是。使用DataTransfer.items,可以在dropenter事件处理程序中至少检查被拖曳文件的MIME类型。有一个回答(在下面远处)提到了它。 - cubuspl42

27
我的问题的简短答案是:不行。在 dragenterdragoverdragleave 事件中,WHATWG spec 没有提供对被拖动元素(在规范中称为“源节点”)的引用。

为什么呢?有两个原因:

首先,正如his comment中所指出的那样,WHATWG 规范基于 IE5+ 的拖放实现,这是在多点触控设备之前的。截至本文撰写时,没有主流的多点触控浏览器实现 HTML 拖放。在“单点触控”环境中,可以很容易地在 dragstart 上存储当前拖动元素的全局引用。

其次,HTML拖放功能允许您在多个文档之间拖动元素。这很棒,但也意味着在每个dragenter、dragover或dragleave事件中提供对正在拖动的元素的引用是没有意义的;您无法引用不同文档中的元素。该API的优点是这些事件无论拖动源是否在同一文档或不同文档中都能够正常工作。
但是,在所有拖动事件中提供序列化信息的无能为力,除了通过dataTransfer.types(如我working solution答案中所述),这是API中一个明显的遗漏。我已经submitted a proposal for public data in drag events给WHATWG,并希望您能表达支持。

我曾经遇到过同样的问题,并得出结论,最好的方法是在数据传输中添加自定义 MIME 类型。由于您提供的拖放功能的主要目的是停止事件,因此我认为它需要一些信息来支持这一点。对于许多目的而言,似乎可以说“拖动包含应用程序/我的应用程序,我可以处理它,因此取消事件”。 - Woody
1
我看到了你的提案链接,但我真的不知道在哪里可以阅读答案,甚至表达我的支持。 - Ulysse BN

14

一个(非常不雅观的)解决方案是将选择器作为数据类型存储在dataTransfer对象中。以下是一个示例:http://jsfiddle.net/TrevorBurnham/eKHap/

这里的关键行是:

e.dataTransfer.setData('text/html', 'foo');
e.dataTransfer.setData('draggable', '');

dragoverdragenter 事件中,e.dataTransfer.types 包含字符串 'draggable',这是确定正在被拖动的元素的 ID 所需的信息。(请注意,浏览器显然需要为已知 MIME 类型(如 text/html)设置数据才能使此方法奏效。在 Chrome 和 Firefox 中测试通过。)

这是一个丑陋的 hack,如果有人能给我更好的解决方案,我很乐意给他们提供奖励。

更新: 还有一个值得添加的警告是,除了缺乏优雅之外,规范还规定所有数据类型将转换为小写 ASCII。因此,请注意使用大写字母或 Unicode 的选择器可能会出错。 Jeffery 的解决方案 避开了这个问题。


是的,我也遇到了将键转为小写的问题很长一段时间:( - RRR

7

根据当前的规范,我认为没有任何解决方案是不是“hack”的。请愿 WHATWG 是解决此问题的一种方式 :-)

扩展“(非常不雅观的)解决方案” (演示):

  • Create a global hash of all elements currently being dragged:

    var dragging = {};
    
  • In the dragstart handler, assign a drag ID to the element (if it doesn't have one already), add the element to the global hash, then add the drag ID as a data type:

    var dragId = this.dragId;
    
    if (!dragId) {
        dragId = this.dragId = (Math.random() + '').replace(/\D/g, '');
    }
    
    dragging[dragId] = this;
    
    e.dataTransfer.setData('text/html', dragId);
    e.dataTransfer.setData('dnd/' + dragId, dragId);
    
  • In the dragenter handler, find the drag ID among the data types and retrieve the original element from the global hash:

    var types = e.dataTransfer.types, l = types.length, i = 0, match, el;
    
    for ( ; i < l; i++) {
        match = /^dnd\/(\w+)$/.exec(types[i].toLowerCase());
    
        if (match) {
            el = dragging[match[1]];
    
            // do something with el
        }
    }
    
如果您将拖动哈希保持私有,第三方代码将无法找到原始元素,即使他们可以访问拖动ID。这假定每个元素只能被拖动一次;使用多点触控,我想使用不同的手指多次拖动相同的元素是可能的...

更新: 为了允许在同一元素上进行多次拖动,我们可以在全局哈希中包含一个拖动计数: http://jsfiddle.net/jefferyto/eKHap/2/


哎呀,我没想到同一个元素竟然可以同时被拖动多次。规范似乎没有排除这种可能性。 - Trevor Burnham
@TrevorBurnham 我已更新了我的回答,以允许在相同元素上进行多次拖动,但老实说我不知道规范如何更改以允许多点触控。 - Jeffery To

5

要检查它是否是文件,请使用:

e.originalEvent.dataTransfer.items[0].kind

要检查类型,请使用:

e.originalEvent.dataTransfer.items[0].type

即,我希望只允许上传一种图片格式,如jpg、png、gif或bmp。

var items = e.originalEvent.dataTransfer.items;
var allowedTypes = ["image/jpg", "image/png", "image/gif", "image/bmp"];
if (items.length > 1 || items["0"].kind != "file" || items["0"].type == "" || allowedTypes.indexOf(items["0"].type) == -1) {
    //Type not allowed
}

参考资料:https://developer.mozilla.org/it/docs/Web/API/DataTransferItem

这里介绍的是DataTransferItem接口。该接口可以获取数据传输过程中的文件或文件夹等内容,并提供了相应的属性和方法进行操作处理。它通常与拖放功能一起使用,而且兼容多种浏览器。


我认为在大多数情况下,即使对于Chrome浏览器来说,originalEvent也没有被填充。 - windmaomao
@windmaomao 我已经在Chrome、Edge和Firefox中进行了测试,它们都可以正常运行。 - user2272143
evt.originaEvent 是在使用 jQuery 时的用法。你可能想要做类似 const evt = $evt.originalEvent; 这样的事情来适应 jQuery 并继续你的代码。 - MarkMYoung

3

当拖动开始时,您可以确定正在拖动的内容并将其保存在变量中,以便在触发dragover/dragenter事件时使用:

var draggedElement = null;

function drag(event) {
    draggedElement = event.srcElement || event.target;
};

function dragEnter(event) {
    // use the dragged element here...
};

我在我的问题中提到了这一点。这种方法在多点触控设备上是不可行的,因为多个元素可以同时被拖动。 - Trevor Burnham

3
在“拖动”事件中,将`event.x`和`event.y`复制到一个对象中,并将该对象设置为被拖动元素的扩展属性的值。
function drag(e) {
    this.draggingAt = { x: e.x, y: e.y };
}

dragenterdragleave 事件中,找到其 expando 属性值与当前事件的 event.xevent.y 匹配的元素。
function dragEnter(e) {
    var draggedElement = dragging.filter(function(el) {
        return el.draggingAt.x == e.x && el.draggingAt.y == e.y;
    })[0];
}

为了减少需要查看的元素数量,您可以在 dragstart 事件中将它们添加到数组中或赋予一个类,然后在 dragend 事件中撤销这些操作。
var dragging = [];
function dragStart(e) {
    e.dataTransfer.setData('text/html', '');
    dragging.push(this);
}
function dragEnd(e) {
    dragging.splice(dragging.indexOf(this), 1);
}

http://jsfiddle.net/gilly3/4bVhL/

理论上,这应该是可以工作的。然而,我不知道如何启用触摸设备的拖动功能,因此无法进行测试。这个链接已经适配移动端,但在我的Android设备上,触摸和滑动并没有引发拖动开始的情况。http://fiddle.jshell.net/gilly3/4bVhL/1/show/

编辑:根据我所读到的,HTML5可拖动属性在任何触摸设备上似乎都不受支持。您能够在任何触摸设备上使可拖动属性生效吗?如果不能,多点触控就不是问题,您可以将拖动的元素存储在变量中。


@Trevor - 真的吗?我的jsfiddle在Chrome中对我有效。 - gilly3
@gilly3 你的方法是如何工作的?我没有使用过多点触控设备,但我认为你可以同时拖动多个对象。最后调用拖动事件的元素是否保证是首先调用拖动进入事件的元素?(我的措辞可能不太清晰,但希望你知道我的意思) - Jules
@Jules - 我会期望拖放和拖入事件在多点触控环境(支持HTML5可拖拽)中针对同一元素立即连续调用,正如您所建议的那样。如果情况不是这样,我会感到惊讶。但是,如果保证这种情况(即由规范定义的方式),我会更加惊讶。我的方法是保持一个被拖动元素的数组,并搜索与当前事件位置相同的元素。我知道每个被拖动元素的位置,因为我在drag事件处理程序中更新了元素上的自定义属性。 - gilly3
@gilly3 我明白你的意思。考虑一下,dragenter 只会在拖动后立即检查,因此在检查其他拖动之前立即调用 dragenter 是没有理由不这样做的。 - Jules
@gilly3 规范确保dragdragenter之前始终触发(它未对我触发的情况是由于Chrome特定版本中的一个错误)。但是,它不能保证xy将具有一致的值。总的来说,这是一个聪明的回复,我给了它一个+1,但我不认为我可以接受它作为答案。 - Trevor Burnham

0

根据我在MDN上阅读的内容,你所做的是正确的。

MDN列出了一些推荐的拖放类型,例如text/html,但如果没有合适的类型,那么只需使用"text/html"类型将id存储为文本,或者创建自己的类型,例如"application/node-id"。


正如我在问题中所说:您可以尝试将id存储为text/html数据,但由于安全模型的限制,直到拖放完成后才能访问该数据。请参见规范,特别是“受保护模式”部分。 - Trevor Burnham
抱歉,我错过了!这是为了给出一个视觉提示,以便确定是否可以放置吗?你能在dragenter事件中至少尝试/捕获一下吗?当涉及到放置时,你就有了所有可用的信息,然后可以取消放置,如果它不适用的话。 - Jules

0

正如其他答案已经解释的那样,使用 DataTransfer.setData(format, data) 设置的 data 集合对于 dragenterdragover 事件处理程序不可访问,而 format 可以在 DataTransfer.types 数组中访问。我们可以利用这一点将数据放入 format 中。

除了其他答案所述之外,我们不仅可以使用 format 存储单个属性,还可以存储实际的 JSON 对象。据我了解,format 将被转换为小写字母,我不确定特殊字符是否会导致任何问题,因此我将数据编码为十六进制字符串(更复杂的解决方案是使用 base32)。

const PREFIX = 'my-app-';

function setPublicDragData(dataTransfer, data) {
    const json = JSON.stringify(data);
    let encoded = '';
    for (let i = 0; i < json.length; i++) {
        encoded += json.charCodeAt(i).toString(16).padStart(2, '0');
    }
    dataTransfer.setData(`${PREFIX}${encoded}`, '');
}

function getPublicDragData(dataTransfer) {
    for (const type of dataTransfer.types) {
        if (type.startsWith(PREFIX)) {
                const encoded = type.slice(PREFIX.length);
                let json = '';
                for (let i = 0; i < encoded.length; i += 2) {
                    json += String.fromCharCode(parseInt(encoded.slice(i, i + 2), 16));
                }
                return JSON.parse(json);
            }
        }
    }
}

通过使用setPublicDragData(e.dataTransfer, data)dragstart事件中设置数据,您可以实现在dragenter/dragover事件中使用getPublicDragData(e.dataTransfer)检索数据。

除了使用全局变量或DOM元素的解决方案外,此解决方案还可用于从一个窗口拖动到另一个窗口。

请注意,根据您的用例,您可能需要向代码添加异常处理(以处理无效的JSON)。此外,我不知道大小限制是多少。


-1

我认为你可以通过调用e.relatedTarget来获得它。请参见:http://help.dottoro.com/ljogqtqm.php

好的,我尝试了e.target.previousElementSibling,它可以工作,但是有点问题... http://jsfiddle.net/Gz8Qw/4/ 我认为它会卡住,因为事件被触发了两次。一次是针对div,一次是针对文本节点(当它针对文本节点时,它是undefined)。不确定这是否能帮助到你...


不行。自己试试。在Chrome中,e.relatedTarget是null。在Firefox中,它是一些奇怪的XrayWrapper。 - Trevor Burnham
我没有得到这些结果(对我来说它返回了#dragTarget),但我意识到这仍然不是你要找的。 - solidau
更正我的先前评论:XrayWrapper 是 Firefox 开发工具的一个怪癖,每当您 console.log 一个 DOM 元素时出现。无论如何,它包装的是鼠标悬停在其上的东西,而不是可拖动的东西。 - Trevor Burnham
1
e.target.previousElementSibling 之所以有效,是因为 draggable 是拖动目标前面的元素... - Trevor Burnham
啊,我明白了,我之前的想法是在不同的上下文中。现在我明白了,它实际上是指DOM中的前一个元素。我很高兴你指出了这一点(而不是什么都不说),帮助我理解 - 谢谢。 - solidau
如果您能更新回答,以便那些不阅读评论的人也能看到,那就太好了。感谢您的努力。 - Pipo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接