Tambatkan anotasi ke DOM Range, lalu posisikan labelnya menggunakan persegi panjang fragmen yang dirender oleh range tersebut. Panduan ini menunjukkan demo browser desktop Mac sederhana yang menjaga catatan tetap menempel saat frasa melintasi elemen inline dan membungkus ke beberapa baris.
Tugasnya spesifik: menempelkan satu anotasi yang terlihat ke sebuah frasa dalam tata letak halaman web biasa tanpa menyusun ulang teksnya. Anda akan mengidentifikasi frasa tersebut, memilih fragmen untuk label, dan mengukur ulang setelah tata letak berubah. MDN mendokumentasikan Range.getClientRects() sebagai API browser untuk mengambil persegi panjang yang ditempati oleh sebuah range. Metode Range: getClientRects().
Mengapa label bergeser menjauh dari teks yang membungkus?
Sebuah frasa memiliki dua representasi yang relevan: posisinya dalam dokumen dan geometri tampilannya saat ini di layar. WHATWG mendefinisikan range melalui titik batas awal dan akhir, yang masing-masing terdiri atas node dan offset. Titik-titik tersebut dapat berada di node teks yang berbeda, sehingga satu range dapat melintasi elemen penekanan yang sudah ada. Standar DOM.
Satu persegi panjang pembatas menghilangkan detail. Bayangkan frasa yang dimulai di tengah baris dan berakhir dekat awal baris berikutnya. Persegi panjang yang mencakupnya akan menyertakan ruang kosong di samping kedua baris. Karena itu, label yang dipusatkan pada persegi panjang tersebut dapat terlihat terpisah dari kata-katanya. Floating UI secara khusus menjelaskan masalah ini untuk tautan multibaris dan seleksi range. inline.
Persegi panjang individual tersebut juga perlu ditafsirkan. W3C menetapkan bahwa sebuah range dapat mengembalikan persegi panjang batas elemen yang dipilih sekaligus persegi panjang teks, dalam urutan konten. Karena itu, elemen bertingkat dapat menghasilkan persegi panjang yang saling tumpang tindih; jumlah persegi panjang bukan cara yang andal untuk menghitung jumlah baris. Batas teks menggunakan metrik font, bukan garis luar glif yang benar-benar dilukis. Modul CSSOM View.
Fragmen mana yang seharusnya menjadi acuan anotasi?
Tentukan aturan penempatan sebelum menulis kode untuk memosisikan elemen. Panduan keputusan berikut membedakan teks yang dianotasi dari titik tempat labelnya muncul.
| Perilaku yang diinginkan | Geometri acuan | Aturan penempatan |
|---|---|---|
| Catatan yang mengikuti sebuah frasa | Seluruh range | Tempelkan di bawah fragmen terakhir yang dapat digunakan dalam tata letak sederhana horizontal, kiri ke kanan |
| Label di dekat interaksi penunjuk | Persegi panjang fragmen range | Pilih fragmen pada koordinat client tempat interaksi terjadi |
| Label ikhtisar yang terpusat | Persegi panjang pembatas | Terima bahwa label mungkin berada di atas ruang kosong |
| Penandaan latar untuk seluruh frasa | Seluruh range | Gunakan sorotan teks secara terpisah dari penempatan label |
Demo ini menggunakan aturan pertama. Demo sengaja menetapkan mode penulisan dan arah agar persegi panjang terakhir menjadi acuan titik akhir yang berguna. Anggap hal itu sebagai kondisi contoh ini. W3C menetapkan urutan konten untuk persegi panjang ini; standar tersebut tidak menjamin urutan universal dari sudut kiri atas ke sudut kanan bawah. Modul CSSOM View.
Untuk penempatan berbasis penunjuk dengan Floating UI, middleware inline() menerima koordinat relatif viewport x dan y. Dokumentasinya membatasi pilihan berbasis koordinat ini pada persegi panjang yang tidak berdekatan, jadi jangan menggambarkannya sebagai pemilih karakter tanpa batasan. inline.
Buat demo lengkap teks yang membungkus di Mac
Gunakan Firefox desktop versi terkini dan buka tab kosong di about:blank. Tekan Command + Option + K untuk membuka Konsol Web halaman tersebut. Mozilla mendokumentasikan pintasan ini dan mengonfirmasi bahwa konsol menjalankan JavaScript dalam konteks halaman. Konsol Web.
Jalankan program lengkap di bawah ini pada konsol tersebut. Program ini membuat semua konten contoh, gaya, dan kontrolnya, jadi tidak diperlukan proyek, impor, atau pustaka eksternal. Frasa sintetisnya melintasi elemen penekanan yang sudah ada. Anotasi ini berupa label tampilan saja; tidak ada kontrol pengeditan.
(() => {
window.fragmentDemo?.destroy();
const host = document.createElement('section');
host.style.cssText = 'margin:48px 24px;padding-bottom:900px';
const p = document.createElement('p');
p.dir = 'ltr';
p.style.cssText = 'font:20px/1.6 Georgia,serif;width:24rem;max-width:80vw;writing-mode:horizontal-tb';
const a = document.createTextNode('Sample A: amber tiles follow the curved path ');
const em = document.createElement('em');
em.textContent = 'past the small blue circle and ';
const c = document.createTextNode('toward the final silver square. End.');
p.append(a, em, c);
const button = document.createElement('button');
button.textContent = 'Change text width';
const status = document.createElement('p');
host.append(button, p, status);
document.body.append(host);
const range = document.createRange();
range.setStart(a, 'Sample A: '.length);
range.setEnd(c, c.data.indexOf(' End.'));
const note = document.createElement('div');
note.textContent = 'Note A: review this phrase';
note.style.cssText = 'position:fixed;z-index:1000;background:#fff2ad;color:#222;padding:6px 10px;font:14px/1.4 system-ui;pointer-events:none';
document.body.append(note);
const style = document.createElement('style');
style.textContent = '::highlight(fragment-demo) {background-color:#dbeafe;}';
document.head.append(style);
const canHighlight = typeof Highlight !== 'undefined' && !!window.CSS?.highlights;
if (canHighlight) CSS.highlights.set('fragment-demo', new Highlight(range));
status.textContent = canHighlight ? 'The blue phrase owns Note A.' : 'Text highlighting unavailable; Note A still uses the range.';
function update() {
if (!host.isConnected) return;
const rects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0);
const r = rects.at(-1);
note.hidden = !r || range.collapsed;
if (note.hidden) return;
const maxLeft = Math.max(8, innerWidth - note.offsetWidth - 8);
note.style.left = Math.max(8, Math.min(r.left, maxLeft)) + 'px';
note.style.top = (r.bottom + 8) + 'px';
}
let narrow = false;
button.onclick = () => {
narrow = !narrow;
p.style.width = narrow ? '16rem' : '24rem';
update();
};
const observer = new ResizeObserver(update);
observer.observe(p);
window.addEventListener('resize', update);
window.addEventListener('scroll', update, true);
document.fonts.ready.then(update);
update();
window.fragmentDemo = {destroy() {
observer.disconnect();
window.removeEventListener('resize', update);
window.removeEventListener('scroll', update, true);
if (canHighlight) CSS.highlights.delete('fragment-demo');
host.remove(); note.remove(); style.remove();
}};
})();
Titik awal dan akhir mengecualikan awalan Sample A: dan akhiran End.. Klik Change text width untuk berganti di antara dua lebar paragraf. Range yang sama tetap menjadi target anotasi sementara label diposisikan ulang di bawah fragmen terakhir yang dapat digunakan. Gulir dan ubah ukuran jendela untuk memeriksa perilakunya. Hapus demo dengan window.fragmentDemo.destroy().
Program ini lolos pemeriksaan sintaks JavaScript, dan offset sampel serta perhitungan posisi fragmennya lolos pemeriksaan deterministik. Perenderan browser tidak dijalankan di sini; langkah-langkah di atas menyediakan verifikasi visual.
Bagaimana demo mempertahankan alur dan pembentukan teks?
Range mengukur teks yang sudah ada. Setelah menyusun sampel, program membiarkan node teks frasa dan elemen penekanannya tetap di tempat. Program menambahkan label secara terpisah dan secara opsional menyorot frasa melalui CSS Custom Highlight API, yang menurut dokumentasi MDN dapat digunakan tanpa mengubah struktur DOM yang mendasarinya. API sorotan khusus CSS.
Perbedaan itu penting saat menyesuaikan pendekatan ini. Menambahkan padding, batas, atau margin inline di antara unit karakter dapat menimbulkan batas pembentukan teks. W3C juga menyebut vertical-align nonawal dan batas isolasi dua arah sebagai kondisi yang mengganggu pembentukan teks. Hindari menyisipkan pembungkus karakter yang diposisikan satu per satu hanya untuk mendapatkan koordinat; ukur hasil yang sudah dirender oleh browser. Modul CSS Text Level 3, Pembentukan Teks Lintas Batas Elemen.
Untuk teks aplikasi yang sebenarnya, tentukan titik awal dan akhir range berdasarkan batas teks yang dimaksud. Offset teks DOM menggunakan unit kode; offset tersebut bukan hitungan glif yang terlihat. Pastikan titik batas selaras dengan karakter lengkap yang dimaksud, alih-alih menganggap setiap karakter yang ditampilkan menempati satu offset. Standar DOM.
Penyorotan bersifat opsional. MDN mencantumkan Highlight sebagai Baseline 2025 dan menyebutkan bahwa browser lama mungkin tidak memilikinya. Pemeriksaan fitur dalam demo menjaga penempatan anotasi saat penyorotan tidak tersedia dan menampilkan pesan status yang menjelaskannya. Highlight.
Apa yang perlu diperbarui saat tata letak halaman berubah?
Demo mengukur ulang saat paragraf diubah ukurannya, jendela diubah ukurannya, dan halaman digulir. ResizeObserver mendeteksi perubahan dimensi elemen, termasuk perubahan yang tidak terkait dengan perubahan ukuran jendela browser. Callback di sini hanya menggeser label terpisah sehingga menghindari putaran umpan balik yang berulang kali mengubah ukuran paragraf yang diukur. ResizeObserver.
Pemuatan font memerlukan pembaruan tersendiri. Program mengukur segera, lalu mengukur kembali saat document.fonts.ready terpenuhi. MDN menyatakan bahwa promise ini terpenuhi setelah pemuatan font dokumen dan operasi tata letak terkait selesai. Perubahan font atau konten berikutnya yang dipicu aplikasi perlu meminta pengukuran ulang secara eksplisit. Properti FontFaceSet: ready.
Gunakan satu sistem koordinat. Demo menggunakan label berposisi tetap pada halaman kosong dan menerapkan koordinat client secara langsung. Menambahkan offset gulir dokumen akan mengubahnya menjadi koordinat dokumen. MDN menjelaskan bahwa persegi panjang client relatif terhadap viewport, dan penambahan offset gulir adalah konversi yang digunakan saat koordinat relatif dokumen diperlukan. Metode Element: getBoundingClientRect().
Contoh kecil ini membatasi penempatan horizontal, tetapi tidak menerapkan penghindaran benturan vertikal, penyembunyian saat berada di luar layar, atau kontainer dengan transformasi sembarang. Contoh ini juga tidak memulihkan anotasi setelah pemuatan ulang atau penggantian teks secara keseluruhan. Untuk aplikasi, simpan cara untuk mengidentifikasi bagian teks yang dimaksud, bangun ulang range setelah perenderan, lalu ukur kembali.
Kapan sebaiknya Anda menggunakan Floating UI?
Gunakan Floating UI saat label memerlukan penempatan yang mempertimbangkan kliping serta siklus pembaruan yang terkelola. Antarmuka referensi virtualnya mendukung range melalui dua metode: getBoundingClientRect() yang mengembalikan persegi panjang pembatas range dan getClientRects() yang mengembalikan persegi panjang fragmennya. Berikan contextElement elemen nyata yang berisi teks agar konteks elemen tersedia untuk kliping dan deteksi pembaruan. Elemen Virtual.
Berikan referensi virtual tersebut ke computePosition() dan sertakan inline() di dekat awal array middleware, sebelum flip(). Sesuaikan posisi CSS elemen mengambang dengan strategi yang dipilih. Dokumentasi menjelaskan flip() untuk berpindah sisi saat ruang tidak mencukupi dan shift() untuk menggeser label agar tetap berada dalam batas kliping. inline, computePosition.
Jalankan autoUpdate() hanya saat anotasi terpasang, lalu panggil fungsi pembersihan yang dikembalikannya saat menghapus label. Mekanisme pembaruannya mencakup pengguliran leluhur, perubahan ukuran, dan pergeseran tata letak. Tetap picu pembaruan setelah perubahan aplikasi yang mengubah fragmen teks terpilih tanpa mengubah dimensi kontainer yang diamati. autoUpdate.
Diagnosis anotasi yang masih tampak keliru
Lakukan pemeriksaan berikut secara berurutan agar masalah geometri tidak menyamarkan masalah penentuan target teks.
- Pastikan frasanya benar. Periksa
range.toString()dan kedua titik batas sebelum menyesuaikan koordinat. - Periksa fragmennya. Catat
Array.from(range.getClientRects()). Beberapa entri yang tumpang tindih dapat menunjukkan konten inline bertingkat; jangan menafsirkan jumlahnya sebagai jumlah baris. - Periksa aturan penempatannya. Persegi panjang pembatas, fragmen terakhir, dan fragmen yang dipilih penunjuk mendukung perilaku anotasi yang berbeda.
- Periksa sistem koordinatnya. Untuk demo halaman kosong, gunakan koordinat client untuk label berposisi tetap.
- Ulangi setelah tata letak berubah. Ubah lebar, gulir, ubah ukuran, dan tunggu hingga font selesai dimuat. Bangun ulang range jika aplikasi mengganti node teksnya.
- Periksa asumsi tata letaknya. Untuk penulisan vertikal, arah campuran, kolom, atau kontainer dengan transformasi, tentukan dan uji aturan pemilihan fragmen yang sesuai sebelum menggunakan kembali cara pintas fragmen terakhir dari demo.
Anotasi yang andal berawal dari range teks yang akurat dan aturan penempatan yang jelas. Biarkan browser menangani pembungkusan dan pembentukan teks, lalu ukur ulang fragmen yang dirender setiap kali tata letak terkait berubah. Dengan begitu, catatan tetap memiliki hubungan yang stabil dengan frasa tersebut sementara kata-katanya mengalir secara alami.
Sumber
- Standar DOM — WHATWG
- Modul CSSOM View — W3C
- Modul CSS Text Level 3 — W3C
- Metode Range: getClientRects() — MDN
- Metode Element: getBoundingClientRect() — MDN
- API sorotan khusus CSS — MDN
- Highlight — MDN
- ResizeObserver — MDN
- Properti FontFaceSet: ready — MDN
- Konsol Web — Mozilla
- inline — Floating UI
- Elemen Virtual — Floating UI
- computePosition — Floating UI
- autoUpdate — Floating UI