基于官方规范系列篇之——HTML 篇(四)用户交互(三)

大家好呀,我是前端创可贴。

上一章我们一起学习了关于用户交互的第二部分,学习了聚焦、快捷键和编辑的相关内容,相信大家对于用户交互有了更加深刻的了解,这一章我们再一起来学习用户交互相关内容的第三部分。

页面查找功能

大家一定都用过浏览器的页面内查找功能(find-in-page),在搜索页面上的内容时该功能是非常有用的,通过 Command + F 或者 Ctrl + F 即可进行查找。页面内查找功能通过页面内查找接口(find-in-page interface) 提供访问,这是一个由浏览器提供的用户界面,允许用户指定输入和搜索参数。

当浏览器成功查找到指定的内容时,会高亮显示并滚动到可视范围内,并且可以前进或后退以查看不同的匹配结果。注意这些都是浏览器的渲染引擎(例如 Chrome 的 Blink 引擎)做的事情,例如内容高亮,并不是通过 CSS 实现的,已经脱离了前端的范畴。

渲染引擎会对页面的 HTML、CSS 进行解析,并构建 DOM 树和 CSSOM 树,两者结合生成渲染树,对于通过样式隐藏的元素是不会出现在渲染树上的,页面内查找功能正是利用了这个已经构建的渲染树来匹配用户输入的字符串。

说到浏览器的页面内查找功能,就不得不提到 window.find() API 了,这个 API 也可以做到查找指定内容并高亮显示,让我们开发者可以控制内容的查找。方法可以传入很多参数,用来指定搜索条件,例如是否大小写敏感、是否整个单词匹配、是否从后往前搜索等。

但是大家注意,这个 API 并不是规范里的标准,建议大家不要在生产环境使用,不同的浏览器在实现上可能会有不同,在未来也可能会随时改变该方法的行为。

大家可以看一下 Issue #3539,该问题追踪了如何标准化 window.find() API 在页面内查找功能中的行为,目前该 API 仍未被标准化,大家可以点进去看看,在十几年前大家就在讨论这个 API 是否需要删除或者标准化,有的人认为需要删掉,有的人认为需要改进它或者创造一个更好的能实现同样功能的 API,一直讨论到今天也没有个最终结果。

details 元素与 hidden=until-found 状态的处理

当 details 元素 open 属性不为 true,或者元素 hidden 属性值为 until-found 时,元素内容都是隐藏不可见的,虽然显示上不可见,但是他们的元素内容是可以通过页面内查找功能查找到并显示出来的。

当页面内查找选择了一个新的匹配项时,会在用户交互任务源(前面文章简单介绍过)的宏任务队列中添加一个任务,所以页面内查找选择新的匹配项是一个异步的宏任务,而不是同步的。对于 details 元素,如果没有 open 属性,任务就会设置 open 属性;对于 hidden=until-found 元素,先触发一个 beforematch 事件,然后移除 hidden 属性。

页面查找结束后,也就是按下 Esc 键关闭页面查找输入框后,之前匹配上的 details 元素和 hidden=until-found 元素,会保持显示状态,不会恢复到隐藏状态。

安全风险

大家有没有思考过,我们在页面上查找内容的时候,搜索内容有没有可能被记录、被监控呢?

浏览器是不会记录我们在页面内的搜索记录的,但是我们浏览的网站会不会呢?如果会的话,是怎么知道我们在搜索框里输入了什么内容呢?

页面可以通过监听浏览器的页面查找功能中的事件,可以推测用户在查找框中输入的内容。浏览器在自动展开 details 元素(或类似结构)时,会触发 scroll 或 toggle 事件,而页面可以利用这些事件来猜测用户输入的搜索词。

基于滚动的攻击

网页创建一个隐藏的或非常小的可滚动区域,包含用户可能搜索的所有字符或组合。

当用户在查找页面对话框中输入内容时,浏览器会滚动到匹配的文本。网页可以监听滚动事件,推测浏览器滚动到哪个位置,从而“猜测”用户输入的内容。

基于 toggle 事件的攻击

通过在每个可能的下一个匹配项外包裹一个未展开的 details 元素,网页可以监听 toggle 事件(当浏览器在查找过程中自动展开 details 元素时触发)。这样,网页可以通过监听这些事件,逐步构建用户的搜索词。

关闭请求与监听器

关闭请求

我们平时在使用计算机的时候,经常会按下 Esc 键,用于关闭菜单、弹框等,很多时候计算机卡住的时候你可能也会非常暴躁的一直按 Esc 键试图关闭当前的程序。

这种行为有个专业名词,叫做关闭请求(Close Requests),对于浏览器来说,关闭请求是用户与浏览器交互的一种方式,通常通过设备特定的操作来触发。浏览器会监听这些关闭请求,并根据请求的内容关闭用户当前正在交互的界面组件,例如弹出的对话框或菜单,用户可以方便地关闭不再需要的界面元素。

常见的关闭请求包括:

  • 桌面端的 Esc 键
  • 移动端的返回按钮或手势
  • 辅助技术的关闭手势,例如 iOS VoiceOver 的双指划“Z”手势
  • 游戏手柄的“返回”按钮,例如手柄上的圆形按钮

浏览器接收到针对文档的潜在的关闭请求时(例如当你暴躁的按下 Esc 键时),会在用户交互任务源的宏任务队列中添加一个任务(同样的也是异步的宏任务,而不是同步的),该任务具体会执行以下关闭请求步骤:

1. 如果文档内有元素处于全屏状态(通过 Element.requestFullscreen() 将元素变为全屏),就会退出全屏模式,然后直接结束任务。大家注意,退出全屏这个过程不会触发任何键盘相关事件,例如 keydown 事件,最终只有 fullscreenchange 事件被触发。

<head>
  <style>
    #container {
      width: 300px;
      height: 300px;
      background-color: antiquewhite;
    }
  </style>
</head>
<body>
  <h1>前端创可贴</h1>
  <div id="container">container</div>
  <button id="btn">全屏</button>

  <script>
    btn.addEventListener("click", () => {
      container.requestFullscreen();
    });

    document.addEventListener('fullscreenchange', () => {
      console.log('fullscreenchange');
    });

    document.addEventListener("keydown", () => {
      console.log("keydown");
    });

    document.addEventListener("keyup", () => {
      console.log("keyup");
    });
  </script>
</body>

点击按钮打开全屏后,再按下 Esc 键关闭全屏,可以看到只有 fullscreenchange 事件被触发,keydown 和 keyup 并没有触发。

2. 可能会直接跳到后面的“替代处理”步骤。例如浏览器检测到用户多次尝试关闭请求但均被拦截,就会直接跳到后面的“替代处理”步骤。

3. 触发相关的事件。

  • 例如一次关闭请求是通过按下 Esc 键触发的,就会触发相应的 keydown 事件。一般来说,按下 Esc 键在大多数平台上会被视为一个“关闭请求”。

  • 例如一些辅助技术(如屏幕阅读器)通过特定的取消手势发出了关闭请求,会模拟 Esc 键的 keydown 事件,即使用户并没有实际按下这个键。

4. 如果有事件触发,当该事件的 canceled 标志被设置为 true,也就是通过执行 event.preventDefault 取消了事件,浏览器不会再继续执行关闭操作,直接结束任务

  • 对于内置的一些组件,例如 popover 属性元素和 dialog 元素,按下 Esc 键触发了关闭请求后浏览器就会自动关闭它们,此时也会触发相关的 keydown 事件。如果在 keydown 事件中执行 event.preventDefault(),这些元素就不会被关闭了。

<h1>前端创可贴</h1>
<dialog id="dialog">弹框内容</dialog>
<button id="btn">打开弹框</button>

<script>
  document.addEventListener("keydown", (event) => {
    console.log("keydown");
    event.preventDefault();
  });

  btn.addEventListener('click', () => {
    dialog.showModal();
  });
</script>

默认情况下我们打开 dialog 元素后按下 Esc 键即可关闭它,但是我们在 keydown 事件中执行了 event.preventDefault(),按下 Esc 键弹框就不会被关闭

5. 如果有事件触发,事件期间文档变为非活跃状态(例如在事件处理过程中文档被卸载或隐藏),关闭操作也将终止,直接结束任务。

6. 浏览器会检查页面上是否有正在监听关闭请求的代码。如果有监听器(一个可以响应关闭请求的处理程序,下面小节具体介绍)并处理了该请求,则停止操作,直接结束任务。

7. 替代处理步骤:如果页面没有监听到关闭请求,浏览器可能会将该请求解释为其他操作,浏览器可以根据具体情况决定如何处理这个交互,而不再将其视为关闭请求。

在一些平台(如 Android)中,按下后退按钮也可以被视为关闭请求。但是后退按钮与 Esc 键不同,它不会触发 keydown 事件,而是直接由浏览器处理。如果有活动的关闭监听器,浏览器会调用该监听器。如果没有,则浏览器可能将后退按钮按下解释为其它操作,比如回退到历史记录中的上一个页面。

正因为像 Android 这样的平台,后退按钮也可以被视为关闭请求,所以关闭请求是不能一直被我们开发者给拦截的,否则用户不管按下后退按钮多少次,永远都无法回退到上一个历史页面。所以为了防止拦截关闭请求被滥用,对于下面要介绍的可以拦截关闭请求的 cancel 事件,只有用户触发了历史动作激活前面章节介绍过),调用 event.preventDefault() 才会生效(即拦截关闭请求)。所以即便我们在 cancel 事件中写死一定要执行 event.preventDefault(),此时用户触发了两次关闭请求,这两次关闭请求期间没有触发任何的历史动作激活,那么只有第一次关闭请求会被拦截,第二次不会被拦截,会正常触发关闭请求

const watcher = new CloseWatcher();

watcher.addEventListener('cancel', e => {
  console.log('cancel 事件触发');
  e.preventDefault();
})

watcher.addEventListener('close', e => {
  console.log('close 事件触发,cancel 事件没有拦截成功');
})

可以看到,我按下了两次 Esc 键,第一次时 close 事件被拦截,而第二次并不会被拦截。

关闭监听器

关闭监听器(Close Watcher)是一个可以响应关闭请求的处理程序。

上面我们说过,内置的一些组件,例如 popover 属性元素和 dialog 元素,触发了关闭请求后浏览器就会自动关闭它们。但是对于我们自己开发的可关闭的自定义 UI 组件来说,浏览器默认是不可能会帮助我们处理的,这时候我们就可以借助关闭监听器的 close 事件来解决这个问题。

CloseWatcher 接口

CloseWatcher 是一个用于管理关闭事件和监听器的实用工具类,可以处理和控制关闭请求,在页面中精确地管理各种关闭行为,例如拦截、取消或处理用户的关闭请求。

注意,该接口的兼容性目前比较差,Chrome 直到 126 版本才支持

CloseWatcher 对象支持两种事件:cancel 事件close 事件正常流程是先触发 cancel 事件,符合条件时再接着触发 close 事件,最终监听器被销毁。有些 API 也可以跳过 cancel 事件直接触发 close 事件

new CloseWatcher()

创建一个新的 CloseWatcher 实例。如果已经有了其他活跃的关闭监听器,并且 Window 没有触发历史动作激活(前面文章介绍过),那么新创建的 CloseWatcher 会在任何关闭请求触发时,和现有的关闭监听器一起被调用和销毁。

const watcher = new CloseWatcher();

new CloseWatcher({ signal })

构造函数可以接受一个 AbortSignal 对象作为参数。如果提供了 signal 选项,监听器可以通过触发这个 AbortSignal 对象的 abort() 来销毁,就像调用了 watcher.destroy() 一样。

let controller = new AbortController();
let watcher = new CloseWatcher({ signal: controller.signal });
// 销毁监听器
controller.abort();

watcher.requestClose()

模拟一个关闭请求,它首先触发 cancel 事件。如果该事件没有被 preventDefault() 取消,则会继续触发 close 事件,并像调用 watcher.destroy() 一样销毁关闭监听器。相反的,如果 cancel 事件调用了 preventDefault 方法,则不会再触发 close 事件,也不会销毁关闭监听器。

watcher.close()

直接触发 close 事件,跳过任何 cancel 事件处理逻辑,并像调用 watcher.destroy() 一样销毁关闭监听器。如果不需要处理 cancel 逻辑,可以直接使用此方法触发关闭行为。

watcher.destroy()

销毁关闭监听器,使其不再接收 close 事件。也可以创建新的独立的 CloseWatcher 实例了。

const watcher = new CloseWatcher();

watcher.addEventListener("cancel", (event) => {
  console.log("cancel 事件触发");
  // 可以通过 preventDefault 阻止触发 close 事件
  // event.preventDefault();
});

watcher.addEventListener("close", () => {
  console.log("close 事件触发");
});

运行上面代码后,在页面按下 Esc 键

可以看到,关闭监听器正在监听页面上任意的关闭请求,并且触发关闭请求后会执行相应的事件回调,执行完后就会销毁监听器,也就是说再次按下 Esc 键将不会再有任何的监听器执行

就算创建了多个关闭监听器,触发了任意一次关闭请求后,所有的关闭监听器会一起执行并销毁。

所以对于我们自定义的可关闭的 UI 组件来说,在打开组件时可以创建关闭监听器进行关闭请求的监听,用户触发关闭请求后,在 cancel 事件中判断是否满足可以关闭的条件,当不满足时(例如内部的表单校验不通过等)调用 preventDefault() 阻止 close 事件的执行。在 close 事件中关闭我们的 UI 组件,即可实现像内置的 dialog 元素那样,通过触发关闭请求,关闭我们的 UI 组件

有的同学可能会说,为啥要用关闭请求监听器呢,我在页面监听 Esc 键的 keydown 事件不就好了?

大家别忘了,不同平台的关闭请求的触发方式可能是不一样的,监听 Esc 键的 keydown 事件,只能用于某些平台。而关闭请求监听器,是直接监听关闭请求,虽然不同平台的关闭请求触发方式不同,但是关闭请求的监听是一样的,所以跨平台性很强。而且事件分为 cancel 和 close 两个事件,逻辑和职责的划分更为清晰。

拖放

大家在一些交互性比较好的网站和一些特殊场景中,经常会用到拖放的功能,例如将一个卡片从一个状态拖拽到另外一个状态,或者通过拖拽来改变元素之间的顺序。拖放功能与用户的交互表现能力还是比较突出的,这一节咱们就一起来学习一下关于拖放的内容。

拖放(Drag and drop)是一种基于事件的机制,用于在页面中拖动和放下元素,一般是从一个 mousedown 事件开始,随后跟随着一系列的 mousemove 事件,最后是鼠标放开触发“放置(drop)”操作。

具体的操作细节在规范中没有明确规定,但是无论用户代理如何实现,拖放操作都必须包含以下几个步骤:

  • 起点:拖放的开始点,通常是鼠标点击的位置,或者用户选择进行拖动的元素或选区。

  • 中间步骤:在拖动过程中,可能会经过多个元素,用户可以将这些元素作为潜在的放置目标,直到最终确定放置目标。

  • 终点或取消:放置操作的终点是用户在松开鼠标按钮时最后经过的元素,表示放置的目标。如果操作被取消,则不会有终点。

对于非指针设备(如键盘或触控设备),用户需要明确表示他们希望进行拖放操作,并选择拖动的对象以及放置的位置。

draggable 属性

通过 draggable 属性,可以使元素变为可拖放。

所有 HTML 元素都可以使用 draggable 属性,属性值可以是:

  • true注意不能写成空字符串):可拖放
  • false:不可拖放

缺省值和无效值都会让 draggable 为默认状态,由浏览器确认其默认行为,例如 div 元素默认不可拖放。

当元素设置了 draggable 属性时,建议大家给该元素再添加一个 title 属性,确保用户在非视觉交互(例如使用屏幕阅读器)时能够明确了解该元素的名称和功能。

draggable 属性可以通过 Javascript 获取和设置:

element.draggable; // 可拖放返回 true,否则返回 false

element.draggable = true;

img 元素、表示图片的 object 元素,以及含有 href 属性的 a 元素,默认都是可拖放的,获取它们的 draggable 属性值会返回 true。

拖放涉及到的事件

一般来说一个成功的拖放操作,需要将拖放的元素的数据也一起传递到放置目标,通过监听 dragstart 事件,可以存储正在拖动元素的数据,以供未来放置操作读取拖动的数据。

一般来说,该事件处理程序需要确保用户不是在拖拽一段文本,而是目标元素,然后将数据存储到 DataTransfer 对象中,并为 effectAllowed 属性设置允许的操作,值可以是:

  • none
  • copy
  • copyLink
  • copyMove
  • link
  • linkMove
  • move
  • all
  • uninitialized

在 dragstart 事件中可以判断是否满足某些自定义条件,不满足的时候可以阻止其拖拽,通过 event.preventDefault 即可阻止拖拽

<h1>前端创可贴</h1>

<ol id="source">
  <li draggable="true" data-value="橘子">橘子</li>
  <li draggable="true" data-value="香蕉">香蕉</li>
  <li draggable="true" data-value="苹果">苹果</li>
</ol>

<script>
  source.addEventListener("dragstart", (e) => {
    if (!(e.target instanceof HTMLLIElement)) {
      // 拖拽的不是 li 元素,可能一段文本选区,阻止其拖拽
      e.preventDefault();
      return;
    }

    // 存储拖拽的数据
    e.dataTransfer.setData("text/plain", e.target.dataset.value);
    // 设置允许的拖放操作
    e.dataTransfer.effectAllowed = 'move';
  });
</script>

一个完整的拖放操作,包含了 拖拽 + 放置。上面我们将一个元素设置为可拖拽,完成了拖拽这一步,那么该如何放置呢?放置目标如何接受放置的操作呢?

放置目标要接受放置的操作,可以监听一些事件:

1. dragenter 事件:在被拖拽元素进入放置目标时触发。可以用于例如在拖放进入放置目标的时候,给放置目标添加一些样式,例如背景色等,增强拖放操作的交互表现,在 dragleave 事件可以再删除背景色。

dragenter 事件不是必需的,在拖放操作中可以不监听该事件,不会影响拖放操作的成功与失败

<head>
  <style>
    .container {
      margin-top: 30px;
      width: 300px;
      height: 300px;
      border: 1px solid red;
    }

    .dragover {
      background-color: antiquewhite;
    }
  </style>
</head>
<body>
  <h1>前端创可贴</h1>

  <ol id="source">
    <li draggable="true" data-value="橘子">橘子</li>
    <li draggable="true" data-value="香蕉">香蕉</li>
    <li draggable="true" data-value="苹果">苹果</li>
  </ol>

  <ol class="container" id="target" />

  <script>
    source.addEventListener("dragstart", (e) => {
      // ... 同上
    });

    target.addEventListener("dragenter", (e) => {
      // 拖拽进入时添加背景色
      e.target.classList.add("dragover");
    });

    target.addEventListener("dragleave", (e) => {
      // 拖拽离开时移除背景色
      e.target.classList.remove("dragover");
    });
  </script>
</body>

2. dragover 事件:在被拖拽元素位于放置目标上方时触发。该事件需要被取消,即调用 event.preventDefault() ,以此来表示允许接受放置操作并且可以设置与事件关联的 DataTransfer 对象的 dropEffect 属性来确定反馈,以此来表示放置目标可以接受的放置操作类型。值可以是:

  • none
  • copy
  • link
  • move

如果不执行 preventDefault,后续的 drop 事件不会触发,此时被拖拽元素的 dropend 事件是正常执行的,只不过 dropend 中的 event.dataTransfer.dropEffect 为 none,而不是在 dropover 事件这里赋的值

target.addEventListener('dragover', e => {
  console.log('dragover');
  e.dataTransfer.dropEffect = 'move';
})

target.addEventListener('drop', e => {
  console.log('drop');
})

source.addEventListener('dragend', () => {
  console.log('dragend', e.dataTransfer);
})

可以看到,drop 事件并没有触发,并且 dragend 事件的 dataTransfer.dropEffect 为 none,而不是在 dropover 事件里赋的值。

如果这里设置的 dropEffect 与 dragstart 事件里设置的 effectAllowed 属性值不相符,则表示该放置目标不接受被拖拽元素的放置操作,同样的 drop 事件也不会触发


source.addEventListener("dragstart", (e) => {
  if (!(e.target instanceof HTMLLIElement)) {
    // 拖拽的不是 li 元素,可能一段文本选区,阻止其拖拽
    e.preventDefault();
    return;
  }

  e.dataTransfer.setData("text/plain", e.target.dataset.value);
  // effectAllowed 设置的是 move
  e.dataTransfer.effectAllowed = 'move';
});

target.addEventListener("dragenter", (e) => {
  e.target.classList.add("dragover");
});

target.addEventListener("dragleave", (e) => {
  e.target.classList.remove("dragover");
});

target.addEventListener('dragover', e => {
  console.log('dragover');
  // dropEffect 设置的是 copy,与上面设置的 effectAllowed 不相符,将不会触发 drop 事件
  e.dataTransfer.dropEffect = 'copy';
  e.preventDefault();
})

target.addEventListener('drop', e => {
  console.log('drop');
})

source.addEventListener('dragend', (e) => {
  console.log('dragend', e.dataTransfer);
})

可以看到,drop 事件同样没有触发。

3. drop 事件:在被拖拽元素位于放置目标上方时松开鼠标,并且放置目标接受该被拖拽元素的放置操作类型时触发。

4. dragleave 事件:在被拖拽元素离开放置目标时调用。有两种时机会被调用:

  • 一种是用户拖拽元素进入放置目标,但是没有放置,继续拖拽着离开了放置目标;
  • 还有一种是虽然在放置目标上方松开了鼠标,但是放置目标不接受放置操作,拖放操作失败,此时也会触发放置目标的 dragleave 事件。

一旦触发了放置目标的 drop 事件,dragleave 事件就不会执行,反之也如此

5. dragend 事件:在拖放操作结束后触发。不管拖放操作成功还是失败,都会触发。

一般来说,拖放操作成功以后,我们都会将被拖拽的元素移动到拖放操作的终点,也就是说原来位置的元素需要删除,那么我们该怎么删除呢?

我们可以在被拖拽元素的 dragend 事件中删除原来的拖动元素,当然也可以做任何其他的拖放操作成功以后的回调处理。

dragend 事件触发,并不代表拖放操作一定成功,例如放置目标的 dragover 事件没有执行 event.preventDefault,放置操作是不成功的,但是 dragend 事件依然会触发,只不过 event.dataTransfer.dropEffect 的值为 none,而不是在 dropover 中赋的值。所以想要执行拖放操作成功以后的回调,需要判断 dropEffect 是否为 dragover 事件中设置的值

结合一下上面提到的所有事件,编写一个移动元素的功能,拖拽元素进入目标元素后,删除原来位置的元素:

<head>
  <style>
    .container {
      margin-top: 30px;
      width: 300px;
      height: 300px;
      border: 1px solid red;
    }

    .dragover {
      background-color: antiquewhite;
    }
  </style>
</head>
<body>
  <h1>前端创可贴</h1>

  <ol id="source">
    <li draggable="true" data-value="橘子">橘子</li>
    <li draggable="true" data-value="香蕉">香蕉</li>
    <li draggable="true" data-value="苹果">苹果</li>
  </ol>

  <ol class="container" id="target"></ol>

  <script>
    source.addEventListener("dragstart", (e) => {
      if (!(e.target instanceof HTMLLIElement)) {
        // 拖拽的不是 li 元素,可能一段文本选区,阻止其拖拽
        e.preventDefault();
        return;
      }

      e.dataTransfer.setData("text/plain", e.target.dataset.value);
      e.dataTransfer.effectAllowed = "move";
    });

    source.addEventListener("dragend", (e) => {
      // 说明放置操作成功,可以删除拖拽元素
      if (e.dataTransfer.dropEffect === "move") {
        e.target.parentNode.removeChild(e.target);
      }
    });

    target.addEventListener("dragenter", (e) => {
      e.target.classList.add("dragover");
    });

    target.addEventListener("dragleave", (e) => {
      e.target.classList.remove("dragover");
    });

    target.addEventListener("dragover", (e) => {
      // 这里设置的值与上面的 effectAllowed 值相同,这一行其实可以删掉,会用 effectAllowed 的值初始化
      e.dataTransfer.dropEffect = "move";
      // 表示接受放置操作
      e.preventDefault();
    });

    target.addEventListener("drop", (e) => {
      e.target.classList.remove("dragover");

      const li = document.createElement("li");
      li.innerText = e.dataTransfer.getData("text/plain");

      e.target.appendChild(li);
    });
  </script>
</body>

effectAllowed 和 dropEffect 属性

上面提到了 dragstart 事件中的 effectAllowed 属性,以及 dragover 事件中的 dropEffect 属性。他们的取值非常接近,effectAllowed 属性其实可以用于初始化 dropEffect 属性的(此时可以不用给 dropEffect 赋值),但是也可以在 dragover 事件中给 dropEffect 属性赋值,如果赋的值与 dragstart 事件中的 effectAllowed 属性不相符,就表明该元素不接受这次放置操作,drop 事件不会触发,效果就好像没有执行 event.preventDefault 一样。

dropEffect 属性用于控制拖放操作的反馈,限定了可以操作的范围,例如设置为 move,则只能用于移动元素,方便我们开发者在 dragend 事件中判断它的值是否是指定值,不是指定的值说明拖放操作失败

effectAllowed 属性是服务于被拖拽的元素,表明该元素拖拽可操作的范围

dropEffect 属性是服务于要放置被拖拽元素的元素,即放置目标元素,表明该元素可接受的拖拽元素的范围,如果值没有设置,默认由 effectAllowed 属性初始化

两者的值必须相符,整体的拖放操作才算成功

下表中列出了 effectAllowed 和 dropEffect 属性值什么时候是相符的,并且当放置目标 dragover 事件没有给 dropEffect 赋值时,会由 effectAllowed 将其初始化,初始化的值即为下表中右列的值:

effectAllowed

dropEffect

none

none

copy

copy

copyLink

copy,某些场景下可能是 link

copyMove

copy,某些场景下可能是move

all

copy,某些场景下可能是link或者move

link

link

linkMove

link,某些场景下可能是move

move

move

uninitialized,并且拖拽的是文本控件的一段选区

move,某些场景下可能是copy或者link

uninitialized,并且拖拽的是一段选区

copy,某些场景下可能是link 或者 move

uninitialized,并且拖拽的是含有 href 属性的 a 元素

link,某些场景下可能是copy或者move

其他

copy,某些场景下可能是link或者move

他们的取值只能在规定的枚举范围之内,如果设置的值不在枚举范围内,设置不会生效。

安全风险

拖放操作是一个表现能力较为突出的交互方式,而且它的交互相对来说更加自由,甚至可以跨文档、跨页面进行拖放。既然它可以这么自由,必然会引入一些安全的风险问题,所以浏览器等用户代理需要解决拖放操作引入的安全问题。

浏览器必须确保在 dragstart 事件期间添加到 DataTransfer 对象中的数据,直到 drop 事件被触发才可以被脚本访问到,在此之前脚本是无法访问到数据的。不然的话,如果用户将敏感信息从一个文档拖动到另一个文档,途中经过一个恶意的第三方文档,数据就有可能会被该恶意文档截取,信息就会遭到泄露,所以只有用户即将要放置的目标元素才能通过 drop 事件获取到数据。

同样的,浏览器必须确保只有用户明确结束拖放操作时,才会认为拖放成功。如果任何脚本结束了拖放操作,则会视为拖放操作不成功,不会触发 drop 事件。

上面的约束条件看起来已经完美解决了安全问题,但是还是可能会发生安全问题。试想一下,用户在按住鼠标按钮的时候,如果有脚本在此时移动了窗口(本来是窗口不动鼠标动,现在变成了鼠标不动窗口动),那么这个行为应不应该算作拖放操作呢?

很明显不能算作拖放操作,否则就可能会有恶意脚本在未经用户的同意下,潜在的进行了拖放操作,导致敏感数据从受保护的源被拖放到恶意文档中。所以浏览器不能将上述行为视为拖放的开始。

结束语

这一章我们一起学习了页面内查找功能,这是一个大家平时经常会用到的功能,我们学习了它的一些特性,了解了它可能带来的安全风险,以及学习了一个跟它的功能很相近的非标准 API window.find

我们还学习了关闭请求与监听器的相关内容,这同样也是大家经常会用到的功能,毕竟谁还没很暴躁的按过 Esc 键呢?

最后我们又学习了拖放功能,这是一个非常强大的功能,在很多场景下它能极大的方便用户的操作。

这些功能都是日常大家经常使用的,它们的重要性可想而知,不知道大家看完有没有一种豁然开朗的感觉呢,就像是终于了解了老朋友的内心~

那么这一章的介绍就结束了,咱们下一章再见啦。

欢迎关注我的公众号,前端创可贴。

猜你喜欢

转载自blog.csdn.net/weixin_44168130/article/details/143272255