Untuk membalut julat teks sambil mengekalkan elemen sebaris sedia ada, balut setiap fragmen teks yang dipilih dalam span tersendiri. Panduan Safari pada Mac ini menggunakan perenggan sintetik untuk mengekalkan elemen penegasan, susunan teks dan rujukan elemen.
Mengapa satu span yang mengelilingi teks boleh gagal
Julat pelayar boleh bermula di dalam satu elemen sebaris dan berakhir di luarnya. Contohnya, pilihan mungkin bermula di tengah-tengah teks yang ditegaskan, merentasi elemen bersarang dan berakhir dalam teks hujung perenggan. Pilihan itu tidak semestinya bersamaan dengan satu subpepohon yang lengkap.
Range.surroundContents() tidak dapat mengendalikan semua pilihan sebegini. Kaedah itu akan mencetuskan ralat apabila julat mengandungi sebahagian nod bukan teks. DOM Standard semasa menetapkan InvalidStateError untuk keadaan tersebut. Julat yang mengandungi teks dan elemen yang dipilih sepenuhnya boleh menggunakan kaedah ini, tetapi melintasi sempadan elemen memerlukan pendekatan yang berbeza. DOM Standard, Range: kaedah surroundContents()
Mengekstrak julat ke dalam satu span baharu ialah satu lagi transformasi yang mungkin, tetapi peraturan pengekalan strukturnya berbeza. extractContents() mengklon elemen leluhur yang dipilih sebahagiannya. Jika tugas anda memerlukan elemen sebaris asal kekal utuh dan mengekalkan identitinya, cabang yang diklon itu tidak sesuai. Range: kaedah extractContents()
Gunakan panduan keputusan ini sebelum mengubah dokumen:
| Hasil yang diperlukan | Pendekatan yang sesuai |
|---|---|
| Satu pembalut di sekeliling teks dan elemen yang dipilih sepenuhnya | Pertimbangkan surroundContents() |
| Elemen sebaris asal kekal utuh merentasi pilihan separa | Balut fragmen teks yang dipilih secara berasingan |
| Satu pembalut merentasi sempadan elemen yang dipilih sebahagiannya | Terima penstrukturan semula dan semak tingkah laku pengekstrakan/pengklonan |
| Penegasan visual tanpa menambah elemen | Pertimbangkan semula sama ada tugas ini memerlukan span sama sekali |
Contoh di bawah memilih pembalut berasingan. Mengekalkan struktur di sini bermaksud mengekalkan objek elemen sedia ada dan hubungan leluhurnya sambil menambah span di bawah elemen tersebut. Nod teks akan dipecahkan pada titik mula atau tamat pilihan.
Buka contoh sintetik dalam Safari pada Mac
Buka tab Safari baharu dan masukkan about:blank. Kemudian pilih Safari → Settings… → Advanced dan dayakan Show features for web developers. Buka Develop → Show Web Inspector, atau tekan Option–Command–I. Inilah langkah yang didokumenkan oleh WebKit. Mendayakan Web Inspector
Pilih tab Console. Jalankan keseluruhan program JavaScript di bawah dalam promnya. WebKit mendokumenkan bahawa prompt konsol menilai JavaScript dalam konteks pelaksanaan terpilih bagi halaman yang diperiksa. Gunakan konteks halaman peringkat atas untuk contoh ini. Antara Muka Web Inspector
Tiada pustaka, import, langkah binaan atau kandungan dokumen sedia ada diperlukan. Program ini mencipta perenggannya sendiri, membalut julat yang telah ditentukan, memulihkan pilihan ke hadapan merentasi teks yang dibalut dan menambah butang untuk membuangnya. Input yang disokong sengaja dihadkan: satu perenggan yang mengandungi teks serta keturunan em, strong, span atau code biasa, dengan kedua-dua sempadan julat berada di dalam nod 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);
})();
Fahami teks yang akan dibalut
Perenggan itu berbunyi Alpha bright blue gamma delta. Elemen strong sedia adanya mengandungi bright dan elemen em bersarang yang mengandungi blue. Julat bermula pada ofset 3 dalam bright dan berakhir pada ofset 6 dalam gamma delta. Ini ialah input ilustrasi yang ditakrifkan oleh program.
Output konsol yang dijangka mengandungi tiga fragmen:
[\"ght \", \"blue\", \" gamma\"]
Span pertama menjadi anak kepada elemen strong asal. Span kedua menjadi anak kepada elemen em asal. Span ketiga menjadi anak kepada perenggan. Membaca teksnya mengikut urutan dokumen menghasilkan semula rentetan yang dipilih, ght blue gamma. Awalan yang tidak dipilih bri dan akhiran delta. kekal di luar pembalut.
Traversal menggunakan createTreeWalker() dengan NodeFilter.SHOW_TEXT untuk melawati nod teks keturunan. intersectsNode() mengenal pasti calon, dan ofset sempadan memangkas calon pertama dan terakhir. Fragmen kosong dikecualikan. Document: kaedah createTreeWalker(), Range: kaedah intersectsNode()
Program ini merekodkan semua sempadan fragmen sebelum membuat sebarang mutasi. Ini penting kerana julat DOM bersifat langsung: pemisahan, penyisipan atau pembuangan nod boleh mengemas kini titik sempadannya. Range yang diklon juga bersifat langsung; menyimpan rujukan nod dan ofset berangka menyediakan senarai kerja stabil yang digunakan di sini. DOM Standard: Julat
Bagi setiap fragmen, program memisahkan sempadan hujung dahulu, kemudian sempadan mula. splitText() mengekalkan awalan dalam nod teks asal dan mengembalikan baki teks sebagai adik-beradik. Ini mengasingkan subrentetan yang dipilih tanpa mencipta semula elemen yang mengelilinginya. Text: kaedah splitText()
Empat kes perancangan fragmen—teks merentasi elemen, subrentetan dalam satu nod, julat runtuh dan sempadan yang tidak memilih sebarang aksara—telah diperiksa secara deterministik dalam Node.js. Mutasi DOM pelayar dan interaksi Safari tidak dijalankan semasa penyediaan; program ini mengandungi penegasan untuk menyemak hasil tersebut apabila dijalankan.
Kekalkan struktur dan kontrak kitar hayat yang betul
Elemen strong dan em sedia ada tidak pernah dibuang, digantikan atau diklon oleh program ini. Rujukan elemennya kekal boleh digunakan, dan pendengar yang dilampirkan pada strong kekal dilampirkan pada objek yang sama. Contoh ini menyemak hubungan induk dan penghantaran peristiwa sintetik selepas pembalutan.
Identiti nod teks mempunyai kontrak yang berbeza. Pemisahan sempadan menghasilkan nod teks baharu, dan setiap fragmen teks yang dipilih dipindahkan ke dalam span baharu. Mana-mana aplikasi yang menyimpan rujukan nod teks perlu mengambil kira perubahan itu. Kaedah ini mengekalkan kandungan teks dan hierarki elemen asal; kaedah ini tidak mengekalkan setiap sempadan nod teks asal.
Menambah span juga mengubah hubungan induk serta-merta bagi teks. Sebelum menggunakan transformasi ini dalam dokumen sedia ada, semak kod yang menjangkakan anak pertama elemen ialah nod teks, pemilih CSS yang bergantung pada anak langsung dan pengendali peristiwa yang memeriksa sasaran peristiwa. Mengekalkan objek elemen sahaja tidak menjamin semua andaian aplikasi di sekelilingnya terus terpakai.
Contoh ini membina semula pilihan ke hadapan secara jelas selepas pembalutan. Contoh ini tidak mengekalkan arah pilihan ke belakang atau menyediakan transaksi buat asal editor. Anggap itu sebagai keperluan berasingan apabila menyepadukan operasi ini ke dalam antara muka penyuntingan. Fungsi pembalut itu sendiri mengembalikan span tanpa menjanjikan keadaan pilihan pelayar yang tertentu.
Mengklik Remove spans menyahbalut hanya span yang dicipta semasa larian itu dan mengosongkan pilihan. Nod teks yang telah dipecahkan kekal berasingan. Tindakan membuang itu memulihkan hierarki elemen dan teks asal, tetapi tidak mengembalikan identiti nod teks dengan tepat. Elakkan penormalan automatik jika kod lain bergantung pada rujukan tersebut.
Ubah suai contoh tanpa meluaskan skopnya secara senyap
Untuk pilihan yang dibuat pengguna dengan menyeret, dapatkan julatnya hanya selepas menyemak bahawa window.getSelection() mengembalikan pilihan dan rangeCount bukan sifar. getRangeAt(0) mengembalikan julat pertama; meminta indeks di luar bilangan julat dalam pilihan akan menghasilkan ralat. Selection: kaedah getRangeAt()
Berikan julat itu kepada pembantu yang sama hanya apabila kedua-dua bekas sempadan ialah nod teks di dalam perenggan yang dimaksudkan. Pilihan yang bekas sempadannya ialah elemen akan ditolak sebelum mutasi. Menyokong sempadan sebegini memerlukan penukaran tambahan yang ditakrifkan secara jelas kepada kedudukan teks keturunan; sekadar membuang penghad itu tidak melaksanakannya.
Pembantu itu juga menolak keturunan yang tidak disokong dan pembalut bertanda yang sedia ada sebelum membuat perubahan. Bagi anotasi yang berulang atau bertindih, tentukan dahulu sama ada span baharu patut bersarang, digabungkan atau menggantikan span terdahulu. Contoh ini sengaja mendedahkan keputusan itu supaya pembalut tidak terkumpul tanpa dasar.
Ofset diukur dalam unit kod UTF-16. Bagi kedudukan yang dikira dalam teks yang mengandungi aksara berbilang unit, pastikan sempadan tidak membahagikan jujukan aksara yang dimaksudkan. Contoh sintetik ini menggunakan teks Latin ringkas, jadi ofset berangkanya tidak samar. Text: kaedah splitText()
Bagi tugas ini, invarian yang berguna adalah mudah: teks span baharu apabila digabungkan mesti sama dengan rentetan asal yang dipilih, keseluruhan teks perenggan mesti kekal tidak berubah, dan elemen sebaris sedia ada mesti mengekalkan identiti serta hubungan leluhurnya. Semak ketiga-tiganya apabila mengubah suai contoh ini; bersama-sama, semua itu menerangkan maksud pengekalan yang berjaya.
Sumber
- DOM Standard — WHATWG.
- Mendayakan Web Inspector — WebKit.
- Antara Muka Web Inspector — WebKit.
- Range: kaedah surroundContents() — MDN Web Docs.
- Range: kaedah extractContents() — MDN Web Docs.
- Document: kaedah createTreeWalker() — MDN Web Docs.
- Range: kaedah intersectsNode() — MDN Web Docs.
- Text: kaedah splitText() — MDN Web Docs.
- Selection: kaedah getRangeAt() — MDN Web Docs.