Chào mừng bạn đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá hai đối tượng quan trọng trong JavaScript là `Selection` và `Range`, từ những kiến thức cơ bản đến nâng cao. Những công cụ này cho phép bạn quản lý và thao tác với các vùng chọn trong tài liệu web một cách hiệu quả. Hãy cùng Cafedev tìm hiểu cách sử dụng chúng để nâng cao kỹ năng lập trình JavaScript của bạn!

Trong chương này, chúng ta sẽ khám phá cách chọn(Selection) trong tài liệu, cũng như chọn(Selection) trong các trường biểu mẫu, chẳng hạn như <input>.
JavaScript có thể truy cập một lựa chọn hiện có, chọn/bỏ chọn các nút DOM toàn bộ hoặc một phần, loại bỏ nội dung đã chọn khỏi tài liệu, bọc nó vào một thẻ, và nhiều thứ khác.

Bạn có thể tìm thấy một số công thức cho các nhiệm vụ thông thường ở cuối chương, trong phần “Tóm tắt”. Có thể điều đó đáp ứng nhu cầu hiện tại của bạn, nhưng bạn sẽ nhận được nhiều hơn nếu đọc toàn bộ văn bản.

Các đối tượng RangeSelection cơ bản rất dễ nắm bắt, và sau đó bạn sẽ không cần công thức để làm cho chúng hoạt động như mong muốn.

1. Range

Khái niệm cơ bản của lựa chọn là Range, về cơ bản là một cặp “điểm biên giới”: điểm bắt đầu của phạm vi và điểm kết thúc của phạm vi.
Một đối tượng Range được tạo ra mà không cần tham số:

let range = new Range();

Sau đó, chúng ta có thể đặt ranh giới của lựa chọn bằng cách sử dụng range.setStart(node, offset)range.setEnd(node, offset).
Như bạn có thể đoán, sau này chúng ta sẽ sử dụng các đối tượng Range để chọn, nhưng trước tiên hãy tạo vài đối tượng như vậy.

Chọn một phần văn bản

Điều thú vị là đối số đầu tiên node trong cả hai phương thức có thể là một nút văn bản hoặc một nút phần tử, và ý nghĩa của đối số thứ hai phụ thuộc vào điều đó.
Nếu node là một nút văn bản, thì offset phải là vị trí trong văn bản của nó.

Ví dụ, với phần tử <p>Hello</p>, chúng ta có thể tạo phạm vi chứa các chữ cái “ll” như sau:


<p id="p">Hello</p>
<script>
  let range = new Range();
  range.setStart(p.firstChild, 2);
  range.setEnd(p.firstChild, 4);
  
  // toString of a range returns its content as text
  console.log(range); // ll
</script>

Ở đây chúng ta lấy phần tử con đầu tiên của <p> (đó là nút văn bản) và chỉ định các vị trí văn bản bên trong nó:

Chọn các nút phần tử

Ngược lại, nếu node là một nút phần tử, thì offset phải là số thứ tự của nút con.
Điều này rất tiện lợi để tạo các phạm vi chứa các nút như một khối, không dừng lại đâu đó bên trong văn bản của chúng.
Ví dụ, chúng ta có một đoạn tài liệu phức tạp hơn:


<p id="p">Example: <i>italic</i> and <b>bold</b></p>

Đây là cấu trúc DOM của nó với cả nút phần tử và nút văn bản:

Chúng ta sẽ tạo một phạm vi cho "Example: italic".
Như chúng ta có thể thấy, cụm từ này bao gồm chính xác hai nút con của <p>, với chỉ số 01:


– Điểm bắt đầu có <p> là nút cha node, và 0 là offset.
Vì vậy, chúng ta có thể đặt nó là range.setStart(p, 0).
– Điểm kết thúc cũng có <p>là nút cha node, nhưng 2 là offset (nó chỉ định phạm vi đến, nhưng không bao gồm offset).

Vì vậy, chúng ta có thể đặt nó là range.setEnd(p, 2).
Đây là bản demo. Nếu bạn chạy nó, bạn có thể thấy rằng văn bản được chọn:


<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();

  range.setStart(p, 0);
  range.setEnd(p, 2);


  // toString of a range returns its content as text, without tags
  console.log(range); // Example: italic

  // apply this range for document selection (explained later below)
  document.getSelection().addRange(range);
</script>

Đây là một bàn thử nghiệm linh hoạt hơn nơi bạn có thể đặt số bắt đầu/kết thúc của phạm vi và khám phá các biến thể khác:

run autorun
<p id="p">Example: <i>italic</i> and <b>bold</b></p>

From <input id="start" type="number" value=1> – To <input id="end" type="number" value=4>
<button id="button">Click to select</button>
<script>
  button.onclick = () => {
 
    let range = new Range();

    range.setStart(p, start.value);
    range.setEnd(p, end.value);
  

    // apply the selection, explained later below
    document.getSelection().removeAllRanges();
    document.getSelection().addRange(range);
  };
</script>

Ví dụ: chọn trong cùng một thẻ <p> từ vị trí 1 đến 4 sẽ cung cấp cho chúng ta khoảng bao gồm thẻ <i> in nghiêng và thẻ <b> in đậm:


Chúng ta không cần phải sử dụng cùng một nút trong setStartsetEnd. Một phạm vi có thể trải dài qua nhiều nút không liên quan. Điều quan trọng là điểm kết thúc phải nằm sau điểm bắt đầu trong tài liệu.

Chọn một đoạn lớn hơn

Hãy thực hiện một lựa chọn lớn hơn trong ví dụ của chúng ta, như thế này:



Chúng ta đã biết cách làm điều đó. Chúng ta chỉ cần đặt điểm bắt đầu và điểm kết thúc dưới dạng offset tương đối trong các nút văn bản.
Chúng ta cần tạo một phạm vi, mà:

  • bắt đầu từ vị trí 2 trong nút con đầu tiên của <p>(lấy tất cả trừ hai chữ cái đầu tiên của “Example: “)
  • kết thúc tại vị trí 3 trong nút con đầu tiên của <b> (lấy ba chữ cái đầu tiên của “bold”, nhưng không nhiều hơn):
<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();

  range.setStart(p.firstChild, 2);
  range.setEnd(p.querySelector('b').firstChild, 3);

  console.log(range); // ample: italic and bol

  // use this range for selection (explained later)
  window.getSelection().addRange(range);
</script>

Như bạn có thể thấy, việc tạo ra một phạm vi theo ý muốn là khá dễ dàng.
Nếu chúng ta muốn lấy các nút như một khối, chúng ta có thể truyền các phần tử vào setStart/setEnd. Ngược lại, chúng ta có thể làm việc ở mức văn bản.

2. Các thuộc tính của Range

Đối tượng phạm vi mà chúng ta tạo ra trong ví dụ trên có các thuộc tính sau:



startContainer, startOffset — nút và offset của điểm bắt đầu,
– trong ví dụ trên: nút văn bản đầu tiên bên trong <p>2.
endContainer, endOffset — nút và offset của điểm kết thúc,
– trong ví dụ trên: nút văn bản đầu tiên bên trong <p>3.
collapsed — kiểu boolean, true nếu phạm vi bắt đầu và kết thúc ở cùng một điểm (vì vậy không có nội dung bên trong phạm vi),
– trong ví dụ trên: false
commonAncestorContainer — tổ tiên chung gần nhất của tất cả các nút trong phạm vi,
– trong ví dụ trên: <p>`

3. Các phương thức chọn phạm vi

Có nhiều phương thức tiện lợi để thao tác với các phạm vi.
Chúng ta đã thấy setStartsetEnd, dưới đây là các phương thức tương tự khác.

Đặt điểm bắt đầu của phạm vi:

  • setStart(node, offset) đặt điểm bắt đầu tại: vị trí offset trong node
  • setStartBefore(node) đặt điểm bắt đầu tại: ngay trước node
  • setStartAfter(node) đặt điểm bắt đầu tại: ngay sau node
    Đặt điểm kết thúc của phạm vi (các phương thức tương tự):
  • setEnd(node, offset) đặt điểm kết thúc tại: vị trí offset trong node
  • setEndBefore(node) đặt điểm kết thúc tại: ngay trước node
  • setEndAfter(node) đặt điểm kết thúc tại: ngay sau node
    Về mặt kỹ thuật, setStart/setEnd có thể làm mọi thứ, nhưng nhiều phương thức hơn sẽ cung cấp sự tiện lợi hơn.
    Trong tất cả các phương thức này, node có thể là nút văn bản hoặc nút phần tử: đối với nút văn bản, offset bỏ qua số lượng ký tự đó, trong khi đối với nút phần tử là số lượng nút con đó.

Còn nhiều phương thức khác để tạo phạm vi:

  • selectNode(node) đặt phạm vi để chọn toàn bộ node
  • selectNodeContents(node) đặt phạm vi để chọn toàn bộ nội dung của node
  • collapse(toStart) nếu toStart=true đặt điểm kết thúc=điểm bắt đầu, nếu không đặt điểm bắt đầu=điểm kết thúc, do đó thu gọn phạm vi
  • cloneRange() tạo một phạm vi mới với cùng điểm bắt đầu/điểm kết thúc

4. Các phương thức chỉnh sửa phạm vi

Khi phạm vi đã được tạo, chúng ta có thể thao tác với nội dung của nó bằng các phương thức sau:
deleteContents() — loại bỏ nội dung phạm vi khỏi tài liệu
extractContents() — loại bỏ nội dung phạm vi khỏi tài liệu và trả về dưới dạng DocumentFragment
cloneContents() — sao chép nội dung phạm vi và trả về dưới dạng DocumentFragment
insertNode(node) — chèn node vào tài liệu tại đầu phạm vi
surroundContents(node) — bọc node quanh nội dung phạm vi. Để điều này hoạt động, phạm vi phải chứa cả thẻ mở và thẻ đóng cho tất cả các phần tử bên trong: không có phạm vi một phần như <i>abc..
Với các phương thức này, chúng ta có thể làm cơ bản mọi thứ với các nút đã chọn.
Đây là bàn thử nghiệm để xem chúng hoạt động:


Click buttons to run methods on the selection, "resetExample" to reset it.

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<p id="result"></p>
<script>
  let range = new Range();

  // Each demonstrated method is represented here:
  let methods = {
    deleteContents() {
      range.deleteContents()
    },
    extractContents() {
      let content = range.extractContents();
      result.innerHTML = "";
      result.append("extracted: ", content);
    },
    cloneContents() {
      let content = range.cloneContents();
      result.innerHTML = "";
      result.append("cloned: ", content);
    },
    insertNode() {
      let newNode = document.createElement('u');
      newNode.innerHTML = "NEW NODE";
      range.insertNode(newNode);
    },
    surroundContents() {
      let newNode = document.createElement('u');
      try {
        range.surroundContents(newNode);
      } catch(e) { console.log(e) }
    },
    resetExample() {
      p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`;
      result.innerHTML = "";

      range.setStart(p.firstChild, 2);
      range.setEnd(p.querySelector('b').firstChild, 3);

      window.getSelection().removeAllRanges();  
      window.getSelection().addRange(range);  
    }
  };

  for(let method in methods) {
    document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`);
  }

  methods.resetExample();
</script>

Cũng có các phương thức để so sánh các phạm vi, nhưng chúng ít được sử dụng. Khi cần, vui lòng tham khảo spec hoặc hướng dẫn MDN.

5. Selection

Range là một đối tượng chung để quản lý các phạm vi lựa chọn. Mặc dù việc tạo ra một Range không có nghĩa là chúng ta thấy sự lựa chọn trên màn hình.
Chúng ta có thể tạo các đối tượng Range, truyền chúng xung quanh — chúng không tự động chọn bất kỳ thứ gì trên màn hình.

Sự lựa chọn trong tài liệu được đại diện bởi đối tượng Selection, có thể được lấy qua window.getSelection() hoặc document.getSelection(). Một sự lựa chọn có thể bao gồm không có hoặc nhiều phạm vi. Ít nhất, Tài liệu API Selection nói như vậy. Trong thực tế, chỉ có Firefox cho phép chọn nhiều phạm vi trong tài liệu bằng cách sử dụng key:Ctrl+click (key:Cmd+click cho Mac).

Đây là ảnh chụp màn hình của một sự lựa chọn với 3 phạm vi, thực hiện trên Firefox:


Các trình duyệt khác hỗ trợ tối đa 1 phạm vi. Như chúng ta sẽ thấy, một số phương thức của Selection giả định rằng có thể có nhiều phạm vi, nhưng một lần nữa, trong tất cả các trình duyệt ngoài Firefox, chỉ có tối đa 1 phạm vi.
Đây là bản demo nhỏ cho thấy sự lựa chọn hiện tại (chọn một cái gì đó và nhấp chuột) dưới dạng văn bản:

6. Các thuộc tính của Selection

Như đã nói, một sự lựa chọn lý thuyết có thể chứa nhiều phạm vi. Chúng ta có thể lấy các đối tượng phạm vi này bằng phương thức:
getRangeAt(i) — lấy phạm vi thứ i, bắt đầu từ 0. Trong tất cả các trình duyệt ngoại trừ Firefox, chỉ 0 được sử dụng.
Ngoài ra, còn có các thuộc tính thường cung cấp sự tiện lợi tốt hơn.
Tương tự như một phạm vi, một đối tượng lựa chọn có điểm bắt đầu, gọi là “anchor”, và điểm kết thúc, gọi là “focus”.

Các thuộc tính chính của sự lựa chọn là:

  • anchorNode — nút nơi sự lựa chọn bắt đầu,
  • anchorOffset — offset trong anchorNode nơi sự lựa chọn bắt đầu,
  • focusNode — nút nơi sự lựa chọn kết thúc,
  • focusOffset — offset trong focusNode nơi sự lựa chọn kết thúc,
  • isCollapsedtrue nếu sự lựa chọn không chọn gì (phạm vi rỗng), hoặc không tồn tại.
  • rangeCount — số lượng phạm vi trong sự lựa chọn, tối đa 1 trong tất cả các trình duyệt ngoại trừ Firefox.
    Có một sự khác biệt quan trọng giữa điểm bắt đầu/kết thúc của sự lựa chọn so với điểm bắt đầu/kết thúc của Range.
    Như chúng ta đã biết, các đối tượng Range luôn có điểm bắt đầu trước điểm kết thúc.

Đối với sự lựa chọn, điều đó không phải lúc nào cũng đúng.

Việc chọn một cái gì đó bằng chuột có thể được thực hiện theo cả hai hướng: từ “trái sang phải” hoặc “phải sang trái”.

Nói cách khác, khi nút chuột được nhấn và sau đó di chuyển về phía trước trong tài liệu, thì điểm kết thúc (focus) của nó sẽ ở sau điểm bắt đầu (anchor).

Ví dụ, nếu người dùng bắt đầu chọn bằng chuột và đi từ “Example” đến “italic”:


…Nhưng sự lựa chọn tương tự có thể được thực hiện ngược lại: bắt đầu từ “italic” đến “Example” (hướng ngược), thì điểm kết thúc (focus) của nó sẽ ở trước điểm bắt đầu (anchor):

7. Các sự kiện chọn

Có các sự kiện để theo dõi sự lựa chọn:
elem.onselectstart — khi sự lựa chọn bắt đầu cụ thể trên phần tử elem (hoặc bên trong nó). Ví dụ, khi người dùng nhấn nút chuột trên đó và bắt đầu di chuyển con trỏ.
– Việc ngăn chặn hành động mặc định hủy bỏ sự bắt đầu lựa chọn. Do đó, việc bắt đầu một sự lựa chọn từ phần tử này trở nên không thể, nhưng phần tử vẫn có thể được chọn. Người dùng chỉ cần bắt đầu sự lựa chọn từ nơi khác.
document.onselectionchange — bất cứ khi nào sự lựa chọn thay đổi hoặc bắt đầu.
– Xin lưu ý: trình xử lý này chỉ có thể được đặt trên document, nó theo dõi tất cả các sự lựa chọn trong đó.

Demo theo dõi sự lựa chọn

Đây là bản demo nhỏ. Nó theo dõi sự lựa chọn hiện tại trên document và hiển thị các ranh giới của nó:


<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

From <input id="from" disabled> – To <input id="to" disabled>
<script>
  document.onselectionchange = function() {
    let selection = document.getSelection();

    let {anchorNode, anchorOffset, focusNode, focusOffset} = selection;

    // anchorNode and focusNode are text nodes usually
    from.value = `${anchorNode?.data}, offset ${anchorOffset}`;
    to.value = `${focusNode?.data}, offset ${focusOffset}`;
  };
</script>

Demo sao chép sự lựa chọn

Có hai cách để sao chép nội dung đã chọn:
1. Chúng ta có thể sử dụng document.getSelection().toString() để lấy nội dung dưới dạng văn bản.
2. Hoặc, để sao chép toàn bộ DOM, ví dụ nếu chúng ta cần giữ định dạng, chúng ta có thể lấy các phạm vi cơ bản bằng getRangeAt(...). Một đối tượng Range, lần lượt, có phương thức cloneContents() sao chép nội dung của nó và trả về dưới dạng đối tượng DocumentFragment, mà chúng ta có thể chèn vào nơi khác.

Đây là bản demo sao chép nội dung đã chọn cả dưới dạng văn bản và dưới dạng các nút DOM:


<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>

<script>
  document.onselectionchange = function() {
    let selection = document.getSelection();

    cloned.innerHTML = astext.innerHTML = "";

    // Clone DOM nodes from ranges (we support multiselect here)
    for (let i = 0; i < selection.rangeCount; i++) {
      cloned.append(selection.getRangeAt(i).cloneContents());
    }

    // Get as text
    astext.innerHTML += selection;
  };
</script>

8. Các phương thức của Selection

Chúng ta có thể làm việc với sự lựa chọn bằng cách thêm/xóa các phạm vi:
getRangeAt(i) — lấy phạm vi thứ i, bắt đầu từ 0. Trong tất cả các trình duyệt ngoại trừ Firefox, chỉ 0 được sử dụng.
addRange(range) — thêm range vào sự lựa chọn. Tất cả các trình duyệt ngoại trừ Firefox đều bỏ qua lệnh gọi nếu sự lựa chọn đã có một phạm vi liên kết.
removeRange(range) — xóa range khỏi sự lựa chọn.
removeAllRanges() — xóa tất cả các phạm vi.
empty() — tên gọi khác của removeAllRanges.
Cũng có các phương thức tiện lợi để thao tác trực tiếp với phạm vi sự lựa chọn, mà không cần các lệnh gọi Range trung gian:
collapse(node, offset) — thay thế phạm vi đã chọn bằng một phạm vi mới bắt đầu và kết thúc tại node đã cho, ở vị trí offset.
setPosition(node, offset) — tên gọi khác của collapse.
collapseToStart() – thu gọn (thay thế bằng một phạm vi rỗng) về điểm bắt đầu của sự lựa chọn,
collapseToEnd() – thu gọn về điểm kết thúc của sự lựa chọn,
extend(node, offset) – di chuyển điểm focus của sự lựa chọn đến node đã cho, vị trí offset,
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset) – thay thế phạm vi sự lựa chọn bằng điểm bắt đầu anchorNode/anchorOffset và điểm kết thúc focusNode/focusOffset. Tất cả nội dung ở giữa chúng được chọn.
selectAllChildren(node) — chọn tất cả các nút con của node.
deleteFromDocument() — xóa nội dung đã chọn khỏi tài liệu.
containsNode(node, allowPartialContainment = false) — kiểm tra xem sự lựa chọn có chứa node không (một phần nếu đối số thứ hai là true).
Đối với hầu hết các tác vụ, các phương thức này là đủ, không cần phải truy cập vào đối tượng Range cơ bản.
Ví dụ, chọn toàn bộ nội dung của đoạn văn <p>:


<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

<script>
  // select from 0th child of <p> to the last child
  document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>

Làm điều tương tự bằng cách sử dụng phạm vi:


<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();
  range.selectNodeContents(p); // or selectNode(p) to select the <p> tag too

  document.getSelection().removeAllRanges(); // clear existing selection if any
  document.getSelection().addRange(range);
</script>

Nếu sự lựa chọn tài liệu đã tồn tại, hãy xóa nó trước bằng removeAllRanges(). Sau đó thêm các phạm vi mới. Nếu không, tất cả các trình duyệt ngoại trừ Firefox đều bỏ qua các phạm vi mới.
Sự ngoại lệ là một số phương thức sự lựa chọn, thay thế sự lựa chọn hiện có, chẳng hạn như setBaseAndExtent.

9. Sự lựa chọn trong các phần tử của biểu mẫu

Các phần tử biểu mẫu, chẳng hạn như inputtextarea, cung cấp API đặc biệt cho sự lựa chọn, không cần các đối tượng Selection hoặc Range. Vì giá trị đầu vào là văn bản thuần túy, không phải HTML, không cần các đối tượng như vậy, mọi thứ đơn giản hơn nhiều.
Các thuộc tính:

  • input.selectionStart — vị trí bắt đầu của sự lựa chọn (có thể ghi),
  • input.selectionEnd — vị trí kết thúc của sự lựa chọn (có thể ghi),
  • input.selectionDirection — hướng của sự lựa chọn, một trong các giá trị: “forward”, “backward” hoặc “none” (nếu ví dụ, được chọn bằng cách nhấp chuột đúp),
    Các sự kiện:
  • input.onselect — kích hoạt khi có gì đó được chọn.
    Các phương thức:
  • input.select() — chọn tất cả nội dung trong điều khiển văn bản (có thể là textarea thay vì input),
  • input.setSelectionRange(start, end, [direction]) — thay đổi sự lựa chọn để trải dài từ vị trí start đến end, theo hướng đã cho (tùy chọn).
  • input.setRangeText(replacement, [start], [end], [selectionMode]) — thay thế một phạm vi văn bản bằng văn bản mới.

    Các đối số tùy chọn startend, nếu được cung cấp, thiết lập điểm bắt đầu và kết thúc của phạm vi, nếu không, sự lựa chọn của người dùng sẽ được sử dụng.
    Đối số cuối cùng, selectionMode, xác định cách sự lựa chọn sẽ được thiết lập sau khi văn bản đã được thay thế. Các giá trị có thể có là:

  • "select" — đoạn văn bản mới chèn sẽ được chọn. – "start" — vùng lựa chọn sẽ thu gọn ngay trước văn bản mới chèn (con trỏ sẽ nằm ngay trước nó). – "end" — vùng lựa chọn sẽ thu gọn ngay sau văn bản mới chèn (con trỏ sẽ nằm ngay sau nó). – "preserve" — cố gắng giữ nguyên lựa chọn. Đây là giá trị mặc định.

    Bây giờ hãy cùng xem các phương thức này hoạt động như thế nào.

Ví dụ: theo dõi lựa chọn

Ví dụ, đoạn mã này sử dụng sự kiện onselect để theo dõi lựa chọn:


<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>

<script>
  area.onselect = function() {
    from.value = area.selectionStart;
    to.value = area.selectionEnd;
  };
</script>

Xin lưu ý:
onselect kích hoạt khi có gì đó được chọn, nhưng không kích hoạt khi lựa chọn bị xóa.
– Sự kiện document.onselectionchange không nên kích hoạt cho các lựa chọn bên trong một điều khiển biểu mẫu, theo spec, vì nó không liên quan đến việc lựa chọn và phạm vi của document. Một số trình duyệt có thể phát sinh sự kiện này, nhưng chúng ta không nên dựa vào nó.

Ví dụ: di chuyển con trỏ

Chúng ta có thể thay đổi selectionStartselectionEnd, điều này sẽ thiết lập lựa chọn.
Một trường hợp đặc biệt quan trọng là khi selectionStartselectionEnd bằng nhau. Khi đó, đó chính xác là vị trí của con trỏ. Hoặc, để diễn đạt lại, khi không có gì được chọn, lựa chọn sẽ thu gọn tại vị trí của con trỏ.

Vì vậy, bằng cách đặt selectionStartselectionEnd thành cùng một giá trị, chúng ta di chuyển con trỏ.

Ví dụ:


<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>

<script>
  area.onfocus = () => {
    // zero delay setTimeout to run after browser "focus" action finishes
    setTimeout(() => {
      // we can set any selection
      // if start=end, the cursor is exactly at that place
      area.selectionStart = area.selectionEnd = 10;
    });
  };
</script>

Ví dụ: sửa đổi lựa chọn

Để sửa đổi nội dung của lựa chọn, chúng ta có thể sử dụng phương thức input.setRangeText(). Tất nhiên, chúng ta có thể đọc selectionStart/End và, với kiến thức về lựa chọn, thay đổi chuỗi con tương ứng của value, nhưng setRangeText mạnh mẽ hơn và thường tiện lợi hơn.
Đây là một phương thức khá phức tạp. Ở dạng đơn giản nhất với một đối số, nó sẽ thay thế vùng lựa chọn của người dùng và loại bỏ lựa chọn.

Ví dụ, ở đây lựa chọn của người dùng sẽ được bao quanh bởi *...*:

run autorun
<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>

<script>
button.onclick = () => {
  if (input.selectionStart == input.selectionEnd) {
    return; // nothing is selected
  }

  let selected = input.value.slice(input.selectionStart, input.selectionEnd);
  input.setRangeText(`*${selected}*`);
};
</script>


Với nhiều tham số hơn, chúng ta có thể thiết lập phạm vi startend.
Trong ví dụ này, chúng ta tìm "THIS" trong văn bản đầu vào, thay thế nó và giữ phần thay thế được chọn:


<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>

<script>
button.onclick = () => {
  let pos = input.value.indexOf("THIS");
  if (pos >= 0) {
    input.setRangeText("*THIS*", pos, pos + 4, "select");
    input.focus(); // focus to make selection visible
  }
};
</script>

Ví dụ: chèn tại vị trí con trỏ

Nếu không có gì được chọn, hoặc chúng ta sử dụng startend bằng nhau trong setRangeText, thì văn bản mới sẽ chỉ được chèn vào, không có gì bị xóa.
Chúng ta cũng có thể chèn một cái gì đó “tại vị trí con trỏ” bằng cách sử dụng setRangeText.

Dưới đây là một nút chèn "HELLO" tại vị trí con trỏ và đặt con trỏ ngay sau nó. Nếu vùng lựa chọn không trống, nó sẽ bị thay thế (chúng ta có thể phát hiện điều này bằng cách so sánh selectionStart!=selectionEnd và thực hiện một hành động khác thay thế):


<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>

<script>
  button.onclick = () => {
    input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
    input.focus();
  };    
</script>

10. Làm cho không thể chọn

Để làm cho một phần tử không thể chọn, có ba cách:
1. Sử dụng thuộc tính CSS user-select: none.


    <style>
    #elem {
      user-select: none;
    }
    </style>
    <div>Selectable <div id="elem">Unselectable</div> Selectable</div>

Điều này không cho phép bắt đầu lựa chọn tại elem. Nhưng người dùng có thể bắt đầu lựa chọn ở nơi khác và bao gồm elem vào đó.
Khi đó, elem sẽ trở thành một phần của document.getSelection(), vì vậy lựa chọn thực tế vẫn xảy ra, nhưng nội dung của nó thường bị bỏ qua khi sao chép-dán.

2.Ngăn chặn hành động mặc định trong các sự kiện onselectstart hoặc mousedown.


    <div>Selectable <div id="elem">Unselectable</div> Selectable</div>

    <script>
      elem.onselectstart = () => false;
    </script>

Điều này ngăn chặn việc bắt đầu lựa chọn trên elem, nhưng người dùng có thể bắt đầu nó tại một phần tử khác, sau đó mở rộng đến elem.
Điều này tiện lợi khi có một bộ xử lý sự kiện khác trên cùng một hành động kích hoạt lựa chọn (ví dụ: mousedown). Vì vậy, chúng ta vô hiệu hóa lựa chọn để tránh xung đột, nhưng vẫn cho phép nội dung của elem được sao chép.

3.Chúng ta cũng có thể xóa lựa chọn sau khi nó xảy ra bằng document.getSelection().empty(). Cách này ít khi được sử dụng vì nó gây ra hiện tượng nhấp nháy không mong muốn khi lựa chọn xuất hiện rồi biến mất.

11. Tài liệu tham khảo

12. Tóm tắt

Chúng ta đã xem xét hai API khác nhau cho việc chọn lựa:
1. Đối với tài liệu: các đối tượng SelectionRange.
2. Đối với input, textarea: các phương pháp và thuộc tính bổ sung.

API thứ hai rất đơn giản, vì nó làm việc với văn bản.

Các công thức thường dùng nhất có thể là:

Dưới đây là đoạn mở bài cho chủ đề:

Lấy lựa chọn:

let selection = document.getSelection();

let cloned = /* element to clone the selected nodes to */;

// then apply Range methods to selection.getRangeAt(0)
// or, like here, to all ranges to support multi-select
for (let i = 0; i < selection.rangeCount; i++) {
  cloned.append(selection.getRangeAt(i).cloneContents());
}

Đặt lựa chọn:

let selection = document.getSelection();

// directly:
selection.setBaseAndExtent(...from...to...);

// or we can create a range and:
selection.removeAllRanges();
selection.addRange(range);

Cuối cùng, về con trỏ. Vị trí của con trỏ trong các phần tử có thể chỉnh sửa, như , luôn nằm ở đầu hoặc cuối của lựa chọn. Chúng ta có thể sử dụng nó để lấy vị trí con trỏ hoặc để di chuyển con trỏ bằng cách thiết lập elem.selectionStartelem.selectionEnd.

Chào mừng bạn đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá hai đối tượng quan trọng trong JavaScript là `Selection` và `Range`, từ những kiến thức cơ bản đến nâng cao. Những công cụ này cho phép bạn quản lý và thao tác với các vùng chọn trong tài liệu web một cách hiệu quả. Hãy cùng Cafedev tìm hiểu cách sử dụng chúng để nâng cao kỹ năng lập trình JavaScript của bạn!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình dev

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!