CSS 實現樹狀結構目錄

XboxYan發表於2022-05-09
歡迎關注微信公眾號: 前端偵探

最近在專案中碰到了一個這樣的樹狀結構目錄,效果如下

Kapture 2022-04-10 at 17.48.33

如果用到了 Ant Design 這樣的框架,那可以直接用現成的元件。如果沒有用到這樣的框架呢?其實純 CSS 也是可以搞定的,下面看看如何實現的,還有很多你可能不知道 CSS 小技巧哦~

一、details 和 summary

首先,實現這樣一個互動需要利用到 detailssummary,天然地支援內容展開和收起。這裡有一個 MDN 的例子

<details>
  <summary>System Requirements</summary>
  <p>Requires a computer running an operating system. The computer
  must have some memory and ideally some kind of long-term storage.
  An input device as well as some form of output device is
  recommended.</p>
</details>

效果如下

Kapture 2022-04-10 at 18.09.18

還可以支援多層巢狀,比如

<details>
  <summary>
    <span class="tree-item">專案1</span>
  </summary>
  <details>
    <summary>
      <span class="tree-item">資料夾0</span>
    </summary>
  </details>
  <details>
    <summary>
      <span class="tree-item">資料夾1-1</span>
    </summary>
    <details>
      <summary>
        <span class="tree-item">資料夾1-1-2</span>
      </summary>
    </details>
    <details>
      <summary>
        <span class="tree-item">資料夾1-1-3</span>
      </summary>
      <details>
        <summary>
          <span class="tree-item">資料夾1-1-3-1</span>
        </summary>
      </details>
      <details>
        <summary>
          <span class="tree-item">資料夾1-1-3-2</span>
        </summary>
      </details>
    </details>
    <details>
      <summary>
        <span class="tree-item">資料夾1-1-4</span>
      </summary>
    </details>
  </details>
  <details>
    <summary>
      <span class="tree-item">資料夾1-2</span>
    </summary>
    <details>
      <summary>
        <span class="tree-item">資料夾1-2-1</span>
      </summary>
    </details>
  </details>
  <details>
    <summary>
      <span class="tree-item">資料夾1-3</span>
    </summary>
  </details>
  <details>
    <summary>
      <span class="tree-item">資料夾1-4</span>
    </summary>
  </details>
</details>

效果如下

Kapture 2022-04-10 at 18.24.16

是不是有點亂了,還看不出層級關係?沒關係,下面可以自定義樣式

二、自定義樹形結構

1. 縮排層級

首先需要突出層級關係,可以給每一層級加一個內邊距

details{
  padding-left: 10px
}

全部展開的樣子如下

image-20220410183028214

2. 自定義三角

這個“黑色三角”太難看了,需要去掉,從開發者工具可以看到,這個“黑色三角”其實是 ::marker生成的,而這個 ::marker是通過list-style生成,如下

image-20220410183611323

所以,去除這個“黑色三角”就容易了

summary{
  list-style: none;
}
舊版本瀏覽器需要通過專門的偽元素修改,::-webkit-details-marker::-moz-list-bullet ,現在都統一成了list-style

image-20220410184100481

然後,可以指定自定義的三角圖示,展開的樣式可以通過details[open]來定義

summary{
    background: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.354 2.646A.5.5 0 0 0 4.5 3v6a.5.5 0 0 0 .854.354l3-3a.5.5 0 0 0 0-.708l-3-3z' fill='%23000' fill-opacity='.45'/%3E%3C/svg%3E") 4px center no-repeat;
}
details[open]>summary{
    background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.354 5.354A.5.5 0 0 0 9 4.5H3a.5.5 0 0 0-.354.854l3 3a.5.5 0 0 0 .708 0l3-3z' fill='%23000' fill-opacity='.45'/%3E%3C/svg%3E");
}

簡單美化以後如下

image-20220410184519528

3. 樹形結構最深層級

上面的小三角還有點問題,比如這樣一個層級

image-20220410185008008

當沒有展開內容時,仍然可以點選切換,所以需要限制一下,這種情況下不顯示小三角,表示已經到最底層目錄了,不可再展開了。

要實現這種也很簡單,仔細觀察 HTML 結構,當沒有展開內容時,就僅存 summary元素了,是唯一的元素,提到“唯一”,可以想到:only-child,所以實現就是:

summary:not(:only-child){
    background: url("xxx") 4px center no-repeat;
}
details[open]>summary:not(:only-child){
    background-image: url("xxx");
}

這樣就可以很直觀的看到樹形目錄是否已經處於最深處了

image-20220410191039926

三、自定義點選範圍

一般情況下,自定義到上面這裡就可以結束了。但是,還有一點點小的體驗問題,比如加個 hover 效果

.tree-item:hover{
  background: aliceblue;
}

Kapture 2022-04-10 at 19.22.22

很明顯可以看到 層級越深,點選範圍越小。那能不能做成通欄都可以點選的呢?

這時,我們可以藉助負的margin來實現,比如給一個足夠大的 padding,然後通過負的margin 歸位,實現如下

.tree-item{
      /**/
    padding-left: 400px;
    margin-left: -400px;
}

這樣就是通欄觸發了,點選區域足夠大

Kapture 2022-04-10 at 19.38.06

由於左邊是足夠大,已經超出樹狀結構了,如果限定在樹狀結構類,可以通過父級超出隱藏或者滾動來解決

.tree{
  overflow: auto;
}

Kapture 2022-04-10 at 19.46.15

還有個問題是 hover背景遮蓋了父級的小三角,而且這種截斷的方式也沒法設定圓角。怎麼解決呢?

可以單獨使用一層偽元素,然後利用“不完全絕對定位”,什麼意思呢?設定一個元素為絕對定位,如果只指定一個方向,比如水平方向(left/right),那麼該元素的最終表現是水平方向上的表現依賴於第一個定位的父級,垂直方向上不依賴於定位父級,仍然處於預設位置

在這個例子中,我們可以只指定水平方向上的定位屬性,這樣可以保證水平方向的尺寸跟隨最外層父級,還可以通過z-index改變層級,不遮擋父級小三角,實現如下

.tree{
  position: relative;
}
.tree-item::after{
    content: '';
    position: absolute;
    left: 10px;
    right: 10px;/*水平方向的尺寸依賴於父級.tree*/
    height: 38px;
    background: #EEF2FF;
    border-radius: 8px;
    z-index: -1;
    opacity: 0;
    transition: .2s;
}
.tree-item:hover::after{
    opacity: 1;
}

這樣就比較完美了

Kapture 2022-04-10 at 20.02.48

還可以加上檔案圖示

.tree-item::before{
    content: '';
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    margin-right: 8px;
    background: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M.833 3.75c0-.92.746-1.667 1.667-1.667h5.417c.247 0 .481.11.64.3l1.833 2.2h7.11c.92 0 1.667.747 1.667 1.667v10c0 .92-.747 1.667-1.667 1.667h-15c-.92 0-1.667-.746-1.667-1.667V3.75zm6.693 0H2.5v4.584h15V6.25H10a.833.833 0 0 1-.64-.3l-1.834-2.2zM17.5 10h-15v6.25h15V10z' fill='%23000' fill-opacity='.45'/%3E%3C/svg%3E") center no-repeat;
}
details[open]>summary:not(:only-child)>.tree-item::before{
    background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.917 2.083c.247 0 .481.11.64.3l1.833 2.2h5.443c.92 0 1.667.747 1.667 1.667v1.667h.833a.833.833 0 0 1 .817.997l-1.666 8.333a.833.833 0 0 1-.817.67H1.677a.814.814 0 0 1-.157-.013.83.83 0 0 1-.687-.82V3.75c0-.92.746-1.667 1.667-1.667h5.417zM10 6.25a.833.833 0 0 1-.64-.3l-1.834-2.2H2.5v6.564l.441-1.766a.833.833 0 0 1 .809-.631h12.083V6.25H10zm-7.266 10L4.4 9.584h12.916l-1.334 6.666H2.733z' fill='%23000' fill-opacity='.45'/%3E%3C/svg%3E");
}

這樣就得到了文章開頭所示的效果

Kapture 2022-04-10 at 17.48.33

完整程式碼可以訪問:CSS tree(codepen.io) 或者 CSS tree (juejin.cn)

四、JS 資料渲染

大部分情況下,這類樹狀結構都是通過資料渲染出來的,假設有這樣一段 json資料

const treeData = [
    {
        "id": 2,
        "name": "專案1",
        "parentId": 1,
        "fileCount": 14,
        "children": [
            {
                "id": 8,
                "name": "資料夾",
                "parentId": 2,
                "fileCount": 12,
                "children": [
                    {
                        "id": 137,
                        "name": "sdd",
                        "parentId": 8,
                        "fileCount": 0
                    }
                ]
            },
            {
                "id": 221,
                "name": "chrome test",
                "parentId": 2,
                "fileCount": 2
            }
        ]
    },
    {
        "id": 52,
        "name": "專案2",
        "parentId": 1,
        "fileCount": 10,
        "children": [
            {
                "id": 54,
                "name": "資料夾2-1",
                "parentId": 52,
                "fileCount": 10,
                "children": [
                    {
                        "id": 55,
                        "name": "資料夾2-1-1",
                        "parentId": 54,
                        "fileCount": 0,
                        "children": [
                            {
                                "id": 56,
                                "name": "資料夾2-1-1-1",
                                "parentId": 55,
                                "fileCount": 0,
                                "children": [
                                    {
                                        "id": 57,
                                        "name": "資料夾2-1-1-1-1",
                                        "parentId": 56,
                                        "fileCount": 0,
                                        "children": [
                                            {
                                                "id": 58,
                                                "name": "資料夾2-1-1-1-1-1",
                                                "parentId": 57,
                                                "fileCount": 0
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    },
    {
        "id": 53,
        "name": "資料夾1",
        "parentId": 1,
        "fileCount": 12,
        "children": [
            {
                "id": 80,
                "name": "資料夾",
                "parentId": 53,
                "fileCount": 11
            },
            {
                "id": 224,
                "name": "資料夾2",
                "parentId": 53,
                "fileCount": 0
            }
        ]
    },
    {
        "id": 69,
        "name": "專案3",
        "parentId": 1,
        "fileCount": 55,
        "children": [
            {
                "id": 70,
                "name": "資料夾1",
                "parentId": 69,
                "fileCount": 12,
                "children": [
                    {
                        "id": 4,
                        "name": "1",
                        "parentId": 70,
                        "fileCount": 3,
                        "children": [
                            {
                                "id": 51,
                                "name": "資料夾2",
                                "parentId": 4,
                                "fileCount": 1
                            }
                        ]
                    }
                ]
            },
            {
                "id": 91,
                "name": "資料夾",
                "parentId": 69,
                "fileCount": 10
            },
            {
                "id": 102,
                "name": "資料夾",
                "parentId": 69,
                "fileCount": 10
            },
            {
                "id": 113,
                "name": "資料夾",
                "parentId": 69,
                "fileCount": 10
            },
            {
                "id": 121,
                "name": "資料夾的副本",
                "parentId": 69,
                "fileCount": 10
            },
            {
                "id": 136,
                "name": "點點點",
                "parentId": 69,
                "fileCount": 0
            },
            {
                "id": 140,
                "name": "hewei",
                "parentId": 69,
                "fileCount": 3,
                "children": [
                    {
                        "id": 142,
                        "name": "hewei02",
                        "parentId": 140,
                        "fileCount": 1
                    }
                ]
            }
        ]
    }
]

這樣一個可以無限巢狀的結構可以用遞迴來實現,這裡簡單實現一下

function gen_tree(childs){
  var html = ''
  childs.forEach(el => {
    html+=`<details>
    <summary>
       <span class="tree-item" title="${el.name}" data-id="${el.id}">${el.name}</span>
    </summary>`
    if (el.children && el.children.length) {
      html += gen_tree(el.children) // 如果有chidren就繼續遍歷
    }
    html+= `</details>`
  })
  return html;
}

然後通過innerHTML賦值就行了

tree.innerHTML = gen_tree(treeData)

效果如下

Kapture 2022-04-10 at 20.23.05

五、簡單總結一下

這樣就通過 CSS 實現了樹狀結構目錄,整體來說並不是很複雜,主要結構是 details 和 summary,然後是一些 CSS 選擇器的運用,這裡簡單總結一下:

  1. details 和 summary 原生支援展開收起
  2. details 和 summary 支援多層巢狀,這樣就得到了簡易的樹狀結構
  3. details 和 summary 支援多層巢狀,這樣就得到了簡易的樹狀結構
  4. summary 的黑色三角形是通過 list-style 生成的
  5. 展開的樣式可以通過 details[open] 來定義
  6. 逐層縮排可以通過給 details 新增內邊距實現
  7. 樹形結構最底層可以通過 :only-child 判斷
  8. 預設情況下點選區域逐層遞減,體驗不是很好
  9. 負的margin 和 padding 可以擴大點選區域
  10. “不完全絕對定位”可以指定一個方向上的尺寸依賴於定位父級
  11. 無限巢狀的結構可以用遞迴來實現

另外,相容性方面也非常不錯,主流瀏覽器均支援,IE 上雖然不支援 details 和 summary,但是通過 polyfill 解決,總的來說非常實用的,大可以放心使用。最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤

歡迎關注微信公眾號: 前端偵探

相關文章