整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CSS 是如何影响浏览器元素在文档中的排列?

前在项目的过程中遇到了一个问题,某个 div 希望始终显示在最上面,而在之后的元素都显示在它之下,当时设置了 z-index 也没有效果,不知道什么原因,因此找了一下 CSS 相关资料,解决了这个问题的同时,也学习了很多知识,特此和大家分享一下。

屏幕是一个二维平面,然而 HTML 元素却是排列在三维坐标系中, x 为水平方向, y 为垂直方向, z为屏幕由内向外方向,我们在看屏幕的时候是沿着 z 轴方向从外向内的。由此,元素在用户视角就形成了层叠的关系,某个元素可能覆盖了其他元素也可能被其他元素覆盖;

这里有几个重要的概念:层叠上下文 (堆叠上下文, Stacking Context)、层叠等级 (层叠水平, Stacking Level)、层叠顺序 (层叠次序, 堆叠顺序, Stacking Order)、z-indexBFC(块级格式化上下文,Block Formatting Context),这些概念共同决定了你看到元素的位置,下面我们就围绕着这几个概念来一起学习一下。

声明:

  1. 以下定位元素指的是 position:absolute|fixed|relative|sticky
  2. 以下非定位元素指的是 position:initial|static
  3. 本文蛮长的,但是如果你可以仔细看完,那对相关概念的掌握就非常好了 (~o ̄▽ ̄)~

1. 层叠上下文 (Stacking Context)

层叠上下文 (堆叠上下文, Stacking Context),是 HTML 中一个三维的概念。在 CSS2.1 规范中,每个元素的位置是三维的,当元素发生层叠,这时它可能覆盖了其他元素或者被其他元素覆盖;排在 z 轴越靠上的位置,距离屏幕观察者越近。

文章 <关于z-index 那些你不知道的事> 有一个很好的比喻,这里引用一下;

可以想象一张桌子,上面有一堆物品,这张桌子就代表着一个层叠上下文。如果在第一张桌子旁还有第二张桌子,那第二张桌子就代表着另一个层叠上下文。现在想象在第一张桌子上有四个小方块,他们都直接放在桌子上。在这四个小方块之上有一片玻璃,而在玻璃片上有一盘水果。这些方块、玻璃片、水果盘,各自都代表着层叠上下文中一个不同的层叠层,而这个层叠上下文就是桌子。

每一个网页都像一个房间,这个房间就是 <html></html>,其他层叠上下文就像这个房间里的桌子,HTML 标签中的一切都被置于这个房间中。

当给一个元素的 position 值赋为 fixed 或 sticky 值时,你就创建了一个新的层叠上下文,其中有着独立于页面上其他层叠上下文和层叠层的层叠层,这就相当于你把另一张桌子带到了房间里。

层叠上下文 1 (Stacking Context 1)是由文档根元素形成的, 层叠上下文 2 和 3 (Stacking Context 2, 3) 都是层叠上下文 1 (Stacking Context 1) 上的层叠层。他们各自也都形成了新的层叠上下文,其中包含着新的层叠上下文。

在层叠上下文中,其子元素按照上面解释的规则进行层叠。形成层叠上下文的方法有:

  • 根元素 <html></html>;
  • position 值为 absolute|relative,且 z-index 值不为 auto;
  • position 值为 fixed|sticky;
  • z-index 值不为 auto 的 flex 元素,即父元素属性 display:flex|inline-flex;
  • opacity 属性值小于 1 的元素;
  • transform 属性值不为 none 的元素;
  • mix-blend-mode 属性值不为 normal 的元素;
  • filter、 perspective、 clip-path、 mask、 mask-image、 mask-border、 motion-path值不为 none 的元素;
  • perspective 值不为 none 的元素;
  • isolation 属性被设置为 isolate 的元素;
  • will-change 中指定了任意 CSS 属性,即便你没有直接指定这些属性的值
  • -webkit-overflow-scrolling 属性设置为 touch 的元素;

总结:

  1. 层叠上下文可以包含在其他层叠上下文中,并且一起组建了一个有层级的层叠上下文;
  2. 每个层叠上下文完全独立于它的兄弟元素,当处理层叠时只考虑子元素,类似于 BFC;
  3. 每个层叠上下文是自包含的:当元素的内容发生层叠后,整个该元素将会在父级叠上下文中按顺序进行层叠;

2. 层叠等级 (Stacking Level)

层叠等级 (层叠水平, Stacking Level) 决定了在同一个层叠上下文中,元素在 z 轴上的显示的顺序;

  1. 普通元素的层叠等级优先由其所在的层叠上下文决定;
  2. 层叠等级的比较,只有在同一个层叠上下文元素中才有意义;
  3. 在同一个层叠上下文中,层叠等级描述定义的是该层叠上下文中的元素在 z 轴上的上下顺序;

对于普通元素的层叠水平探讨只局限于在当前层叠上下文中:

层叠上下文本身是一个强力的「层叠结界」,普通的元素水平是无法突破这个结界和结界外的元素去较量层叠水平的。

— CSS 世界

另外,层叠等级并不一定由 z-index 决定,只有定位元素的层叠等级才由 z-index 决定,其他类型元素的层叠等级由层叠顺序、他们在 HTML 中出现的顺序、他们的祖先元素的层叠等级一同决定,详细的规则见下面层叠顺序的介绍。

3. z-index

在 CSS 2.1 中, 所有的盒模型元素都处于三维坐标系中。除了我们常用的横坐标和纵坐标, 盒模型元素还可以沿着「z 轴」层叠摆放,当他们相互覆盖时,z 轴顺序就变得十分重要。

-- CSS 2.1 Section 9.9.1 - Layered presentation

z-index 只适用于定位的元素,对非定位元素无效,它可以被设置为正整数、负整数、 0、 auto,如果一个定位元素没有设置 z-index,那么默认为 auto;

元素的 z-index 值只在同一个层叠上下文中有意义。如果父级层叠上下文的层叠等级低于另一个层叠上下文的,那么它 z-index 设的再高也没用。所以如果你遇到 z-index 值设了很大,但是不起作用的话,就去看看它的父级层叠上下文是否被其他层叠上下文盖住了。

4. 层叠顺序 (Stacking Order)

层叠顺序 (层叠次序, 堆叠顺序, Stacking Order) 描述的是元素在同一个层叠上下文中的顺序规则(之前的层叠上下文和层叠等级是概念),从层叠的底部开始,共有七种层叠顺序:

  1. 背景和边框:形成层叠上下文的元素的背景和边框。
  2. 负 z-index 值:层叠上下文内有着负 z-index 值的定位子元素,负的越大层叠等级越低;
  3. 块级盒:文档流中的块级、非定位子元素;
  4. 浮动盒:非定位浮动元素;
  5. 行内盒:文档流中行内、非定位子元素;
  6. z-index: 0: z-index 为 0 或 auto 的定位元素, 这些元素形成了新的层叠上下文;
  7. 正 z-index 值: z-index 为正的定位元素,正的越大层叠等级越高;

第 7 级顺序的元素会显示在之前顺序元素的上方,也就是看起来覆盖了更低级的元素:

除层叠顺序优先级规则之外,还有一条后来居上规则:同一个层叠顺序的元素按照在 HTML 里出现的顺序依次层叠。这两个规则共同决定浏览器元素在文档中是如何层叠的。

5. 文档流 (Document Flow)

5.1 常规流 (Normal flow)

  • 在常规流中,盒一个接着一个排列;
  • 在块级格式化上下文里面, 它们竖着排列;
  • 在行内格式化上下文里面, 它们横着排列;
  • 当 position 为 static 或 relative,并且 float 为 none 时会触发常规流;
  • 对于静态定位(static positioning), position:static,盒的位置是常规流布局里的位置;
  • 对于相对定位(relative positioning), position:relative,盒偏移位置由 top、 bottom、 left、 right 属性定义。即使有偏移,仍然保留原有的位置,其它常规流不能占用这个位置。

5.2 浮动 (Floats)

  1. 左浮动元素尽量靠左、靠上,右浮动同理;
  2. 这导致常规流环绕在它的周边,除非设置 clear 属性;
  3. 浮动元素不会影响块级元素的布局;
  4. 但浮动元素会影响行内元素的布局,让其围绕在自己周围,撑大父级元素,从而间接影响块级元素布局;
  5. 最高点不会超过当前行的最高点、它前面的浮动元素的最高点;
  6. 不超过它的包含块,除非元素本身已经比包含块更宽;
  7. 行内元素出现在左浮动元素的右边和右浮动元素的左边,左浮动元素的左边和右浮动元素的右边是不会摆放浮动元素的;

5.3 绝对定位 (Absolute positioning)

  1. 绝对定位方案,盒从常规流中被移除,不影响常规流的布局;
  2. 它的定位相对于它的包含块,相关 CSS 属性: top、 bottom、 left、 right;
  3. 如果元素的属性 position 为 absolute 或 fixed,它是绝对定位元素;
  4. 对于 position:absolute,元素定位将相对于上级元素中最近的一个 relative、 fixed、 absolute,如果没有则相对于 body;

6. BFC (Block Formatting Context)

6.1 什么是 BFC

BFC (Block Formatting Context) 块级格式化上下文,是用于布局块级盒子的一块渲染区域,相对应的还有 IFC(Inline Formatting Context)内联格式化上下文,不是本文重点,读者可以自行查阅相关知识。

BFC 是 Web 页面 CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域。

— MDN - 块格式化上下文

一个 BFC 的范围包含创建该上下文元素的所有子元素,但不包括创建了新 BFC 的子元素的内部元素。这从另一方角度说明,一个元素不能同时存在于两个 BFC 中。因为如果一个元素能够同时处于两个 BFC 中,那么就意味着这个元素能与两个 BFC 中的元素发生作用,就违反了 BFC 的隔离作用。

触发 BFC 的方式有:

  1. 根元素,即 HTML 标签;
  2. 浮动元素,即 float 值为 left、 right;
  3. overflow 值不为 visible,即值为 auto、 scroll、 hidden;
  4. display 值为 inline-block、 table-cell、 table-caption、 table、 inline-table、 flex、 inline-flex、 grid、 inline-grid;
  5. 定位元素: position 值为 absolute、 fixed;
  6. contain 为 layout、 content、 paint 的元素;

注意: display:table 也可以生成 BFC 的原因在于 Table 会默认生成一个匿名的 table-cell,是这个匿名的 table-cell 生成了 BFC。

6.2 用法

1. 阻止相邻元素的 margin 合并

属于同一个 BFC 的两个相邻块级子元素的上下 margin 会发生重叠,(设置 writing-mode:tb-rl时,水平 margin 会发生重叠)。所以当两个相邻块级子元素分属于不同的 BFC 时可以阻止 margin 重叠。可以给任一个相邻块级盒子的外面包一个 div,通过改变此 div 的属性使两个原盒子分属于两个不同的 BFC,以此来阻止 margin 重叠。

代码和预览参见:Codepen - 使用BFC阻止margin合并:https://codepen.io/SHERlocked93/pen/eVOevN

2. 阻止元素被浮动元素覆盖

一个正常文档流的块级元素可能被一个 float 元素覆盖,挤占正常文档流,因此可以设置一个元素的 float、 display、 position 值等方式触发 BFC,以阻止被浮动盒子覆盖。

代码和预览参见:Codepen - 使用BFC阻止元素被浮动元素覆盖:https://codepen.io/SHERlocked93/pen/pazdzB

3. 包含浮动元素

通过改变包含浮动子元素的父盒子的属性值,触发 BFC,以此来包含子元素的浮动盒子。

代码和预览参见:Codepen - 使用BFC包含浮动元素:https://codepen.io/SHERlocked93/pen/OQLOqG

7. 实战

下面一起来看几个例子实战一下,帮助理解。

7.1 普通情况

三个 relative 定位的 div 块中各有 absolute 的不同颜色的 span.red、 span.green、 span.blue,它们都设置了 position:absolute;

代码和预览参见:Codepen - 普通情况:https://codepen.io/SHERlocked93/pen/aaPord

那么当没有元素包含 z-index 属性时,这个例子中的元素按照如下顺序层叠(从底到顶顺序):

  1. 根元素的背景和边界;
  2. 块级非定位元素按 HTML 中的出现顺序层叠;
  3. 行内非定位元素按 HTML 中的出现顺序层叠;
  4. 定位元素按 HTML中的出现顺序层叠;

红绿蓝都属于 z-index 为 auto 的定位元素,因此按照 7 层层叠顺序规则来说同属于层叠顺序第 6 级,所以按 HTML 中的出现顺序层叠:红->绿->蓝

7.2 在相同层叠上下文的父元素内的情况

红绿位于一个 div.first-box 下,蓝位于 div.second-box 下,红绿蓝都设置了 position:absolute, first-box 与 second-box 都设置了 position:relative;

代码和预览参见:Codepen - 父元素不同但都位于根元素下:https://codepen.io/SHERlocked93/pen/RYENBw

这个例子中,红蓝绿元素的父元素 first-box 与 second-box 都没有生成新的层叠上下文,都属于根层叠上下文中的元素,且都是层叠顺序第 6 级,所以按 HTML 中的出现顺序层叠:红->绿->蓝

7.3 给子元素增加 z-index

红绿位于一个 div.first-box 下,蓝黄位于 div.second-box 下,红绿蓝都设置了 position:absolute,如果这时给绿加一个属性 z-index:1,那么此时 .green 位于最上面;

如果再在 .second-box 下 .green 后加一个绝对定位的 span.gold,设置 z-index:-1,那么它将位于红绿蓝的下面;

代码和预览参见:Codepen - 设置了z-index:https://codepen.io/SHERlocked93/pen/gdZOrK

这个例子中,红蓝绿黄元素的父元素中都没有生成新的层叠上下文,都属于根层叠上下文中的元素

  1. 红蓝都没有设置 z-index,同属于层叠顺序中的第 6 级,按 HTML 中的出现顺序层叠;
  2. 绿设置了正的 z-index,属于第 7 级;
  3. 黄设置了负的 z-index,属于第 2 级;

所以这个例子中的从底到高显示的顺序就是:黄->红->蓝->绿

7.4 在不同层叠上下文的父元素内的情况

红绿位于一个 div.first-box 下,蓝位于 div.second-box 下,红绿蓝都设置了 position:absolute,如果 first-box 的 z-index 设置的比 second-box 的大,那么此时无论蓝的 z-index 设置的多大 z-index:999,蓝都位于红绿的下面;如果我们只更改红绿的 z-index 值,由于这两个元素都在父元素 first-box 产生的层叠上下文中,此时谁的 z-index 值大,谁在上面;

代码和预览参见:Codepen - 不同层叠上下文的父元素:https://codepen.io/SHERlocked93/pen/gdZbOJ

这个例子中,红绿蓝都属于设置了 z-index 的定位元素,不过他们的父元素创建了新的层叠上下文;

  1. 红绿的父元素 first-box 是设置了正 z-index 的定位元素,因此创建了一个层叠上下文,属于层叠顺序中的第 7 级;
  2. 蓝的父元素 second-box 也同样创建了一个层叠上下文,属于层叠顺序中的第6级;
  3. 按照层叠顺序, first-box 中所有元素都排在 second-box 上;
  4. 红绿都属于层叠上下文 first-box 中且设置了不同的正 z-index,都属于层叠顺序中第 7 级;
  5. 蓝属于层叠上下文 second-box,且设置了一个很大的正 z-index,属于层叠元素中第 7 级;
  6. 虽然蓝的 z-index 很大,但是因为 second-box 的层叠等级比 first-box 小,因此位于红绿之下;

所以这个例子中从低到到显示的顺序:蓝->红->绿

(我遇到的的情况就属于这个例子类似情形)

7.5 给子元素设置 opacity

红绿位于 div.first-box 下,蓝位于 div.second-box 下,红绿蓝都设置了 position:absolute,绿设置了 z-index:1,那么此时绿位于红蓝的最上面;

如果此时给 first-box 设置 opacity:.99,这时无论红绿的 z-index 设置的多大 z-index:999,蓝都位于红绿的上面;

如果再在 .second-box 下 .green 后加一个 span.gold,设置 z-index:-1,那么它将位于红绿蓝的下面;

代码和预览参见:Codepen - opacity的影响:https://codepen.io/SHERlocked93/pen/GXPRWB

之前已经介绍了,设置 opacity 也可以形成层叠上下文,因此:

  1. first-box 设置了 opacity, first-box 成为了一个新的层叠上下文;
  2. second-box 没有形成新的层叠上下文,因此其中的元素都属于根层叠上下文;
  3. 黄属于层叠顺序中第 2 级,红绿属于第 7 级, first-box 属于第 6 级,蓝属于层叠顺序中第6级且按HTML出现顺序位于 first-box 之上;

所以这个例子中从低到到显示的顺序:黄->红->绿->蓝


关注微信公众号:安徽思恒信息科技有限公司,了解更多技术内容……

篇文章主要讲述了CSS样式更改中的背景Background,这篇文章我们来谈谈字体设置Font&边框Border的基础用法。

1.字体设置Font

1).字体系列

<div style='font-family: sans-serif normal'></div>
可用字体:
Serif
Sans-serif
Monospace
Cursive
Fantasy
Times
Courier

2).字体风格

<div style='font-style:normal'></div>
文本倾斜:
normal   文本正常显示
italic   文本斜体显示
oblique  文本倾斜显示

3).字体变形

<div style='font-variant:small-caps'></div>
normal          显示标准字体。
small-caps      显示小型大写字母的字体。

4).字体加粗

<div style='font-weight:normal'></div>
normal    标准的字符
bold      粗体字符
bolder    更粗的字符
lighter   更细的字符
也可以使用数字表示,范围为100~900

5).字体大小

<div style='font-size:60px'></div>
smaller 变小
larger  变大
length  固定值
而且还支持百分比

2.边框Border

首先说一下边框风格,它的风格比较多,常用的一般是实线为主:

<div style='border-style:none'></div>
hidden     隐藏边框
dotted     点状边框
dashed     虚线边框
solid      实线边框
double     双线边框
groove     3D凹槽边框
ridge      3D垄状边框
inset      3D inset边框
outset     3D outset边框
边框也有四面,所以也会有上下左右
所以有时候为了更精确定位并修改样式可以使用:
border-top-style     上边框样式
border-right-style   右边框样式
border-bottom-style  下边框样式
border-left-style    左边框样式

先定义边框的宽度 风格和颜色,然后定义边框的其它属性。

1).边框形状

<div style='border-radius:25px;'></div>

2).边框阴影

<div style='box-shadow:1px 2px 2px 2px red'></div>
参数含义:
边框各个方向的大小和颜色

3).边框图片

<div style='border-image:url(1.png) 30 30 10 round'></div>
参数含义:
边框图片的路径
图片边框向内偏移
图片边框的宽度
边框图像区域超出边框的量
图像边框是否应平铺(repeated)、铺满(rounded)或拉伸(stretched)。

总结

这篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,希望让大家对CSS选择器有个简单的认识和了解。
****看完本文有收获?请转发分享给更多的人****

IT共享之家

想要学习更多,请前往Python爬虫与数据挖掘专用网站:http://pdcfighting.com/

S 鼠标框选(页面选择)时返回对应的 HTML 或文案内容

一、需求背景

1、项目需求

当用户进行鼠标框选选择了页面上的内容时,把选择的内容进行上报。

2、需求解析

虽然这需求就一句话的事,但是很显然,没那么简单...

因为鼠标框选说起来简单,就是选择的内容,但是这包含很多中情况,比如:只选择文案、选择图片、选择输入框、输入框中的内容选择、iframe、等。

简单总结,分为以下几点:

  1. 选择文案时
  2. 选择图片、svg、iframe、video、audio 等标签时
  3. 选择 input、select、textarea 等标签时
  4. 选择 input、textarea 标签内容时
  5. 选择类似 字符时
  6. 键盘全选时
  7. 鼠标右键选择
  8. 以上各模块结合时
  9. 当包含标签的时候,返回 html 结构,只有文本时返回文本内容

二、技术要点

鼠标框选包含以下几点:

  1. debounce 防抖
  2. addEventListener 事件监听
  3. Range 对象
  4. Selection 对象

1、debounce

老生常谈的技术点了,这里不能用节流,因为肯定不能你鼠标选择的时候,隔一段时间返回一段内容,肯定是选择之后一起返回。

这里用 debounce 主要也是用在事件监听和事件处理上。

  • 【debounce 掘金】
  • 【debounce CSDN】

2、addEventListener

事件监听,因为鼠标选择,不仅仅是鼠标按下到鼠标抬起,还包括双击、右键、全选。

需要使用事件监听对事件作处理。

  • 【addEventListener MDN】

3、Range

Range 接口表示一个包含节点与文本节点的一部分的文档片段。

Range 是浏览器原生的对象。

3.1. 创建 Range 实例,并设置起始位置

<body>
  <ul>
    <li>Vite</li>
    <li>Vue</li>
    <li>React</li>
    <li>VitePress</li>
    <li>NaiveUI</li>
  </ul>
</body>
<script>
  // 创建 Range 对象
  const range = new Range()
  const liDoms = document.querySelectorAll("li");
  // Range 起始位置在 li 2
  range.setStartBefore(liDoms[1]);
  // Range 结束位置在 li 3
  range.setEndAfter(liDoms[2]);
  // 获取 selection 对象
  const selection = window.getSelection();
  // 添加光标选择的范围
  selection.addRange(range);
</script>


可以看到,选择内容为第二行和第三行

3.1.1 浏览器兼容情况


3.2. Range 属性

  1. startContainer:起始节点。
  2. startOffset:起始节点偏移量。
  3. endContainer:结束节点。
  4. endOffset:结束节点偏移量。
  5. collapsed:范围的开始和结束是否为同一点。
  6. commonAncestorContainer:返回完整包含 startContainer 和 endContainer 的最深一级的节点。

3.2.1. 用我们上面创建的实例来看下 range 属性的值


3.2.2. 如果我们只选择文本内容时

只选择 li 中的 itePres


可以看出 range 属性对应的值


3.3. Range 方法

  1. cloneContents():复制范围内容,并将复制的内容作为 DocumentFragment 返回。
  2. cloneRange():创建一个具有相同起点/终点的新范围, 非引用,可以随意改变,不会影响另一方。
  3. collapse(toStart):如果 toStart=true 则设置 end=start,否则设置 start=end,从而折叠范围。
  4. compareBoundaryPoints(how, sourceRange):两个范围边界点进行比较,返回一个数字 -1、0、1。
  5. comparePoint(referenceNode, offset):返回-1、0、1具体取决于 是 referenceNode 在 之前、相同还是之后。
  6. createContextualFragment(tagString):返回一个 DocumentFragment。
  7. deleteContents():删除框选的内容。
  8. extractContents():从文档中删除范围内容,并将删除的内容作为 DocumentFragment 返回。
  9. getBoundingClientRect():和 dom 一样,返回 DOMRect 对象。
  10. getClientRects():返回可迭代的对象序列 DOMRect。
  11. insertNode(node):在范围的起始处将 node 插入文档。
  12. intersectsNode(referenceNode):判断与给定的 node 是否相交。
  13. selectNode(node):设置范围以选择整个 node。
  14. selectNodeContents(node):设置范围以选择整个 node 的内容。
  15. setStart(startNode, startOffset):设置起点。
  16. setEnd(endNode, endOffset):设置终点。
  17. setStartBefore(node):将起点设置在 node 前面。
  18. setStartAfter(node):将起点设置在 node 后面。
  19. setEndBefore(node):将终点设置为 node 前面。
  20. setEndAfter(node):将终点设置为 node 后面。
  21. surroundContents(node):使用 node 将所选范围内容包裹起来。

3.4. 创建 Range 的方法

3.4.1. Document.createRange

const range = document.createRange();

3.4.2. Selection 的 getRangeAt() 方法

const range = window.getSelection().getRangeAt(0)

3.4.3. caretRangeFromPoint() 方法

if (document.caretRangeFromPoint) {
    range = document.caretRangeFromPoint(e.clientX, e.clientY);
}

3.4.4. Range() 构造函数

const range = new Range()

3.5. Range 兼容性


4、Selection

Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。

4.1. 获取文本对象

window.getSelection()



4.2. Selection 术语

4.2.1. 锚点 (anchor)

锚指的是一个选区的起始点(不同于 HTML 中的锚点链接)。当我们使用鼠标框选一个区域的时候,锚点就是我们鼠标按下瞬间的那个点。在用户拖动鼠标时,锚点是不会变的。

4.2.2. 焦点 (focus)

选区的焦点是该选区的终点,当你用鼠标框选一个选区的时候,焦点是你的鼠标松开瞬间所记录的那个点。随着用户拖动鼠标,焦点的位置会随着改变。

4.2.3. 范围 (range)

范围指的是文档中连续的一部分。一个范围包括整个节点,也可以包含节点的一部分,例如文本节点的一部分。用户通常下只能选择一个范围,但是有的时候用户也有可能选择多个范围。

4.2.4. 可编辑元素 (editing host)

一个用户可编辑的元素(例如一个使用 contenteditable 的 HTML 元素,或是在启用了 designMode 的 Document 的子元素)。

4.3. Selection 的属性

首先要清楚,选择的起点称为锚点(anchor),终点称为焦点(focus)。

  1. anchorNode:选择的起始节点。
  2. anchorOffset:选择开始的 anchorNode 中的偏移量。
  3. focusNode:选择的结束节点。
  4. focusOffset:选择开始处 focusNode 的偏移量。
  5. isCollapsed:如果未选择任何内容(空范围)或不存在,则为 true。
  6. rangeCount:选择中的范围数,之前说过,除 Firefox 外,其他浏览器最多为1。
  7. type:类型:None、Caret、Range

4.4. Selection 方法

  1. addRange(range): 将一个 Range 对象添加到当前选区。
  2. collapse(node, offset): 将选区折叠到指定的节点和偏移位置。
  3. collapseToEnd(): 将选区折叠到当前选区的末尾。
  4. collapseToStart(): 将选区折叠到当前选区的起始位置。
  5. containsNode(node, partlyContained): 判断选区是否包含指定的节点,可以选择是否部分包含。
  6. deleteFromDocument(): 从文档中删除选区内容。
  7. empty(): 从选区中移除所有范围(同 `removeAllRanges()``,已废弃)。
  8. extend(node, offset): 将选区的焦点节点扩展到指定的节点和偏移位置。
  9. getRangeAt(index): 返回选区中指定索引处的 Range 对象。
  10. removeAllRanges(): 移除所有选区中的范围。
  11. removeRange(range): 从选区中移除指定的 Range 对象。
  12. selectAllChildren(node): 选中指定节点的所有子节点。
  13. setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset): 设置选区的起始和结束节点及偏移位置。
  14. setPosition(node, offset):collapse 的别名

4.5. Selection 兼容性


三、项目实现

1、实现思路

  1. 先获取选择的内容,开发 getSelectContent 函数
  2. 对获取的内容进行判断,是否存在 selection 实例,没有直接返回 null
  3. 判断 selection 实例的 isCollapsed 属性 没有选中,对 selection 进行 toString().trim() 操作,判断内容 有内容,直接返回 text 类型 无内容,返回 null 有选中,则判断内容
  4. 判断选中的内容有没有节点 没有节点,则和没有选中一样处理,进行 toString().trim() 操作,判断内容 有内容,直接返回 text 类型 无内容,返回 null 有节点,进行 toString().trim() 操作,判断内容 没有内容,判断是否有特殊节点 有 'iframe', 'svg', 'img', 'audio', 'video' 节点,返回 html 类型 有 'input', 'textarea', 'select',判断 value 值,是否存在 存在:返回 html 类型 不存在:返回 null 没有特殊节点,返回 null 有内容,返回 html 类型
  5. 对鼠标 mousedown、mouseup 事件和 selectionchange、contextmenu、dblclick 事件进行监听,触发 getSelectContent 函数
  6. 在需要的地方进行 debounce 防抖处理

2、简易流程图


2、Debounce 方法实现

2.1. JS

function debounce (fn, time = 500) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout) // 每当触发时,把前一个 定时器 clear 掉
    timeout = setTimeout(() => { // 创建一个新的 定时器,并赋值给 timeout
      fn.apply(this, arguments)
    }, time)
  }
}

2.2. TS

/**
 * debounce 函数类型
 */
type DebouncedFunction<F extends (...args: any[]) => any> = (...args: Parameters<F>) => void
/**
 * debounce 防抖函数
 * @param {Function} func 函数
 * @param {number} wait 等待时间
 * @param {false} immediate 是否立即执行
 * @returns {DebouncedFunction}
 */
function debounce<F extends (...args: any[]) => any>(
  func: F,
  wait = 500,
  immediate = false
): DebouncedFunction<F> {
  let timeout: ReturnType<typeof setTimeout> | null
  return function (this: ThisParameterType<F>, ...args: Parameters<F>) {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const context = this
    const later = function () {
      timeout = null
      if (!immediate) {
        func.apply(context, args)
      }
    }
    const callNow = immediate && !timeout
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(later, wait)
    if (callNow) {
      func.apply(context, args)
    }
  }
}

3、获取选择的文本/html 元素

3.1. 获取文本/html 元素

nterface IGetSelectContentProps {
  type: 'html' | 'text'
  content: string
}
/**
 * 获取选择的内容
 * @returns {null | IGetSelectContentProps} 返回选择的内容
 */
const getSelectContent = (): null | IGetSelectContentProps => {
  const selection = window.getSelection()
  if (selection) {
    // 1. 是焦点在 input 输入框
    // 2. 没有选中
    // 3. 选择的是输入框
    if (selection.isCollapsed) {
      return selection.toString().trim().length
        ? {
            type: 'text',
            content: selection.toString().trim()
          }
        : null
    }
    // 获取选择范围
    const range = selection.getRangeAt(0)
    // 获取选择内容
    const rangeClone = range.cloneContents()
    // 判断选择内容里面有没有节点
    if (rangeClone.childElementCount > 0) {
      // 创建 div 标签
      const container = document.createElement('div')
      // div 标签 append 复制节点
      container.appendChild(rangeClone)
      // 如果复制的内容长度为 0
      if (!selection.toString().trim().length) {
        // 判断是否有选择特殊节点
        const isSpNode = hasSpNode(container)
        return isSpNode
          ? {
              type: 'html',
              content: container.innerHTML
            }
          : null
      }
      return {
        type: 'html',
        content: container.innerHTML
      }
    } else {
      return selection.toString().trim().length
        ? {
            type: 'text',
            content: selection.toString().trim()
          }
        : null
    }
  } else {
    return null
  }
}
/**
 * 判断是否包含特殊元素
 * @param {Element} parent 父元素
 * @returns {boolean} 是否包含特殊元素
 */
const hasSpNode = (parent: Element): boolean => {
  const nodeNameList = ['iframe', 'svg', 'img', 'audio', 'video']
  const inpList = ['input', 'textarea', 'select']
  return Array.from(parent.children).some((node) => {
    if (nodeNameList.includes(node.nodeName.toLocaleLowerCase())) return true
    if (
      inpList.includes(node.nodeName.toLocaleLowerCase()) &&
      (node as HTMLInputElement).value.trim().length
    )
      return true
    if (node.children) {
      return hasSpNode(node)
    }
    return false
  })
}

3.2. 只需要文本

/**
 * 获取框选的文案内容
 * @returns {string} 返回框选的内容
 */
const getSelectTextContent = (): string => {
  const selection = window.getSelection()
  return selection?.toString().trim() || ''
}

4、添加事件监听

// 是否时鼠标点击动作
let selectionchangeMouseTrack: boolean = false
const selectionChangeFun = debounce(() => {
  const selectContent = getSelectContent()
  console.log('selectContent', selectContent)
  // todo... 处理上报
  selectionchangeMouseTrack = false
})
// 添加 mousedown 监听事件
document.addEventListener('mousedown', () => {
  selectionchangeMouseTrack = true
})
// 添加 mouseup 监听事件
document.addEventListener(
  'mouseup',
  debounce(() => {
    selectionChangeFun()
  }, 100)
)
// 添加 selectionchange 监听事件
document.addEventListener(
  'selectionchange',
  debounce(() => {
    if (selectionchangeMouseTrack) return
    selectionChangeFun()
  })
)
// 添加 dblclick 监听事件
document.addEventListener('dblclick', () => {
  selectionChangeFun()
})
// 添加 contextmenu 监听事件
document.addEventListener(
  'contextmenu',
  debounce(() => {
    selectionChangeFun()
  })
)

也可以进行封装

/**
 * addEventlistener function 类型
 */
export interface IEventHandlerProps {
  [eventName: string]: EventListenerOrEventListenerObject
}

let selectionchangeMouseTrack: boolean = false
const eventHandlers: IEventHandlerProps = {
  // 鼠标 down 事件
  mousedown: () => {
    selectionchangeMouseTrack = true
  },
  // 鼠标 up 事件
  mouseup: debounce(() => selectionChangeFun(), 100),
  // 选择事件
  selectionchange:  debounce(() => {
    if (selectionchangeMouseTrack) return
    selectionChangeFun()
  }),
  // 双击事件
  dblclick: () => selectionChangeFun(),
  // 右键事件
  contextmenu: debounce(() => selectionChangeFun())
}
Object.keys(eventHandlers).forEach((event) => {
  document.addEventListener(event, eventHandlers[event])
})

5、返回内容

5.1. 纯文本内容


5.2. html 格式


6. 完整 JS 代码

function debounce (fn, time = 500) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout) // 每当触发时,把前一个 定时器 clear 掉
    timeout = setTimeout(() => { // 创建一个新的 定时器,并赋值给 timeout
      fn.apply(this, arguments)
    }, time)
  }
}

let selectionchangeMouseTrack = false
document.addEventListener('mousedown', (e) => {
  selectionchangeMouseTrack = true
  console.log('mousedown', e)
})
document.addEventListener('mouseup', debounce((e) => {
  console.log('mouseup', e)
  selectionChangeFun()
}, 100))
document.addEventListener('selectionchange', debounce((e) => {
  console.log('selectionchange', e)
  if (selectionchangeMouseTrack) return
  selectionChangeFun()
}))
document.addEventListener('dblclick', (e) => {
  console.log('dblclick', e)
  selectionChangeFun()
})
document.addEventListener('contextmenu',debounce(() => {
  selectionChangeFun()
}))

const selectionChangeFun = debounce(() => {
  const selectContent = getSelectContent()
  selectionchangeMouseTrack = false
  console.log('selectContent', selectContent)
})

const getSelectContent = () => {
  const selection = window.getSelection();
  if (selection) {
    // 1. 是焦点在 input 输入框
    // 2. 没有选中
    // 3. 选择的是输入框
    if (selection.isCollapsed) {
      return selection.toString().trim().length ? {
        type: 'text',
        content: selection.toString().trim()
      } : null
    }
    // 获取选择范围
    const range = selection.getRangeAt(0);
    // 获取选择内容
    const rangeClone = range.cloneContents()
    // 判断选择内容里面有没有节点
    if (rangeClone.childElementCount > 0) {
      const container = document.createElement('div');
      container.appendChild(rangeClone);
      if (!selection.toString().trim().length) {
        const hasSpNode = getSpNode(container)
        return hasSpNode ? {
          type: 'html',
          content: container.innerHTML
        } : null
      }
      return {
        type: 'html',
        content: container.innerHTML
      }
    } else {
      return selection.toString().trim().length ? {
        type: 'text',
        content: selection.toString().trim()
      } : null
    }
  } else {
    return null
  }
}

const getSpNode = (parent) => {
  const nodeNameList = ['iframe', 'svg', 'img', 'audio', 'video']
  const inpList = ['input', 'textarea', 'select']
  return Array.from(parent.children).some((node) => {
    if (nodeNameList.includes(node.nodeName.toLocaleLowerCase())) return true
    if (inpList.includes(node.nodeName.toLocaleLowerCase()) && node.value.trim().length) return true
    if (node.children) {
      return getSpNode(node)
    }
    return false
  })
}

四、总结

  1. 鼠标框选上报能监控用户在页面的行为,能为后续的数据分析等提供便利
  2. 基于 JS 中的 Selection 和 Range 实现的,使用原生 JS
  3. 涉及到的操作比较多,包含键盘、鼠标右键、全选等
  4. 能对框选的内容进行分类,区别 html 和 text,更方便的看出用户选择了哪些内容