Untuk membungkus rentang teks sambil mempertahankan elemen inline yang sudah ada, bungkus setiap fragmen teks yang dipilih dalam span tersendiri. Panduan Safari di Mac ini menggunakan paragraf sintetis agar elemen penekanan, urutan teks, dan referensi elemen tetap utuh.
Mengapa satu span yang membungkus seluruhnya bisa gagal
Rentang browser dapat dimulai di dalam satu elemen inline dan berakhir di luarnya. Misalnya, pemilihan dapat dimulai di tengah teks yang diberi penekanan, melintasi elemen bersarang, lalu berakhir di teks penutup paragraf. Pemilihan seperti itu belum tentu sesuai dengan satu subtree yang utuh.
Range.surroundContents() tidak dapat menangani semua pemilihan seperti itu. Metode ini menghasilkan error saat rentang mencakup sebagian node nonteks. DOM Standard saat ini menetapkan InvalidStateError untuk kondisi tersebut. Rentang yang berisi teks dan elemen yang dipilih secara utuh dapat menggunakan metode ini, tetapi pemilihan yang melintasi batas elemen memerlukan pendekatan lain. DOM Standard, Range: metode surroundContents()
Mengekstrak rentang ke dalam satu span baru adalah transformasi lain yang mungkin, tetapi aturan pelestariannya berbeda. extractContents() mengkloning elemen leluhur yang hanya dipilih sebagian. Jika tugas Anda mengharuskan elemen inline asli tetap utuh dan mempertahankan identitasnya, cabang hasil kloning tersebut tidak sesuai. Range: metode extractContents()
Gunakan panduan keputusan ini sebelum mengubah dokumen:
| Hasil yang diperlukan | Pendekatan yang sesuai |
|---|---|
| Satu pembungkus untuk teks dan elemen yang dipilih secara utuh | Pertimbangkan surroundContents() |
| Elemen inline asli tetap utuh saat pemilihan hanya mencakup sebagian | Bungkus fragmen teks yang dipilih secara terpisah |
| Satu pembungkus melintasi batas elemen yang hanya dipilih sebagian | Terima perubahan struktur dan tinjau perilaku ekstraksi/kloning |
| Penekanan visual tanpa menambahkan elemen | Pertimbangkan kembali apakah tugas ini memang memerlukan span |
Contoh di bawah memilih pembungkus terpisah. Mempertahankan struktur di sini berarti menjaga objek elemen yang ada beserta hubungan leluhurnya, sambil menambahkan span di dalamnya. Node teks akan terpisah di titik awal atau akhir pemilihan.
Buka contoh sintetis di Safari pada Mac
Buka tab Safari baru dan masukkan about:blank. Lalu pilih Safari → Settings… → Advanced dan aktifkan Show features for web developers. Buka Develop → Show Web Inspector, atau tekan Option–Command–I. Langkah-langkah ini didokumentasikan oleh WebKit. Mengaktifkan Web Inspector
Pilih tab Console. Jalankan seluruh program JavaScript di bawah ini pada prompt-nya. WebKit mendokumentasikan bahwa prompt konsol menjalankan JavaScript dalam konteks eksekusi halaman yang sedang diperiksa. Gunakan konteks halaman tingkat atas untuk contoh ini. Antarmuka Web Inspector
Tidak diperlukan pustaka, impor, langkah build, atau konten dokumen yang sudah ada. Program ini membuat paragrafnya sendiri, membungkus rentang yang telah ditentukan, memulihkan pemilihan maju atas teks yang dibungkus, dan menambahkan tombol penghapusan. Masukan yang didukung sengaja dibatasi: satu paragraf yang berisi teks dan turunan em, strong, span, atau code biasa, dengan kedua batas rentang berada di dalam node teks.
(() => {
function wrapTextRange(root, range) {
const doc = root.ownerDocument;
const startNode = range.startContainer;
const endNode = range.endContainer;
if (root.tagName !== "P" ||
startNode.nodeType !== 3 || endNode.nodeType !== 3 ||
!root.contains(startNode) || !root.contains(endNode)) {
throw new TypeError("Use one paragraph and text-node boundaries.");
}
const allowed = new Set(["EM", "STRONG", "SPAN", "CODE"]);
for (const element of root.querySelectorAll("*")) {
if (!allowed.has(element.tagName) ||
element.hasAttribute("data-range-wrap")) {
throw new TypeError("Unsupported element or existing wrappers.");
}
}
if (range.collapsed) return [];
// Snapshot every fragment before changing the live DOM range.
const jobs = [];
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
if (!range.intersectsNode(node)) continue;
const start = node === startNode ? range.startOffset : 0;
const end = node === endNode ? range.endOffset : node.length;
if (end > start) jobs.push({ node, start, end });
}
const spans = [];
for (const { node, start, end } of jobs) {
// Split the end first so the start offset remains valid.
if (end < node.length) node.splitText(end);
const selected = start > 0 ? node.splitText(start) : node;
const span = doc.createElement("span");
span.setAttribute("data-range-wrap", "");
span.style.backgroundColor = "#fff0a6";
selected.parentNode.insertBefore(span, selected);
span.appendChild(selected);
spans.push(span);
}
return spans;
}
const panel = document.createElement("section");
const root = document.createElement("p");
const strong = document.createElement("strong");
const em = document.createElement("em");
const tail = document.createTextNode(" gamma delta.");
strong.append(document.createTextNode("bright "));
em.textContent = "blue";
strong.append(em);
root.append(document.createTextNode("Alpha "), strong, tail);
panel.append(root);
document.body.append(panel);
let clicks = 0;
strong.addEventListener("click", () => { clicks += 1; });
const originalText = root.textContent;
const range = document.createRange();
range.setStart(strong.firstChild, 3);
range.setEnd(tail, 6);
const selectedText = range.toString();
const spans = wrapTextRange(root, range);
const selection = window.getSelection();
const restored = document.createRange();
restored.setStart(spans[0].firstChild, 0);
const lastText = spans[spans.length - 1].firstChild;
restored.setEnd(lastText, lastText.length);
selection.removeAllRanges();
selection.addRange(restored);
strong.dispatchEvent(new Event("click"));
console.assert(root.textContent === originalText, "Text changed");
console.assert(strong.parentNode === root, "Strong element moved");
console.assert(em.parentNode === strong, "Emphasis element moved");
console.assert(clicks === 1, "Existing listener failed");
console.assert(spans.map(s => s.textContent).join("") === selectedText);
console.log(spans.map(s => s.textContent));
const remove = document.createElement("button");
remove.textContent = "Remove spans";
remove.addEventListener("click", () => {
selection.removeAllRanges();
for (const span of spans) {
span.replaceWith(...Array.from(span.childNodes));
}
remove.disabled = true;
});
panel.append(remove);
})();
Pahami teks yang akan dibungkus
Paragrafnya berbunyi Alpha bright blue gamma delta. Elemen strong yang sudah ada berisi bright dan elemen em bersarang yang berisi blue. Rentang dimulai pada offset 3 dalam bright dan berakhir pada offset 6 dalam gamma delta. Ini adalah masukan ilustratif yang ditentukan oleh program.
Keluaran konsol yang diharapkan berisi tiga fragmen:
["ght ", "blue", " gamma"]
Span pertama menjadi anak dari elemen strong asli. Span kedua menjadi anak dari elemen em asli. Span ketiga menjadi anak paragraf. Membaca teksnya sesuai urutan dokumen menghasilkan kembali string yang dipilih, ght blue gamma. Awalan yang tidak dipilih bri dan akhiran delta. tetap berada di luar pembungkus.
Penelusuran menggunakan createTreeWalker() dengan NodeFilter.SHOW_TEXT untuk mengunjungi node teks turunan. intersectsNode() mengidentifikasi kandidat, dan offset batas memangkas kandidat pertama dan terakhir. Fragmen sepanjang nol dikecualikan. Document: metode createTreeWalker(), Range: metode intersectsNode()
Program mencatat semua batas fragmen sebelum mengubah apa pun. Ini penting karena rentang DOM bersifat live: pemisahan, penyisipan, atau penghapusan node dapat memperbarui titik batasnya. Range yang dikloning juga akan bersifat live; menyimpan referensi node dan offset numerik menyediakan daftar kerja stabil yang digunakan di sini. DOM Standard: Rentang
Untuk setiap fragmen, program memisahkan batas akhir terlebih dahulu, lalu batas awal. splitText() mempertahankan awalan di node teks asli dan mengembalikan sisa teks sebagai saudara. Ini mengisolasi substring yang dipilih tanpa membuat ulang elemen yang membungkusnya. Text: metode splitText()
Empat kasus perencanaan fragmen—teks yang melintasi elemen, substring dalam satu node, rentang yang diciutkan, dan batas yang tidak memilih karakter apa pun—diperiksa secara deterministik di Node.js. Mutasi DOM browser dan interaksi Safari tidak dijalankan selama persiapan; program ini menyertakan assertion untuk memeriksa hasil tersebut saat dijalankan.
Pertahankan struktur dan kontrak siklus hidup yang tepat
Elemen strong dan em yang sudah ada tidak pernah dihapus, diganti, atau dikloning oleh program ini. Referensi elemennya tetap dapat digunakan, dan listener yang terpasang pada strong tetap terpasang pada objek yang sama. Contoh ini memeriksa hubungan induk dan pemanggilan dispatch peristiwa sintetis setelah pembungkusan.
Identitas node teks memiliki kontrak yang berbeda. Pemisahan batas menghasilkan node teks baru, dan setiap fragmen teks yang dipilih berpindah ke span baru. Aplikasi apa pun yang menyimpan referensi node teks harus memperhitungkan perubahan ini. Metode ini mempertahankan konten teks dan hierarki elemen asli; metode ini tidak mempertahankan setiap batas node teks asli.
Menambahkan span juga mengubah hubungan induk langsung untuk teks. Sebelum menerapkan transformasi ini pada dokumen yang sudah ada, periksa kode yang mengharapkan anak pertama suatu elemen berupa node teks, pemilih CSS yang bergantung pada anak langsung, dan penangan peristiwa yang memeriksa target peristiwa. Mempertahankan objek elemen saja tidak menjamin semua asumsi aplikasi di sekitarnya tetap berlaku.
Contoh ini secara eksplisit membuat ulang pemilihan maju setelah pembungkusan. Contoh ini tidak mempertahankan arah pemilihan mundur atau menyediakan transaksi undo editor. Perlakukan keduanya sebagai persyaratan terpisah saat mengintegrasikan operasi ini ke antarmuka penyuntingan. Fungsi pembungkus itu sendiri mengembalikan span tanpa menjanjikan keadaan pemilihan browser tertentu.
Mengklik Remove spans membuka kembali hanya span yang dibuat selama eksekusi tersebut dan menghapus pemilihan. Node teks yang terpisah tetap terpisah. Tindakan penghapusan itu memulihkan hierarki elemen dan teks asli, tetapi tidak membalikkan identitas node teks secara persis. Hindari menambahkan normalisasi otomatis jika kode lain bergantung pada referensi tersebut.
Sesuaikan contoh tanpa diam-diam memperluas cakupannya
Untuk pemilihan yang diseret pengguna, ambil rentangnya hanya setelah memeriksa bahwa window.getSelection() mengembalikan pemilihan dan rangeCount bukan nol. getRangeAt(0) mengembalikan rentang pertama; meminta indeks di luar jumlah rentang pemilihan akan menghasilkan error. Selection: metode getRangeAt()
Berikan rentang itu ke helper yang sama hanya jika kedua kontainer batas adalah node teks di dalam paragraf yang dimaksud. Pemilihan dengan kontainer batas berupa elemen akan ditolak sebelum mutasi. Dukungan untuk batas seperti itu memerlukan konversi tambahan yang didefinisikan secara eksplisit ke posisi teks turunan; sekadar menghapus pemeriksaan tersebut tidak menerapkannya.
Helper ini juga menolak turunan yang tidak didukung dan pembungkus bertanda yang sudah ada sebelum membuat perubahan. Untuk anotasi berulang atau tumpang tindih, tentukan terlebih dahulu apakah span baru harus bersarang, digabungkan, atau menggantikan yang sebelumnya. Contoh ini sengaja menampilkan keputusan tersebut alih-alih terus menambahkan pembungkus tanpa kebijakan.
Offset diukur dalam unit kode UTF-16. Untuk posisi terhitung dalam teks yang berisi karakter multi-unit, pastikan batas tidak membagi urutan karakter yang dimaksud. Contoh sintetis ini menggunakan teks Latin sederhana, sehingga offset numeriknya tidak ambigu. Text: metode splitText()
Untuk tugas ini, invarian yang berguna cukup sederhana: penggabungan teks span baru harus sama dengan string asli yang dipilih, teks lengkap paragraf harus tetap sama, dan elemen inline yang sudah ada harus mempertahankan identitas serta hubungan leluhurnya. Periksa ketiganya saat menyesuaikan contoh; bersama-sama, ketiganya menjelaskan arti keberhasilan mempertahankan struktur.
Sumber
- DOM Standard — WHATWG.
- Mengaktifkan Web Inspector — WebKit.
- Antarmuka Web Inspector — WebKit.
- Range: metode surroundContents() — MDN Web Docs.
- Range: metode extractContents() — MDN Web Docs.
- Document: metode createTreeWalker() — MDN Web Docs.
- Range: metode intersectsNode() — MDN Web Docs.
- Text: metode splitText() — MDN Web Docs.
- Selection: metode getRangeAt() — MDN Web Docs.