本机 HTML5 拖放

HTML5 Rocks

简介

多年来,我们一直使用 JQuery 和 Dojo 等库,以简化动画、圆角和拖放等复杂的用户界面元素。毫无疑问,美观养眼的外观对于创造丰富逼真的网络体验而言非常重要。不过,为什么所有开发人员都执行的常规任务需要使用库?

拖放 (DnD) 是 HTML5 中的头等公民!该规范定义了基于事件的机制 (JavaScript API) 和附加标记,以标记网页上几乎所有 draggable 的元素类型。我想,应该没人会反对本机浏览器对特定功能的支持。本机浏览器 DnD 意味着更快、更灵活的网络应用。

功能检测

许多使用 DnD 的应用在不使用该功能的情况下表现会很糟糕。例如,您可以想象一下无法移动象棋棋子的情况。太糟糕了!尽管浏览器支持相当完备,但确定浏览器是否实施 DnD(或任何相关的 HTML5 功能)对于提供妥善降级的解决方案而言仍然非常重要。如果 DnD 不可用,可启动后备库维持应用运行。

如果您需要借助 API,请始终使用功能检测,而非嗅探浏览器的用户代理。Modernizr 是可用于功能检测的其中一个更出色的库。Modernizr 为其测试的每个功能都设置了布尔值属性。因此只需一行程序即可检测 DnD:

if (Modernizr.draganddrop) {
  // Browser supports HTML5 DnD.
} else {
  // Fallback to a library solution.
}

创建可拖动内容

制作可拖动对象非常简单。在要设为可移动的元素上设置 draggable=true 属性。可以是任何能启用拖动功能的内容,包括图片、链接、文件或其他 DOM 节点。

我们可以开始创建可重新排序的列进行举例说明。基本标记格式如下:

<div id="columns">
  <div class="column" draggable="true"><header>A</header></div>
  <div class="column" draggable="true"><header>B</header></div>
  <div class="column" draggable="true"><header>C</header></div>
</div>

值得注意的是,默认情况下,大部分带有 href 属性的浏览器、文本选择内容、图片元素和定位元素都是可拖动的。例如,拖动 google.com 上的徽标会产生重像:

拖动浏览器中的图片
默认情况下,大部分浏览器均支持图片拖动。

该重像可拖放到地址栏、<input type="file" /> 元素甚至桌面。要将其他类型的内容设为可拖动,您需要使用各种 HTML5 DnD API。

只需稍微使用下神奇的 CSS3,我们就可以将标记整理成列的样子。添加 cursor: move 可让用户清楚地了解该内容可以移动:

<style>
/* Prevent the text contents of draggable elements from being selectable. */
[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Required to make elements draggable in old WebKit */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}
.column {
  height: 150px;
  width: 150px;
  float: left;
  border: 2px solid #666666;
  background-color: #ccc;
  margin-right: 5px;
  -webkit-border-radius: 10px;
  -ms-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 3px #000;
  -ms-box-shadow: inset 0 0 3px #000;
  box-shadow: inset 0 0 3px #000;
  text-align: center;
  cursor: move;
}
.column header {
  color: #fff;
  text-shadow: #000 0 1px;
  box-shadow: 5px;
  padding: 5px;
  background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -webkit-gradient(linear, left top, right top,
                               color-stop(0, rgb(0,0,0)),
                               color-stop(0.50, rgb(79,79,79)),
                               color-stop(1, rgb(21,21,21)));
  background: -webkit-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -ms-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  border-bottom: 1px solid #ddd;
  -webkit-border-top-left-radius: 10px;
  -moz-border-radius-topleft: 10px;
  -ms-border-radius-topleft: 10px;
  border-top-left-radius: 10px;
  -webkit-border-top-right-radius: 10px;
  -ms-border-top-right-radius: 10px;
  -moz-border-radius-topright: 10px;
  border-top-right-radius: 10px;
}
</style>

结果(可拖动,但不会有任何改变):

A
B
C

在以上示例中,大部分浏览器会创建被拖动内容的重像。其他浏览器(尤其是 FF)需要在拖动操作中发送数据。在下一部分中,我们会添加监听器以处理拖/放事件模型,从而使我们的列示例更加有趣。

监听拖动事件

可附加大量不同事件以监听整个拖放过程:

  • dragstart
  • drag
  • dragenter
  • dragleave
  • dragover
  • drop
  • dragend

要处理 DnD 流程,我们需要以下概念:源元素(拖动的起源点)、数据有效负载(我们尝试拖放的内容)和目标(接纳拖放内容的区域)。源元素可以是图片、列表、链接、文件对象、HTML 内容等。目标是接纳用户尝试放置的数据的放置区(或放置区组)。请注意,并非所有元素都可以作为目标(例如图片)。

1. 开始拖动

在您的内容上定义 draggable="true" 属性后,附加 dragstart 事件处理程序以展开每一列的 DnD 序列。

该代码会在用户开始拖动时将列的不透明度设为 40%:

function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
});

结果:

A
B
C

由于 dragstart 事件的目标是源元素,将 this.style.opacity 设置为 40% 可让用户直观地了解到该元素为当前选择拖动的元素。完成拖动后,我们还需要将列的不透明度恢复到 100%。处理该项操作的一个很明显的位置是 dragend 事件。稍后会有更详细的介绍。

2. dragenter、dragover 和 dragleave

dragenterdragoverdragleave 事件处理程序可用于在拖动过程中提供额外的可视化提示。例如,在拖动期间将鼠标悬停在某一列上方时,其边框可能会变成虚线。这样,用户就能知道这些列也是放置的目标区域。

<style>
.column.over {
  border: 2px dashed #000;
}
</style>
function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

function handleDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault(); // Necessary. Allows us to drop.
  }

  e.dataTransfer.dropEffect = 'move';  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnter(e) {
  // this / e.target is the current hover target.
  this.classList.add('over');
}

function handleDragLeave(e) {
  this.classList.remove('over');  // this / e.target is previous target element.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false);
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
});

该代码中有以下几点值得讨论:

  • 每种事件类型的 this/e.target 各不相同,具体取决于我们在 DnD 事件模型中所处的位置。
  • 如果要拖动链接之类的内容,我们需要阻止浏览器的默认行为,不让其导航至该链接。为实现这一目标,可调用 dragover 事件中的 e.preventDefault()。在同一处理程序中调用 return false 也是个不错的做法。浏览器与这些所需操作会有点不协调,但并不会有所损害。
  • 我们使用 dragenter 触发“over”类,而不是使用 dragover。如果我们使用 dragover,系统将反复触发 CSS 类,因为 dragover 事件会在鼠标悬停在列上方时不断启动。这将最终导致浏览器的渲染器进行大量不必要的工作。将重复工作降至最低始终是很好的理念。

3. 完成拖动

要处理实际拖动,可为 dropdragend 事件添加事件监听器。在使用该处理程序时,您需要阻止浏览器的默认行为(通常是某种令人困扰的重定向)以便进行拖放。您可以调用 e.stopPropagation(),阻止该事件触发 DOM。

如果没有 drop 事件,我们的列示例不会采取过多操作,不过除了添加该事件外,使用 dragend 从每一列中删除“over”类也可以迅速作出改善:

...

function handleDrop(e) {
  // this / e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // stops the browser from redirecting.
  }

  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnd(e) {
  // this/e.target is the source node.

  [].forEach.call(cols, function (col) {
    col.classList.remove('over');
  });
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false)
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
  col.addEventListener('drop', handleDrop, false);
  col.addEventListener('dragend', handleDragEnd, false);
});

结果:

A
B
C

如果到目前为止您一直在密切关注,那么您可能会发现,我们的示例仍然没有如预期般放下该列。输入 DataTransfer 对象。

DataTransfer 对象

dataTransfer 属性正是这一神奇 DnD 功能的动力来源,控制着拖动操作中发送的数据。dataTransfer 可在 dragstart 事件中进行设置,并在 drop 事件中读取/处理。调用 e.dataTransfer.setData(format, data) 会将对象内容设置成 MIME 类型,并将数据有效负载作为参数传递。

在我们的示例中,数据有效负载被设为源列的实际 HTML:

var dragSrcEl = null;

function handleDragStart(e) {
  // Target (this) element is the source node.
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

dataTransfer 还有用于以 MIME 类型获取拖动数据的 getData(format),非常方便。以下是用于放下列的修改代码:

function handleDrop(e) {
  // this/e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // Stops some browsers from redirecting.
  }

  // Don't do anything if dropping the same column we're dragging.
  if (dragSrcEl != this) {
    // Set the source column's HTML to the HTML of the columnwe dropped on.
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

我添加了名为 dragSrcEl 的全局变量,为促进列的交换提供便利。在 handleDragStart() 中,源列的 innerHTML 存储在该变量中,然后在 handleDrop() 中读取以交换源列和目标列的 HTML。

结果:

A
B
C

拖动属性

dataTransfer 使用的属性在拖动过程中为用户提供了可视化反馈。这些属性还可用于控制每个放置目标响应特定数据类型的方式。

dataTransfer.effectAllowed
限制用户可在元素上执行的“拖动类型”。在拖放处理模型中用于初始化 dragenterdragover 事件中的 dropEffect。该属性可设置为以下值:nonecopycopyLinkcopyMovelinklinkMovemovealluninitialized
dataTransfer.dropEffect
控制用户在 dragenterdragover 事件期间收到的反馈。当用户将鼠标悬停在目标元素上方时,浏览器的光标会显示即将采取的操作类型(例如复制、移动等)。结果可能呈现为以下某个值:nonecopylinkmove
e.dataTransfer.setDragImage(img element, x, y)
除了使用浏览器默认的“重像”反馈外,您也可以选择设置拖动图标
var dragIcon = document.createElement('img');
dragIcon.src = 'logo.png';
dragIcon.width = 100;
e.dataTransfer.setDragImage(dragIcon, -10, -10);

结果(您在拖动这些列时看到的应该是 Google 徽标):

A
B
C

拖动文件

使用各种 DnD API,您就可以将文件从桌面拖动到浏览器窗口中的网络应用。作为这一想法的延伸,Google Chrome 浏览器还支持将文件对象从浏览器拖出到桌面的功能。

拖入:从桌面拖动到浏览器

将 DnD 事件作为其他内容类型使用即可从桌面拖动文件。主要差别在于您的 drop 处理程序。其数据会包含在 dataTransfer.files 属性中,而非使用 dataTransfer.getData()

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; f = files[i]; i++) {
    // Read the File objects in this FileList.
  }
}

有关将文件从桌面拖动到浏览器的完整指南,请参阅“使用 JavaScript 读取本地文件”中的使用拖放进行选择

拖出:从浏览器拖动到桌面

有关将文件从浏览器拖出到桌面的完整指南,请参阅 CSS 帮助资料中的“拖出文件(如 Gmail)”。

示例

以下为略加修改后的最终版本,且会对每次移动进行计数:

A
B
C
D

在这个列示例中有一点很有意思:这些列既是拖动源,也是放置目标。更常见的情况是源元素和目标元素各不相同。访问 html5demos.com/drag 查看演示。

总结

毫无疑问,HTML5 的 DnD 模型比 JQuery UI 等其他解决方案更加简单。不过,如果您可以利用浏览器的本机 API,请善加利用!毕竟,这才是 HTML5 的要点,即为浏览器提供丰富的本机 API 集并进行标准化。但愿热门的执行 DnD 功能的库最后会默认添加本机 HTML5 支持,并根据需要添加自定义 JS 解决方案的后备内容。

参考

Comments

0