花潮论坛

搜索
热搜: 活动 交友 discuz
查看: 13|回复: 3

初识 Range 对象

[复制链接]
  • TA的每日心情
    奋斗
    2026-7-4 09:39
  • 签到天数: 1862 天

    [LV.Master]伴坛终老

    3267

    主题

    13万

    回帖

    29万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

    发表于 2026-7-4 09:40 | 显示全部楼层 |阅读模式

    请马上登录,朋友们都在花潮里等着你哦:)

    您需要 登录 才可以下载或查看,没有账号?立即注册

    x
    Range 接口表示一个包含节点与文本节点的一部分的文档片段。(——MDN官网的定义)

    Selection 和 Range 都是现代浏览器原生提供的对象和 API,一般通称为 Selection 和 Range API(接口)。Selection(选区)是“视图状态”(用户在屏幕上高亮了什么),Range(范围)是“数据模型”(描述选了什么)。换言之,Range 是 Selection 的基本概念,Range 的本质是对一对“边界点”的描述——范围的起点和范围的终点。通俗说来,Selection 是用户“拖蓝”的视觉效果,Range 则是“拖蓝”背后的数据描述;另外,很重要的,Selection 和 Range 的关系是包含关系,具体而言,Selection 是 Range 的容器,尽管有时候 Range 可以不依托显性提供的 Selection 而独立存在。

    通常,Selection 和 Range 总是一同出现的:先获取 Selection 对象,然后通过 Range 来做什么。其一,使用 Range 读取基于 Selection 的相关数据(例如锚点和锚点偏移量、焦点和焦点偏移量等),其二,创建新的范围并添加到 Selection 对象中。不过,Range 在特定场合可以不依赖 Selection,可以独立使用。学习 Range 需要了解其构造、属性和方法。

    一、Range的获取和构造

    // 获取选区 Range(参数为选区序号)
    Selection.getRangeAt(index)
    
    // 创建 Range 方法一
    document.createRange()
    
    // 创建 Range 方法二
    new Range()
        

    二、Range的常用属性

    1. collapsed : 只读,返回 Range 的起始位置和终止位置是否相同(true/false)
    2. commonAncestorContainer :只读,返回完整包含 startContainer 和 endContainer 的最深一级的节点
    3. endContainer :只读,返回包含 Range 终点的节点
    4. endOffset :只读,返回 Range 终点在 endContainer 中的位置
    5. startContainer :只读,返回包含 Range 开始的节点
    6. startOffset :只读,返回 Range 在 startContainer 中的起始位置

    三、Range的常用方法

    1. collapse() :将 Range 折叠到其边界的端点
    2. compareBoundaryPoints() :将 Range 的边界与另一个 Range 的边界进行比较
    3. compareNode() :返回一个常量,表示节点是否在 Range 的前、后、中、外
    4. comparePoint() :返回 -1、0 或 1,分别表示端点在 Range 之前、内部还是之后
    5. cloneContents() :返回一个复制 Range 中所有节点的文档片段
    6. cloneRange() :返回一个拥有和原 Range 对象相同端点的克隆的 Range 对象
    7. createContextualFragment() :返回从给定的代码字符串创建的文档片段
    8. deleteContents() :从 Document 中移除 Range 内容
    9. detach() :将 Range 从使用状态释放,提高性能
    10. extractContents() :将 Range 的内容从文档树移动到一个文档片段
    11. getBoundingClientRect() :返回一个 DOMRect 对象,其绑定了 Range 的整个内容;这将是通过 range.getClientRects() 返回所有边界矩形集合的其中之一
    12. getClientRects() :返回一个 DOMRect 列表对象,该列表汇总了 Range 中所有元素的 Element.getClientRects() 返回结果
    13. isPointInRange() :返回一个 boolean,表示点端点是否在 Range 中
    14. insertNode() :在 Range 开头插入一个节点
    15. intersectsNode() :返回一个 boolean,表示给定的节点是否与 Range 相交
    16. selectNode() :设置 Range 包含某个节点及其他的内容
    17. selectNodeContents() :设置 Range 包含某个节点的内容
    18. setEnd() :设置 Range 的终点
    19. setStart() :设置 Range 的起点
    20. setEndAfter() :以另一个节点为基准,设置 Range 的终点位置
    21. setEndBefore() :以另一个节点为基准,设置 Range 的终点位置
    22. setStartAfter() :以另一个节点为基准,设置 Range 的起点位置
    23. setStartBefore() :以另一个节点为基准,设置 Range 的起点位置
    24. surroundContents() :将 Range 中的内容移动到一个新的节点
    25. toString() :返回 Range 中的文本

    四、应用实例

    【例一】在简单文本节点中创建选区范围并“拖蓝”

    <!-- 下面的 p 标签仅包含一个 #Text 节点,本例目标是将 “花朝论坛” 拖蓝 -->
    <p id="p1">花朝论创建于哪一年?</p>
    <p><button onclick="selectText(p1,0,4)">创建选区(0~4)</button></p>
    
    <script>
        // 创建选区函数(仅适用于理想型文本节点 —— #Text节点)
        // 参数 start :选区范围起点位置;end :选区范围结束位置
        // 例如,0~4表示选取范围从文本节点第一个字符的左边起算,到第4个字符的右边结束
        function selectText(elm, start, end) {
            const range = new Range(); // 创建范围
            const node = p1.firstChild; // 指定节点(p1的第一个文本节点)
            range.setStart(node, start); // 范围从 node 节点的第 start 位置开始
            range.setEnd(node, end); // 范围从 node 节点的第 end 偏移量结束
            const sel = window.getSelection(); // 获取选区对象
            sel.removeAllRanges(); // 清除选区之前已有全部 Range 范围
            sel.addRange(range); // 在选区中添加新创建的 Range
        }
    </script>
        
    当确定待处理的节点是文本节点(#text),Range 的 setStart(节点, 位置)setEnd(节点, 位置) 方法中的位置参数以字符索引做参照,0 表示节点内第一个字符的前面的位置(就像在新建一个Word文档时输入光标的位置),1 表示节点内第一个字符和第二个字符之间的位置,2 表示第二个字符和第三个字符之间的位置,其它以此类推。

    【例二】在复杂的混合节点环境中创建选区

    <!-- 下面的 p 标签有文本节点和元素节点,环境复杂,共 5 个子节点 -->
    <!-- 节点索引0. #text : 才思敏捷、多才多艺的 -->
    <!-- 节点索引1. B :<b>樵歌</b> -->
    <!-- 节点索引2. #text :是 -->
    <!-- 节点索引3. MARK :<mark>花潮论坛</mark> -->
    <!-- 节点索引4. #text :的联合创建者之一 -->
    <p id="p2">才思敏捷、多才多艺的<b>樵歌</b>是<mark>花潮论坛</mark>的联合创建者之一</p>
    <p><button onclick="selectText(p2,0,4)">创建选区(0~4)</button></p>
    
    <script>
        // 创建选区函数(适用于混合节点环境,若子节点是#text会全选)
        // 参数 start :选区范围起点节点;end :选区范围结束节点(数孩子节点索引而不是字符索引)
        // 例如,0~4表示选取范围从0号索引节点(对应第一个孩子节点)前起算,
        // 到第4个孩子节点(对应第五个孩子节点)之前结束
        function selectText(elm, start, end) {
            const sel = window.getSelection(); // 获取选区对象
            const range = new Range(); // 创建范围
            const total = p2.childNodes.length; // 获取子节点总数
            //console.log(total); // -> 5
            if (end > total) end = total; // 结束节点索引号不能超出总子节点数量
            range.setStart(p2, start); // 范围从 p2 中索引号号是第 start 孩子节点前开始
            range.setEnd(p2, end); // 范围从 p2 中索引号是第 end 孩子节点前结束
            sel.removeAllRanges(); // 清除选区已有全部 Range 范围
            sel.addRange(range); // 添加新创建的 Range
        }
    </script>
        
    如果确定容器内的子节点为混合节点,对子节点的处理变为复杂,Range 的 setStart 和 setEnd 方法的位置参数表示的是子节点在容器中的索引序号,序号从 0 开始,换言之,位置参数不是指代字符索引,而是指向子节点序号。这是 Selection + Range API的浏览器底层设计。
    如果需要精准按字符索引实现拖蓝或读取拖蓝数据,需要遍历所有子节点、找出最底层的 #text 并通过相应算法加以实现。

    【例三】利用 Range 在指定节点前添加新的文本节点

    <style>
        #mdiv { margin: 20px auto 0; width: 600px; font-size: 30px; border: 1px solid gray; padding: 12px; }
    </style>
    
    <div id="mdiv" title="点击补全句子">只怕有心人</div>
    
    <script>
        const mdiv = document.getElementById('mdiv');
        
        mdiv.onclick = () => {
            //console.log(mdiv.childNodes.length); // -> 1
            
            const txt = '世上无难事 '; // 待创建文本节点内容
            if (mdiv.innerText.includes(txt)) return; // 避免重复补全
            const txtNode = document.createTextNode(txt); // 创建文本节点
            const range = new Range(); // 创建新 Range 对象
            range.selectNode(mdiv.firstChild); // 选择 mdiv 容器第一个子节点为 range
            range.insertNode(txtNode); // range 前插入新创建的文本节点
            
            //console.log(mdiv.childNodes.length); // -> 2
        };
    </script>
        
    Range 的 selectNode() 方法将 Range 设置为包含整个 Node 及其内容。insertNode() 方法在 Range 的起始位置插入新的节点。

    【例四】借助 Range 创建范围删除表格指定行

    <style>
        #tab { width: 600px; border-collapse: collapse; }
        #tab th, #tab td { border: 1px solid #ccc; padding: 8px; }
        #tab th { background: #ddd; }
    </style>
    
    <p><button onclick="delTabRow();">删除表格的最后一行</button></p>
    <table id="tab">
        <tr>
            <th>表头单元格一</th>
            <th>表头单元格二</th>
        </tr>
        <tr>
            <td>内容1</td>
            <td>内容2</td>
        </tr>
        <tr>
        <td>内容3</td>
            <td>内容4</td>
        </tr>
        <tr>
            <td>内容5</td>
            <td>内容6</td>
        </tr>
        <tr>
        <td>内容7</td>
            <td>内容8</td>
        </tr>
    </table>
    
    <script>
        // 删除表格最后一行
        function delTabRow() {
            const tab = document.getElementById('tab'); // 获得表格dom实体
            const rest = tab.rows.length - 1; // 最后一行
            if (rest <= 0) return; // 如果表格仅剩一行放过(不执行下面的代码)
            const row = tab.rows[rest]; // 获取表格的第后一行
            const range = document.createRange(); // 创建 Range 范围
            range.setStartBefore(row); // Range 起点设在表格最后一行的前边
            range.setEndAfter(row); // Range 终点设在表格最后一行的后边
            range.deleteContents(); // 删除 Range 范围包裹的内容(即 <tr>...</tr>
        }
    </script>
        
    Range 对象的 setStartBefore(节点)setStartAfter(节点) 方法在指定节点前、后创建范围,deleteContents() 方法则删除范围内的内容。

    【例五】搬运元素节点

    <style>
        .mydiv { margin: 20px; padding: 12px; border: 1px solid gray; width: 600px; min-height: 100px; }
        .mydiv > p { margin: 0; padding: 0; }
    </style>
    
    <h2>单击第一个方框</h2>
    <div id="d1" class="mydiv" onclick="moveline();">
        <p>红豆生南国,</p>
        <p>春来发几枝。</p>
        <p>愿君多采撷,</p>
        <p>此物最相思。</p>
    </div>
    <div id="d2" class="mydiv"></div>
    
    <script>
        const rawHTML = d1.innerHTML; // d1 原始代码
    
        // 逐行移动p标签到另一个方框
        function moveline() {
            let lock = false; // 搬运锁
    
            // 如果搬完了
            if (d1.innerHTML.trim() === '') {
                d1.innerHTML = rawHTML; // 重置 d1 内容
                d2.innerHTML = ''; // d2 清空
                lock = true; // 上锁
            }
    
            if (lock) return; // 上锁状态下不执行下面的任务
            
            // 核心 :将 d1 的第一个 p 标签移动到 d2
            const ps = d1.querySelectorAll('p'); // 获得所有 p 标签节点
            if (ps.length < 1) return; // p 标签没了就不要继续干活(会犯错误)
            const range = document.createRange(); // 创建 Range 范围
            range.selectNode(ps[0]); // Range 选择第一个 p 节点
            const frg = range.extractContents(); // 提取 p 节点内容(会从原始宿主中删除节点)加入文档碎片
            d2.appendChild(frg); // 文档碎片追加给 d2
        }
    </script>
        
    Range 的 extractContents() 方法提取 Range 范围内的节点(可以是文本或元素,例如上例的第一个 p 标签),并删除原始范围,返回的是文档碎片(相当于 DocumentFragment API 生成的碎片),该碎片可以追加给另外的目标元素。

    Range 方法较多并且基本都很抽象、复杂,不过学习难度不在其所提供的方法自身,这些方法通过潜心学习并不断实践总能融会贯通,难的是编程方法——比如如何将相关知识有机结合起来去实现预定目标。

    本文提供的几个实例不可能覆盖所有的 Range 知识,若能从中获得一些启发和灵感便达到本文的目的。

    评分

    参与人数 1威望 +30 金钱 +60 经验 +30 收起 理由
    梦江南 + 30 + 60 + 30 匠心独运,细节精致入微!

    查看全部评分

  • TA的每日心情
    奋斗
    2026-7-4 09:39
  • 签到天数: 1862 天

    [LV.Master]伴坛终老

    3267

    主题

    13万

    回帖

    29万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

     楼主| 发表于 2026-7-4 09:41 | 显示全部楼层
    本帖尚未仔细校对,可能会对存在的问题随时做出修正
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2026-7-4 08:12
  • 签到天数: 769 天

    [LV.10]以坛为家III

    542

    主题

    3万

    回帖

    7万

    积分

    贵宾

    Rank: 7Rank: 7Rank: 7Rank: 7Rank: 7Rank: 7Rank: 7

    花潮美女鼠牛虎兔龙蛇马羊猴鸡狗猪缤纷心情心曲飞扬花好月圆飞龙在天音画大师天籁妙音共看流星风雨同行我心永远喜乐安康花潮贵宾

    发表于 2026-7-4 11:47 | 显示全部楼层
    谢谢黑黑老师辛苦,学习了。
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2026-7-4 09:32
  • 签到天数: 16 天

    [LV.4]偶尔看看III

    58

    主题

    2188

    回帖

    5794

    积分

    论坛元老

    Rank: 80Rank: 80Rank: 80Rank: 80Rank: 80Rank: 80Rank: 80Rank: 80

    发表于 2026-7-4 13:38 | 显示全部楼层
    好复杂,精彩的分享
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    小黑屋|手机版|Archiver|服务支持:DZ动力|huachaowang.com Inc. ( 蜀ICP备17032287号-1 )

    GMT+8, 2026-7-4 14:44 , Processed in 0.092487 second(s), 26 queries .

    Powered by Discuz! X3.4

    © 2001-2013 Comsenz Inc.

    快速回复 返回顶部 返回列表