整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          CSS 是如何影響瀏覽器元素在文檔中的排列?

          前在項(xiàng)目的過程中遇到了一個(gè)問題,某個(gè) div 希望始終顯示在最上面,而在之后的元素都顯示在它之下,當(dāng)時(shí)設(shè)置了 z-index 也沒有效果,不知道什么原因,因此找了一下 CSS 相關(guān)資料,解決了這個(gè)問題的同時(shí),也學(xué)習(xí)了很多知識(shí),特此和大家分享一下。

          屏幕是一個(gè)二維平面,然而 HTML 元素卻是排列在三維坐標(biāo)系中, x 為水平方向, y 為垂直方向, z為屏幕由內(nèi)向外方向,我們在看屏幕的時(shí)候是沿著 z 軸方向從外向內(nèi)的。由此,元素在用戶視角就形成了層疊的關(guān)系,某個(gè)元素可能覆蓋了其他元素也可能被其他元素覆蓋;

          這里有幾個(gè)重要的概念:層疊上下文 (堆疊上下文, Stacking Context)、層疊等級(jí) (層疊水平, Stacking Level)、層疊順序 (層疊次序, 堆疊順序, Stacking Order)、z-indexBFC(塊級(jí)格式化上下文,Block Formatting Context),這些概念共同決定了你看到元素的位置,下面我們就圍繞著這幾個(gè)概念來一起學(xué)習(xí)一下。

          聲明:

          1. 以下定位元素指的是 position:absolute|fixed|relative|sticky
          2. 以下非定位元素指的是 position:initial|static
          3. 本文蠻長的,但是如果你可以仔細(xì)看完,那對相關(guān)概念的掌握就非常好了 (~o ̄▽ ̄)~

          1. 層疊上下文 (Stacking Context)

          層疊上下文 (堆疊上下文, Stacking Context),是 HTML 中一個(gè)三維的概念。在 CSS2.1 規(guī)范中,每個(gè)元素的位置是三維的,當(dāng)元素發(fā)生層疊,這時(shí)它可能覆蓋了其他元素或者被其他元素覆蓋;排在 z 軸越靠上的位置,距離屏幕觀察者越近。

          文章 <關(guān)于z-index 那些你不知道的事> 有一個(gè)很好的比喻,這里引用一下;

          可以想象一張桌子,上面有一堆物品,這張桌子就代表著一個(gè)層疊上下文。如果在第一張桌子旁還有第二張桌子,那第二張桌子就代表著另一個(gè)層疊上下文。現(xiàn)在想象在第一張桌子上有四個(gè)小方塊,他們都直接放在桌子上。在這四個(gè)小方塊之上有一片玻璃,而在玻璃片上有一盤水果。這些方塊、玻璃片、水果盤,各自都代表著層疊上下文中一個(gè)不同的層疊層,而這個(gè)層疊上下文就是桌子。

          每一個(gè)網(wǎng)頁都像一個(gè)房間,這個(gè)房間就是 <html></html>,其他層疊上下文就像這個(gè)房間里的桌子,HTML 標(biāo)簽中的一切都被置于這個(gè)房間中。

          當(dāng)給一個(gè)元素的 position 值賦為 fixed 或 sticky 值時(shí),你就創(chuàng)建了一個(gè)新的層疊上下文,其中有著獨(dú)立于頁面上其他層疊上下文和層疊層的層疊層,這就相當(dāng)于你把另一張桌子帶到了房間里。

          層疊上下文 1 (Stacking Context 1)是由文檔根元素形成的, 層疊上下文 2 和 3 (Stacking Context 2, 3) 都是層疊上下文 1 (Stacking Context 1) 上的層疊層。他們各自也都形成了新的層疊上下文,其中包含著新的層疊上下文。

          在層疊上下文中,其子元素按照上面解釋的規(guī)則進(jìn)行層疊。形成層疊上下文的方法有:

          • 根元素 <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 屬性被設(shè)置為 isolate 的元素;
          • will-change 中指定了任意 CSS 屬性,即便你沒有直接指定這些屬性的值
          • -webkit-overflow-scrolling 屬性設(shè)置為 touch 的元素;

          總結(jié):

          1. 層疊上下文可以包含在其他層疊上下文中,并且一起組建了一個(gè)有層級(jí)的層疊上下文;
          2. 每個(gè)層疊上下文完全獨(dú)立于它的兄弟元素,當(dāng)處理層疊時(shí)只考慮子元素,類似于 BFC;
          3. 每個(gè)層疊上下文是自包含的:當(dāng)元素的內(nèi)容發(fā)生層疊后,整個(gè)該元素將會(huì)在父級(jí)疊上下文中按順序進(jìn)行層疊;

          2. 層疊等級(jí) (Stacking Level)

          層疊等級(jí) (層疊水平, Stacking Level) 決定了在同一個(gè)層疊上下文中,元素在 z 軸上的顯示的順序;

          1. 普通元素的層疊等級(jí)優(yōu)先由其所在的層疊上下文決定;
          2. 層疊等級(jí)的比較,只有在同一個(gè)層疊上下文元素中才有意義;
          3. 在同一個(gè)層疊上下文中,層疊等級(jí)描述定義的是該層疊上下文中的元素在 z 軸上的上下順序;

          對于普通元素的層疊水平探討只局限于在當(dāng)前層疊上下文中:

          層疊上下文本身是一個(gè)強(qiáng)力的「層疊結(jié)界」,普通的元素水平是無法突破這個(gè)結(jié)界和結(jié)界外的元素去較量層疊水平的。

          — CSS 世界

          另外,層疊等級(jí)并不一定由 z-index 決定,只有定位元素的層疊等級(jí)才由 z-index 決定,其他類型元素的層疊等級(jí)由層疊順序、他們在 HTML 中出現(xiàn)的順序、他們的祖先元素的層疊等級(jí)一同決定,詳細(xì)的規(guī)則見下面層疊順序的介紹。

          3. z-index

          在 CSS 2.1 中, 所有的盒模型元素都處于三維坐標(biāo)系中。除了我們常用的橫坐標(biāo)和縱坐標(biāo), 盒模型元素還可以沿著「z 軸」層疊擺放,當(dāng)他們相互覆蓋時(shí),z 軸順序就變得十分重要。

          -- CSS 2.1 Section 9.9.1 - Layered presentation

          z-index 只適用于定位的元素,對非定位元素?zé)o效,它可以被設(shè)置為正整數(shù)、負(fù)整數(shù)、 0、 auto,如果一個(gè)定位元素沒有設(shè)置 z-index,那么默認(rèn)為 auto;

          元素的 z-index 值只在同一個(gè)層疊上下文中有意義。如果父級(jí)層疊上下文的層疊等級(jí)低于另一個(gè)層疊上下文的,那么它 z-index 設(shè)的再高也沒用。所以如果你遇到 z-index 值設(shè)了很大,但是不起作用的話,就去看看它的父級(jí)層疊上下文是否被其他層疊上下文蓋住了。

          4. 層疊順序 (Stacking Order)

          層疊順序 (層疊次序, 堆疊順序, Stacking Order) 描述的是元素在同一個(gè)層疊上下文中的順序規(guī)則(之前的層疊上下文和層疊等級(jí)是概念),從層疊的底部開始,共有七種層疊順序:

          1. 背景和邊框:形成層疊上下文的元素的背景和邊框。
          2. 負(fù) z-index 值:層疊上下文內(nèi)有著負(fù) z-index 值的定位子元素,負(fù)的越大層疊等級(jí)越低;
          3. 塊級(jí)盒:文檔流中的塊級(jí)、非定位子元素;
          4. 浮動(dòng)盒:非定位浮動(dòng)元素;
          5. 行內(nèi)盒:文檔流中行內(nèi)、非定位子元素;
          6. z-index: 0: z-index 為 0 或 auto 的定位元素, 這些元素形成了新的層疊上下文;
          7. 正 z-index 值: z-index 為正的定位元素,正的越大層疊等級(jí)越高;

          第 7 級(jí)順序的元素會(huì)顯示在之前順序元素的上方,也就是看起來覆蓋了更低級(jí)的元素:

          除層疊順序優(yōu)先級(jí)規(guī)則之外,還有一條后來居上規(guī)則:同一個(gè)層疊順序的元素按照在 HTML 里出現(xiàn)的順序依次層疊。這兩個(gè)規(guī)則共同決定瀏覽器元素在文檔中是如何層疊的。

          5. 文檔流 (Document Flow)

          5.1 常規(guī)流 (Normal flow)

          • 在常規(guī)流中,盒一個(gè)接著一個(gè)排列;
          • 在塊級(jí)格式化上下文里面, 它們豎著排列;
          • 在行內(nèi)格式化上下文里面, 它們橫著排列;
          • 當(dāng) position 為 static 或 relative,并且 float 為 none 時(shí)會(huì)觸發(fā)常規(guī)流;
          • 對于靜態(tài)定位(static positioning), position:static,盒的位置是常規(guī)流布局里的位置;
          • 對于相對定位(relative positioning), position:relative,盒偏移位置由 top、 bottom、 left、 right 屬性定義。即使有偏移,仍然保留原有的位置,其它常規(guī)流不能占用這個(gè)位置。

          5.2 浮動(dòng) (Floats)

          1. 左浮動(dòng)元素盡量靠左、靠上,右浮動(dòng)同理;
          2. 這導(dǎo)致常規(guī)流環(huán)繞在它的周邊,除非設(shè)置 clear 屬性;
          3. 浮動(dòng)元素不會(huì)影響塊級(jí)元素的布局;
          4. 但浮動(dòng)元素會(huì)影響行內(nèi)元素的布局,讓其圍繞在自己周圍,撐大父級(jí)元素,從而間接影響塊級(jí)元素布局;
          5. 最高點(diǎn)不會(huì)超過當(dāng)前行的最高點(diǎn)、它前面的浮動(dòng)元素的最高點(diǎn);
          6. 不超過它的包含塊,除非元素本身已經(jīng)比包含塊更寬;
          7. 行內(nèi)元素出現(xiàn)在左浮動(dòng)元素的右邊和右浮動(dòng)元素的左邊,左浮動(dòng)元素的左邊和右浮動(dòng)元素的右邊是不會(huì)擺放浮動(dòng)元素的;

          5.3 絕對定位 (Absolute positioning)

          1. 絕對定位方案,盒從常規(guī)流中被移除,不影響常規(guī)流的布局;
          2. 它的定位相對于它的包含塊,相關(guān) CSS 屬性: top、 bottom、 left、 right;
          3. 如果元素的屬性 position 為 absolute 或 fixed,它是絕對定位元素;
          4. 對于 position:absolute,元素定位將相對于上級(jí)元素中最近的一個(gè) relative、 fixed、 absolute,如果沒有則相對于 body;

          6. BFC (Block Formatting Context)

          6.1 什么是 BFC

          BFC (Block Formatting Context) 塊級(jí)格式化上下文,是用于布局塊級(jí)盒子的一塊渲染區(qū)域,相對應(yīng)的還有 IFC(Inline Formatting Context)內(nèi)聯(lián)格式化上下文,不是本文重點(diǎn),讀者可以自行查閱相關(guān)知識(shí)。

          BFC 是 Web 頁面 CSS 視覺渲染的一部分,用于決定塊盒子的布局及浮動(dòng)相互影響范圍的一個(gè)區(qū)域。

          — MDN - 塊格式化上下文

          一個(gè) BFC 的范圍包含創(chuàng)建該上下文元素的所有子元素,但不包括創(chuàng)建了新 BFC 的子元素的內(nèi)部元素。這從另一方角度說明,一個(gè)元素不能同時(shí)存在于兩個(gè) BFC 中。因?yàn)槿绻粋€(gè)元素能夠同時(shí)處于兩個(gè) BFC 中,那么就意味著這個(gè)元素能與兩個(gè) BFC 中的元素發(fā)生作用,就違反了 BFC 的隔離作用。

          觸發(fā) BFC 的方式有:

          1. 根元素,即 HTML 標(biāo)簽;
          2. 浮動(dòng)元素,即 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 會(huì)默認(rèn)生成一個(gè)匿名的 table-cell,是這個(gè)匿名的 table-cell 生成了 BFC。

          6.2 用法

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

          屬于同一個(gè) BFC 的兩個(gè)相鄰塊級(jí)子元素的上下 margin 會(huì)發(fā)生重疊,(設(shè)置 writing-mode:tb-rl時(shí),水平 margin 會(huì)發(fā)生重疊)。所以當(dāng)兩個(gè)相鄰塊級(jí)子元素分屬于不同的 BFC 時(shí)可以阻止 margin 重疊。可以給任一個(gè)相鄰塊級(jí)盒子的外面包一個(gè) div,通過改變此 div 的屬性使兩個(gè)原盒子分屬于兩個(gè)不同的 BFC,以此來阻止 margin 重疊。

          代碼和預(yù)覽參見:Codepen - 使用BFC阻止margin合并:https://codepen.io/SHERlocked93/pen/eVOevN

          2. 阻止元素被浮動(dòng)元素覆蓋

          一個(gè)正常文檔流的塊級(jí)元素可能被一個(gè) float 元素覆蓋,擠占正常文檔流,因此可以設(shè)置一個(gè)元素的 float、 display、 position 值等方式觸發(fā) BFC,以阻止被浮動(dòng)盒子覆蓋。

          代碼和預(yù)覽參見:Codepen - 使用BFC阻止元素被浮動(dòng)元素覆蓋:https://codepen.io/SHERlocked93/pen/pazdzB

          3. 包含浮動(dòng)元素

          通過改變包含浮動(dòng)子元素的父盒子的屬性值,觸發(fā) BFC,以此來包含子元素的浮動(dòng)盒子。

          代碼和預(yù)覽參見:Codepen - 使用BFC包含浮動(dòng)元素:https://codepen.io/SHERlocked93/pen/OQLOqG

          7. 實(shí)戰(zhàn)

          下面一起來看幾個(gè)例子實(shí)戰(zhàn)一下,幫助理解。

          7.1 普通情況

          三個(gè) relative 定位的 div 塊中各有 absolute 的不同顏色的 span.red、 span.green、 span.blue,它們都設(shè)置了 position:absolute;

          代碼和預(yù)覽參見:Codepen - 普通情況:https://codepen.io/SHERlocked93/pen/aaPord

          那么當(dāng)沒有元素包含 z-index 屬性時(shí),這個(gè)例子中的元素按照如下順序?qū)盈B(從底到頂順序):

          1. 根元素的背景和邊界;
          2. 塊級(jí)非定位元素按 HTML 中的出現(xiàn)順序?qū)盈B;
          3. 行內(nèi)非定位元素按 HTML 中的出現(xiàn)順序?qū)盈B;
          4. 定位元素按 HTML中的出現(xiàn)順序?qū)盈B;

          紅綠藍(lán)都屬于 z-index 為 auto 的定位元素,因此按照 7 層層疊順序規(guī)則來說同屬于層疊順序第 6 級(jí),所以按 HTML 中的出現(xiàn)順序?qū)盈B:紅->綠->藍(lán)

          7.2 在相同層疊上下文的父元素內(nèi)的情況

          紅綠位于一個(gè) div.first-box 下,藍(lán)位于 div.second-box 下,紅綠藍(lán)都設(shè)置了 position:absolute, first-box 與 second-box 都設(shè)置了 position:relative;

          代碼和預(yù)覽參見:Codepen - 父元素不同但都位于根元素下:https://codepen.io/SHERlocked93/pen/RYENBw

          這個(gè)例子中,紅藍(lán)綠元素的父元素 first-box 與 second-box 都沒有生成新的層疊上下文,都屬于根層疊上下文中的元素,且都是層疊順序第 6 級(jí),所以按 HTML 中的出現(xiàn)順序?qū)盈B:紅->綠->藍(lán)

          7.3 給子元素增加 z-index

          紅綠位于一個(gè) div.first-box 下,藍(lán)黃位于 div.second-box 下,紅綠藍(lán)都設(shè)置了 position:absolute,如果這時(shí)給綠加一個(gè)屬性 z-index:1,那么此時(shí) .green 位于最上面;

          如果再在 .second-box 下 .green 后加一個(gè)絕對定位的 span.gold,設(shè)置 z-index:-1,那么它將位于紅綠藍(lán)的下面;

          代碼和預(yù)覽參見:Codepen - 設(shè)置了z-index:https://codepen.io/SHERlocked93/pen/gdZOrK

          這個(gè)例子中,紅藍(lán)綠黃元素的父元素中都沒有生成新的層疊上下文,都屬于根層疊上下文中的元素

          1. 紅藍(lán)都沒有設(shè)置 z-index,同屬于層疊順序中的第 6 級(jí),按 HTML 中的出現(xiàn)順序?qū)盈B;
          2. 綠設(shè)置了正的 z-index,屬于第 7 級(jí);
          3. 黃設(shè)置了負(fù)的 z-index,屬于第 2 級(jí);

          所以這個(gè)例子中的從底到高顯示的順序就是:黃->紅->藍(lán)->綠

          7.4 在不同層疊上下文的父元素內(nèi)的情況

          紅綠位于一個(gè) div.first-box 下,藍(lán)位于 div.second-box 下,紅綠藍(lán)都設(shè)置了 position:absolute,如果 first-box 的 z-index 設(shè)置的比 second-box 的大,那么此時(shí)無論藍(lán)的 z-index 設(shè)置的多大 z-index:999,藍(lán)都位于紅綠的下面;如果我們只更改紅綠的 z-index 值,由于這兩個(gè)元素都在父元素 first-box 產(chǎn)生的層疊上下文中,此時(shí)誰的 z-index 值大,誰在上面;

          代碼和預(yù)覽參見:Codepen - 不同層疊上下文的父元素:https://codepen.io/SHERlocked93/pen/gdZbOJ

          這個(gè)例子中,紅綠藍(lán)都屬于設(shè)置了 z-index 的定位元素,不過他們的父元素創(chuàng)建了新的層疊上下文;

          1. 紅綠的父元素 first-box 是設(shè)置了正 z-index 的定位元素,因此創(chuàng)建了一個(gè)層疊上下文,屬于層疊順序中的第 7 級(jí);
          2. 藍(lán)的父元素 second-box 也同樣創(chuàng)建了一個(gè)層疊上下文,屬于層疊順序中的第6級(jí);
          3. 按照層疊順序, first-box 中所有元素都排在 second-box 上;
          4. 紅綠都屬于層疊上下文 first-box 中且設(shè)置了不同的正 z-index,都屬于層疊順序中第 7 級(jí);
          5. 藍(lán)屬于層疊上下文 second-box,且設(shè)置了一個(gè)很大的正 z-index,屬于層疊元素中第 7 級(jí);
          6. 雖然藍(lán)的 z-index 很大,但是因?yàn)?second-box 的層疊等級(jí)比 first-box 小,因此位于紅綠之下;

          所以這個(gè)例子中從低到到顯示的順序:藍(lán)->紅->綠

          (我遇到的的情況就屬于這個(gè)例子類似情形)

          7.5 給子元素設(shè)置 opacity

          紅綠位于 div.first-box 下,藍(lán)位于 div.second-box 下,紅綠藍(lán)都設(shè)置了 position:absolute,綠設(shè)置了 z-index:1,那么此時(shí)綠位于紅藍(lán)的最上面;

          如果此時(shí)給 first-box 設(shè)置 opacity:.99,這時(shí)無論紅綠的 z-index 設(shè)置的多大 z-index:999,藍(lán)都位于紅綠的上面;

          如果再在 .second-box 下 .green 后加一個(gè) span.gold,設(shè)置 z-index:-1,那么它將位于紅綠藍(lán)的下面;

          代碼和預(yù)覽參見:Codepen - opacity的影響:https://codepen.io/SHERlocked93/pen/GXPRWB

          之前已經(jīng)介紹了,設(shè)置 opacity 也可以形成層疊上下文,因此:

          1. first-box 設(shè)置了 opacity, first-box 成為了一個(gè)新的層疊上下文;
          2. second-box 沒有形成新的層疊上下文,因此其中的元素都屬于根層疊上下文;
          3. 黃屬于層疊順序中第 2 級(jí),紅綠屬于第 7 級(jí), first-box 屬于第 6 級(jí),藍(lán)屬于層疊順序中第6級(jí)且按HTML出現(xiàn)順序位于 first-box 之上;

          所以這個(gè)例子中從低到到顯示的順序:黃->紅->綠->藍(lán)


          關(guān)注微信公眾號(hào):安徽思恒信息科技有限公司,了解更多技術(shù)內(nèi)容……

          篇文章主要講述了CSS樣式更改中的背景Background,這篇文章我們來談?wù)勛煮w設(shè)置Font&邊框Border的基礎(chǔ)用法。

          1.字體設(shè)置Font

          1).字體系列

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

          2).字體風(fēng)格

          <div style='font-style:normal'></div>
          文本傾斜:
          normal   文本正常顯示
          italic   文本斜體顯示
          oblique  文本傾斜顯示

          3).字體變形

          <div style='font-variant:small-caps'></div>
          normal          顯示標(biāo)準(zhǔn)字體。
          small-caps      顯示小型大寫字母的字體。

          4).字體加粗

          <div style='font-weight:normal'></div>
          normal    標(biāo)準(zhǔn)的字符
          bold      粗體字符
          bolder    更粗的字符
          lighter   更細(xì)的字符
          也可以使用數(shù)字表示,范圍為100~900

          5).字體大小

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

          2.邊框Border

          首先說一下邊框風(fēng)格,它的風(fēng)格比較多,常用的一般是實(shí)線為主:

          <div style='border-style:none'></div>
          hidden     隱藏邊框
          dotted     點(diǎn)狀邊框
          dashed     虛線邊框
          solid      實(shí)線邊框
          double     雙線邊框
          groove     3D凹槽邊框
          ridge      3D壟狀邊框
          inset      3D inset邊框
          outset     3D outset邊框
          邊框也有四面,所以也會(huì)有上下左右
          所以有時(shí)候?yàn)榱烁_定位并修改樣式可以使用:
          border-top-style     上邊框樣式
          border-right-style   右邊框樣式
          border-bottom-style  下邊框樣式
          border-left-style    左邊框樣式

          先定義邊框的寬度 風(fēng)格和顏色,然后定義邊框的其它屬性。

          1).邊框形狀

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

          2).邊框陰影

          <div style='box-shadow:1px 2px 2px 2px red'></div>
          參數(shù)含義:
          邊框各個(gè)方向的大小和顏色

          3).邊框圖片

          <div style='border-image:url(1.png) 30 30 10 round'></div>
          參數(shù)含義:
          邊框圖片的路徑
          圖片邊框向內(nèi)偏移
          圖片邊框的寬度
          邊框圖像區(qū)域超出邊框的量
          圖像邊框是否應(yīng)平鋪(repeated)、鋪滿(rounded)或拉伸(stretched)。

          總結(jié)

          這篇文章主要介紹了CSS樣式更改篇中的字體設(shè)置Font&邊框Border設(shè)置,希望讓大家對CSS選擇器有個(gè)簡單的認(rèn)識(shí)和了解。
          ****看完本文有收獲?請轉(zhuǎn)發(fā)分享給更多的人****

          IT共享之家

          想要學(xué)習(xí)更多,請前往Python爬蟲與數(shù)據(jù)挖掘?qū)S镁W(wǎng)站:http://pdcfighting.com/

          S 鼠標(biāo)框選(頁面選擇)時(shí)返回對應(yīng)的 HTML 或文案內(nèi)容

          一、需求背景

          1、項(xiàng)目需求

          當(dāng)用戶進(jìn)行鼠標(biāo)框選選擇了頁面上的內(nèi)容時(shí),把選擇的內(nèi)容進(jìn)行上報(bào)。

          2、需求解析

          雖然這需求就一句話的事,但是很顯然,沒那么簡單...

          因?yàn)槭髽?biāo)框選說起來簡單,就是選擇的內(nèi)容,但是這包含很多中情況,比如:只選擇文案、選擇圖片、選擇輸入框、輸入框中的內(nèi)容選擇、iframe、等。

          簡單總結(jié),分為以下幾點(diǎn):

          1. 選擇文案時(shí)
          2. 選擇圖片、svg、iframe、video、audio 等標(biāo)簽時(shí)
          3. 選擇 input、select、textarea 等標(biāo)簽時(shí)
          4. 選擇 input、textarea 標(biāo)簽內(nèi)容時(shí)
          5. 選擇類似 字符時(shí)
          6. 鍵盤全選時(shí)
          7. 鼠標(biāo)右鍵選擇
          8. 以上各模塊結(jié)合時(shí)
          9. 當(dāng)包含標(biāo)簽的時(shí)候,返回 html 結(jié)構(gòu),只有文本時(shí)返回文本內(nèi)容

          二、技術(shù)要點(diǎn)

          鼠標(biāo)框選包含以下幾點(diǎn):

          1. debounce 防抖
          2. addEventListener 事件監(jiān)聽
          3. Range 對象
          4. Selection 對象

          1、debounce

          老生常談的技術(shù)點(diǎn)了,這里不能用節(jié)流,因?yàn)榭隙ú荒苣闶髽?biāo)選擇的時(shí)候,隔一段時(shí)間返回一段內(nèi)容,肯定是選擇之后一起返回。

          這里用 debounce 主要也是用在事件監(jiān)聽和事件處理上。

          • 【debounce 掘金】
          • 【debounce CSDN】

          2、addEventListener

          事件監(jiān)聽,因?yàn)槭髽?biāo)選擇,不僅僅是鼠標(biāo)按下到鼠標(biāo)抬起,還包括雙擊、右鍵、全選。

          需要使用事件監(jiān)聽對事件作處理。

          • 【addEventListener MDN】

          3、Range

          Range 接口表示一個(gè)包含節(jié)點(diǎn)與文本節(jié)點(diǎn)的一部分的文檔片段。

          Range 是瀏覽器原生的對象。

          3.1. 創(chuàng)建 Range 實(shí)例,并設(shè)置起始位置

          <body>
            <ul>
              <li>Vite</li>
              <li>Vue</li>
              <li>React</li>
              <li>VitePress</li>
              <li>NaiveUI</li>
            </ul>
          </body>
          <script>
            // 創(chuàng)建 Range 對象
            const range = new Range()
            const liDoms = document.querySelectorAll("li");
            // Range 起始位置在 li 2
            range.setStartBefore(liDoms[1]);
            // Range 結(jié)束位置在 li 3
            range.setEndAfter(liDoms[2]);
            // 獲取 selection 對象
            const selection = window.getSelection();
            // 添加光標(biāo)選擇的范圍
            selection.addRange(range);
          </script>


          可以看到,選擇內(nèi)容為第二行和第三行

          3.1.1 瀏覽器兼容情況


          3.2. Range 屬性

          1. startContainer:起始節(jié)點(diǎn)。
          2. startOffset:起始節(jié)點(diǎn)偏移量。
          3. endContainer:結(jié)束節(jié)點(diǎn)。
          4. endOffset:結(jié)束節(jié)點(diǎn)偏移量。
          5. collapsed:范圍的開始和結(jié)束是否為同一點(diǎn)。
          6. commonAncestorContainer:返回完整包含 startContainer 和 endContainer 的最深一級(jí)的節(jié)點(diǎn)。

          3.2.1. 用我們上面創(chuàng)建的實(shí)例來看下 range 屬性的值


          3.2.2. 如果我們只選擇文本內(nèi)容時(shí)

          只選擇 li 中的 itePres


          可以看出 range 屬性對應(yīng)的值


          3.3. Range 方法

          1. cloneContents():復(fù)制范圍內(nèi)容,并將復(fù)制的內(nèi)容作為 DocumentFragment 返回。
          2. cloneRange():創(chuàng)建一個(gè)具有相同起點(diǎn)/終點(diǎn)的新范圍, 非引用,可以隨意改變,不會(huì)影響另一方。
          3. collapse(toStart):如果 toStart=true 則設(shè)置 end=start,否則設(shè)置 start=end,從而折疊范圍。
          4. compareBoundaryPoints(how, sourceRange):兩個(gè)范圍邊界點(diǎn)進(jìn)行比較,返回一個(gè)數(shù)字 -1、0、1。
          5. comparePoint(referenceNode, offset):返回-1、0、1具體取決于 是 referenceNode 在 之前、相同還是之后。
          6. createContextualFragment(tagString):返回一個(gè) DocumentFragment。
          7. deleteContents():刪除框選的內(nèi)容。
          8. extractContents():從文檔中刪除范圍內(nèi)容,并將刪除的內(nèi)容作為 DocumentFragment 返回。
          9. getBoundingClientRect():和 dom 一樣,返回 DOMRect 對象。
          10. getClientRects():返回可迭代的對象序列 DOMRect。
          11. insertNode(node):在范圍的起始處將 node 插入文檔。
          12. intersectsNode(referenceNode):判斷與給定的 node 是否相交。
          13. selectNode(node):設(shè)置范圍以選擇整個(gè) node。
          14. selectNodeContents(node):設(shè)置范圍以選擇整個(gè) node 的內(nèi)容。
          15. setStart(startNode, startOffset):設(shè)置起點(diǎn)。
          16. setEnd(endNode, endOffset):設(shè)置終點(diǎn)。
          17. setStartBefore(node):將起點(diǎn)設(shè)置在 node 前面。
          18. setStartAfter(node):將起點(diǎn)設(shè)置在 node 后面。
          19. setEndBefore(node):將終點(diǎn)設(shè)置為 node 前面。
          20. setEndAfter(node):將終點(diǎn)設(shè)置為 node 后面。
          21. surroundContents(node):使用 node 將所選范圍內(nèi)容包裹起來。

          3.4. 創(chuàng)建 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() 構(gòu)造函數(shù)

          const range = new Range()

          3.5. Range 兼容性


          4、Selection

          Selection 對象表示用戶選擇的文本范圍或插入符號(hào)的當(dāng)前位置。它代表頁面中的文本選區(qū),可能橫跨多個(gè)元素。

          4.1. 獲取文本對象

          window.getSelection()



          4.2. Selection 術(shù)語

          4.2.1. 錨點(diǎn) (anchor)

          錨指的是一個(gè)選區(qū)的起始點(diǎn)(不同于 HTML 中的錨點(diǎn)鏈接)。當(dāng)我們使用鼠標(biāo)框選一個(gè)區(qū)域的時(shí)候,錨點(diǎn)就是我們鼠標(biāo)按下瞬間的那個(gè)點(diǎn)。在用戶拖動(dòng)鼠標(biāo)時(shí),錨點(diǎn)是不會(huì)變的。

          4.2.2. 焦點(diǎn) (focus)

          選區(qū)的焦點(diǎn)是該選區(qū)的終點(diǎn),當(dāng)你用鼠標(biāo)框選一個(gè)選區(qū)的時(shí)候,焦點(diǎn)是你的鼠標(biāo)松開瞬間所記錄的那個(gè)點(diǎn)。隨著用戶拖動(dòng)鼠標(biāo),焦點(diǎn)的位置會(huì)隨著改變。

          4.2.3. 范圍 (range)

          范圍指的是文檔中連續(xù)的一部分。一個(gè)范圍包括整個(gè)節(jié)點(diǎn),也可以包含節(jié)點(diǎn)的一部分,例如文本節(jié)點(diǎn)的一部分。用戶通常下只能選擇一個(gè)范圍,但是有的時(shí)候用戶也有可能選擇多個(gè)范圍。

          4.2.4. 可編輯元素 (editing host)

          一個(gè)用戶可編輯的元素(例如一個(gè)使用 contenteditable 的 HTML 元素,或是在啟用了 designMode 的 Document 的子元素)。

          4.3. Selection 的屬性

          首先要清楚,選擇的起點(diǎn)稱為錨點(diǎn)(anchor),終點(diǎn)稱為焦點(diǎn)(focus)。

          1. anchorNode:選擇的起始節(jié)點(diǎn)。
          2. anchorOffset:選擇開始的 anchorNode 中的偏移量。
          3. focusNode:選擇的結(jié)束節(jié)點(diǎn)。
          4. focusOffset:選擇開始處 focusNode 的偏移量。
          5. isCollapsed:如果未選擇任何內(nèi)容(空范圍)或不存在,則為 true。
          6. rangeCount:選擇中的范圍數(shù),之前說過,除 Firefox 外,其他瀏覽器最多為1。
          7. type:類型:None、Caret、Range

          4.4. Selection 方法

          1. addRange(range): 將一個(gè) Range 對象添加到當(dāng)前選區(qū)。
          2. collapse(node, offset): 將選區(qū)折疊到指定的節(jié)點(diǎn)和偏移位置。
          3. collapseToEnd(): 將選區(qū)折疊到當(dāng)前選區(qū)的末尾。
          4. collapseToStart(): 將選區(qū)折疊到當(dāng)前選區(qū)的起始位置。
          5. containsNode(node, partlyContained): 判斷選區(qū)是否包含指定的節(jié)點(diǎn),可以選擇是否部分包含。
          6. deleteFromDocument(): 從文檔中刪除選區(qū)內(nèi)容。
          7. empty(): 從選區(qū)中移除所有范圍(同 `removeAllRanges()``,已廢棄)。
          8. extend(node, offset): 將選區(qū)的焦點(diǎn)節(jié)點(diǎn)擴(kuò)展到指定的節(jié)點(diǎn)和偏移位置。
          9. getRangeAt(index): 返回選區(qū)中指定索引處的 Range 對象。
          10. removeAllRanges(): 移除所有選區(qū)中的范圍。
          11. removeRange(range): 從選區(qū)中移除指定的 Range 對象。
          12. selectAllChildren(node): 選中指定節(jié)點(diǎn)的所有子節(jié)點(diǎn)。
          13. setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset): 設(shè)置選區(qū)的起始和結(jié)束節(jié)點(diǎn)及偏移位置。
          14. setPosition(node, offset):collapse 的別名

          4.5. Selection 兼容性


          三、項(xiàng)目實(shí)現(xiàn)

          1、實(shí)現(xiàn)思路

          1. 先獲取選擇的內(nèi)容,開發(fā) getSelectContent 函數(shù)
          2. 對獲取的內(nèi)容進(jìn)行判斷,是否存在 selection 實(shí)例,沒有直接返回 null
          3. 判斷 selection 實(shí)例的 isCollapsed 屬性 沒有選中,對 selection 進(jìn)行 toString().trim() 操作,判斷內(nèi)容 有內(nèi)容,直接返回 text 類型 無內(nèi)容,返回 null 有選中,則判斷內(nèi)容
          4. 判斷選中的內(nèi)容有沒有節(jié)點(diǎn) 沒有節(jié)點(diǎn),則和沒有選中一樣處理,進(jìn)行 toString().trim() 操作,判斷內(nèi)容 有內(nèi)容,直接返回 text 類型 無內(nèi)容,返回 null 有節(jié)點(diǎn),進(jìn)行 toString().trim() 操作,判斷內(nèi)容 沒有內(nèi)容,判斷是否有特殊節(jié)點(diǎn) 有 'iframe', 'svg', 'img', 'audio', 'video' 節(jié)點(diǎn),返回 html 類型 有 'input', 'textarea', 'select',判斷 value 值,是否存在 存在:返回 html 類型 不存在:返回 null 沒有特殊節(jié)點(diǎn),返回 null 有內(nèi)容,返回 html 類型
          5. 對鼠標(biāo) mousedown、mouseup 事件和 selectionchange、contextmenu、dblclick 事件進(jìn)行監(jiān)聽,觸發(fā) getSelectContent 函數(shù)
          6. 在需要的地方進(jìn)行 debounce 防抖處理

          2、簡易流程圖


          2、Debounce 方法實(shí)現(xiàn)

          2.1. JS

          function debounce (fn, time = 500) {
            let timeout = null; // 創(chuàng)建一個(gè)標(biāo)記用來存放定時(shí)器的返回值
            return function () {
              clearTimeout(timeout) // 每當(dāng)觸發(fā)時(shí),把前一個(gè) 定時(shí)器 clear 掉
              timeout = setTimeout(() => { // 創(chuàng)建一個(gè)新的 定時(shí)器,并賦值給 timeout
                fn.apply(this, arguments)
              }, time)
            }
          }

          2.2. TS

          /**
           * debounce 函數(shù)類型
           */
          type DebouncedFunction<F extends (...args: any[]) => any> = (...args: Parameters<F>) => void
          /**
           * debounce 防抖函數(shù)
           * @param {Function} func 函數(shù)
           * @param {number} wait 等待時(shí)間
           * @param {false} immediate 是否立即執(zhí)行
           * @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
          }
          /**
           * 獲取選擇的內(nèi)容
           * @returns {null | IGetSelectContentProps} 返回選擇的內(nèi)容
           */
          const getSelectContent = (): null | IGetSelectContentProps => {
            const selection = window.getSelection()
            if (selection) {
              // 1. 是焦點(diǎn)在 input 輸入框
              // 2. 沒有選中
              // 3. 選擇的是輸入框
              if (selection.isCollapsed) {
                return selection.toString().trim().length
                  ? {
                      type: 'text',
                      content: selection.toString().trim()
                    }
                  : null
              }
              // 獲取選擇范圍
              const range = selection.getRangeAt(0)
              // 獲取選擇內(nèi)容
              const rangeClone = range.cloneContents()
              // 判斷選擇內(nèi)容里面有沒有節(jié)點(diǎn)
              if (rangeClone.childElementCount > 0) {
                // 創(chuàng)建 div 標(biāo)簽
                const container = document.createElement('div')
                // div 標(biāo)簽 append 復(fù)制節(jié)點(diǎn)
                container.appendChild(rangeClone)
                // 如果復(fù)制的內(nèi)容長度為 0
                if (!selection.toString().trim().length) {
                  // 判斷是否有選擇特殊節(jié)點(diǎn)
                  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. 只需要文本

          /**
           * 獲取框選的文案內(nèi)容
           * @returns {string} 返回框選的內(nèi)容
           */
          const getSelectTextContent = (): string => {
            const selection = window.getSelection()
            return selection?.toString().trim() || ''
          }

          4、添加事件監(jiān)聽

          // 是否時(shí)鼠標(biāo)點(diǎn)擊動(dòng)作
          let selectionchangeMouseTrack: boolean = false
          const selectionChangeFun = debounce(() => {
            const selectContent = getSelectContent()
            console.log('selectContent', selectContent)
            // todo... 處理上報(bào)
            selectionchangeMouseTrack = false
          })
          // 添加 mousedown 監(jiān)聽事件
          document.addEventListener('mousedown', () => {
            selectionchangeMouseTrack = true
          })
          // 添加 mouseup 監(jiān)聽事件
          document.addEventListener(
            'mouseup',
            debounce(() => {
              selectionChangeFun()
            }, 100)
          )
          // 添加 selectionchange 監(jiān)聽事件
          document.addEventListener(
            'selectionchange',
            debounce(() => {
              if (selectionchangeMouseTrack) return
              selectionChangeFun()
            })
          )
          // 添加 dblclick 監(jiān)聽事件
          document.addEventListener('dblclick', () => {
            selectionChangeFun()
          })
          // 添加 contextmenu 監(jiān)聽事件
          document.addEventListener(
            'contextmenu',
            debounce(() => {
              selectionChangeFun()
            })
          )

          也可以進(jìn)行封裝

          /**
           * addEventlistener function 類型
           */
          export interface IEventHandlerProps {
            [eventName: string]: EventListenerOrEventListenerObject
          }
          
          let selectionchangeMouseTrack: boolean = false
          const eventHandlers: IEventHandlerProps = {
            // 鼠標(biāo) down 事件
            mousedown: () => {
              selectionchangeMouseTrack = true
            },
            // 鼠標(biāo) 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、返回內(nèi)容

          5.1. 純文本內(nèi)容


          5.2. html 格式


          6. 完整 JS 代碼

          function debounce (fn, time = 500) {
            let timeout = null; // 創(chuàng)建一個(gè)標(biāo)記用來存放定時(shí)器的返回值
            return function () {
              clearTimeout(timeout) // 每當(dāng)觸發(fā)時(shí),把前一個(gè) 定時(shí)器 clear 掉
              timeout = setTimeout(() => { // 創(chuàng)建一個(gè)新的 定時(shí)器,并賦值給 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. 是焦點(diǎn)在 input 輸入框
              // 2. 沒有選中
              // 3. 選擇的是輸入框
              if (selection.isCollapsed) {
                return selection.toString().trim().length ? {
                  type: 'text',
                  content: selection.toString().trim()
                } : null
              }
              // 獲取選擇范圍
              const range = selection.getRangeAt(0);
              // 獲取選擇內(nèi)容
              const rangeClone = range.cloneContents()
              // 判斷選擇內(nèi)容里面有沒有節(jié)點(diǎn)
              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
            })
          }

          四、總結(jié)

          1. 鼠標(biāo)框選上報(bào)能監(jiān)控用戶在頁面的行為,能為后續(xù)的數(shù)據(jù)分析等提供便利
          2. 基于 JS 中的 Selection 和 Range 實(shí)現(xiàn)的,使用原生 JS
          3. 涉及到的操作比較多,包含鍵盤、鼠標(biāo)右鍵、全選等
          4. 能對框選的內(nèi)容進(jìn)行分類,區(qū)別 html 和 text,更方便的看出用戶選擇了哪些內(nèi)容

          主站蜘蛛池模板: 无码人妻精品一区二区三区夜夜嗨| 国内精品无码一区二区三区| 91福利视频一区| 国内精品无码一区二区三区| 亚洲AV无码一区二区乱孑伦AS | 国产精品毛片一区二区三区| 99久久国产精品免费一区二区| 国产视频一区在线观看| 精品无码人妻一区二区三区品| 国产乱码精品一区二区三区| 一区二区手机视频| 性无码一区二区三区在线观看| 亚洲一区二区三区久久久久| 国产大秀视频一区二区三区| 一夲道无码人妻精品一区二区| 射精专区一区二区朝鲜| 国产精品久久一区二区三区| 国产一区二区三区四| 日本不卡一区二区视频a| 少妇一晚三次一区二区三区| 爆乳熟妇一区二区三区霸乳 | 亚洲av无码一区二区三区不卡| 中文字幕人妻第一区| 国产情侣一区二区三区| 麻豆果冻传媒2021精品传媒一区下载| 变态调教一区二区三区| 亚洲一区二区免费视频| 精品国产一区在线观看| 精品亚洲一区二区三区在线播放| 无码日韩人妻AV一区二区三区| 中文字幕一区二区精品区| 精品一区二区ww| 久久精品道一区二区三区| AV天堂午夜精品一区二区三区| 亚洲色婷婷一区二区三区| 日本高清一区二区三区| 免费播放一区二区三区| 在线一区二区三区| 久久精品一区二区三区资源网 | 亚洲av无码一区二区三区四区| 亚洲日本中文字幕一区二区三区|