Zakotwicz adnotację w zakresie DOM Range, a następnie umieść jej etykietę, korzystając z prostokątów renderowanych fragmentów zakresu. Ten przewodnik przedstawia niewielkie demo w przeglądarce na komputerze Mac, w którym notatka pozostaje przy frazie przechodzącej przez elementy inline i zawijającej się na kilka wierszy.
Zadanie jest konkretne: dołączyć jedną widoczną adnotację do frazy w zwykłym układzie strony internetowej, bez ponownego tworzenia jej tekstu. Wskażesz frazę, wybierzesz fragment dla etykiety i ponownie wykonasz pomiar po zmianach układu. MDN opisuje Range.getClientRects() jako przeglądarkowe API służące do pobierania prostokątów zajmowanych przez zakres. Range: getClientRects() method.
Dlaczego etykieta oddala się od zawiniętego tekstu?
Fraza ma dwie istotne reprezentacje: pozycję w dokumencie i bieżącą geometrię na ekranie. WHATWG definiuje zakres przez punkty początkowy i końcowy, z których każdy składa się z węzła i przesunięcia. Te punkty mogą znajdować się w różnych węzłach tekstowych, dzięki czemu jeden zakres może obejmować istniejący element wyróżnienia. DOM Standard.
Pojedynczy prostokąt obwiedni gubi szczegóły. Wyobraź sobie frazę zaczynającą się w połowie jednego wiersza i kończącą blisko początku następnego. Jej prostokąt obwiedni obejmuje puste miejsce obok obu wierszy. Etykieta wyśrodkowana względem tego prostokąta może więc sprawiać wrażenie oderwanej od słów. Floating UI wyraźnie wskazuje ten problem w przypadku wielowierszowych linków i zaznaczeń zakresu. inline.
Poszczególne prostokąty również wymagają interpretacji. W3C określa, że zakres może zwracać zarówno prostokąty obramowania zaznaczonych elementów, jak i prostokąty tekstu, w kolejności treści. Zagnieżdżone elementy mogą więc powodować nakładanie się prostokątów; ich liczba nie jest wiarygodną miarą liczby wierszy. Granice tekstu opierają się na metrykach fontu, a nie dokładnych obrysach wyrenderowanych glifów. CSSOM View Module.
Który fragment powinien być punktem odniesienia dla adnotacji?
Przed napisaniem kodu pozycjonującego wybierz regułę umieszczania. Poniższa tabela rozróżnia tekst, którego dotyczy adnotacja, od miejsca, w którym pojawia się jej etykieta.
| Zamierzone zachowanie | Geometria odniesienia | Reguła umieszczania |
|---|---|---|
| Notatka podążająca za frazą | Cały zakres | Umieść ją pod ostatnim użytecznym fragmentem w prostym poziomym układzie, od lewej do prawej |
| Etykieta blisko interakcji wskaźnikiem | Prostokąty fragmentów zakresu | Wybierz fragment znajdujący się pod współrzędnymi klienta interakcji |
| Wyśrodkowana etykieta poglądowa | Prostokąt obwiedni | Zaakceptuj, że etykieta może znaleźć się nad pustym miejscem |
| Tło zaznaczające całą frazę | Cały zakres | Użyj wyróżnienia tekstu niezależnie od położenia etykiety |
Demo stosuje pierwszą regułę. Celowo ustala tryb pisma i kierunek, aby ostatni prostokąt był przydatnym punktem odniesienia dla końca frazy. Traktuj to jako założenie przykładu. W3C określa kolejność treści dla tych prostokątów; nie gwarantuje uniwersalnego porządku od lewego górnego do prawego dolnego rogu. CSSOM View Module.
W przypadku umieszczania zależnego od wskaźnika z Floating UI jego mechanizm pośredniczący inline() przyjmuje współrzędne x i y względem obszaru widoku. Dokumentacja ogranicza wybór oparty na współrzędnych do rozłącznych prostokątów, więc nie należy opisywać go jako nieograniczonego selektora znaków. inline.
Zbuduj kompletne demo zawijanego tekstu na Macu
Użyj aktualnej desktopowej wersji Firefoksa i otwórz pustą kartę pod adresem about:blank. Naciśnij Command + Option + K, aby otworzyć konsolę internetową strony. Mozilla dokumentuje ten skrót i potwierdza, że konsola wykonuje JavaScript w kontekście strony. Web Console.
Uruchom poniższy kompletny program w tej konsoli. Tworzy on całą przykładową treść, style i elementy sterujące, więc nie wymaga projektu, importów ani zewnętrznej biblioteki. Syntetyczna fraza przechodzi przez istniejący element wyróżnienia. Adnotacja jest wyłącznie etykietą prezentacyjną; nie ma elementów sterujących edycją.
(() => {
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();
}};
})();
Punkty końcowe wykluczają prefiks Sample A: i sufiks End.. Kliknij Change text width, aby przełączać się między dwiema szerokościami akapitu. Ten sam zakres pozostaje celem adnotacji, a etykieta jest umieszczana pod jego ostatnim użytecznym fragmentem. Przewiń stronę i zmień rozmiar okna, aby sprawdzić działanie. Usuń demo za pomocą window.fragmentDemo.destroy().
Program przeszedł kontrolę składni JavaScript, a jego przykładowe przesunięcia i obliczenia pozycji fragmentów przeszły deterministyczne sprawdzenia. Renderowania w przeglądarce tutaj nie uruchomiono; powyższe kroki pozwalają zweryfikować wygląd.
Jak demo zachowuje przepływ tekstu i kształtowanie glifów?
Zakres mierzy istniejący tekst. Po utworzeniu przykładu program pozostawia w miejscu węzły tekstowe frazy i element wyróżnienia. Dodaje osobno etykietę i opcjonalnie wyróżnia frazę za pomocą CSS Custom Highlight API, które według MDN działa bez zmiany bazowej struktury DOM. CSS custom highlight API.
To rozróżnienie ma znaczenie przy dostosowywaniu tego podejścia. Dodawanie wewnętrznych dopełnień, obramowań lub marginesów między jednostkami znaków może wprowadzać granice kształtowania. W3C wskazuje także niepoczątkowe vertical-align oraz granice izolacji tekstu dwukierunkowego jako warunki przerywające kształtowanie. Unikaj wstawiania osobno pozycjonowanych opakowań znaków tylko po to, by uzyskać współrzędne; mierz istniejący wynik renderowania przeglądarki. CSS Text Module Level 3, Shaping Across Element Boundaries.
W przypadku tekstu aplikacji wyznacz punkty końcowe zakresu na podstawie zamierzonych granic tekstu. Przesunięcia tekstu DOM są liczone w jednostkach kodowych; nie są liczbą widocznych glifów. Ustawiaj punkty końcowe zgodnie z pełnymi zamierzonymi znakami, zamiast zakładać, że każdy wyświetlany znak zajmuje jedno przesunięcie. DOM Standard.
Wyróżnianie jest opcjonalne. MDN podaje, że Highlight ma status Baseline 2025, i zaznacza, że starsze przeglądarki mogą go nie obsługiwać. Sprawdzenie dostępności funkcji w demo zachowuje pozycjonowanie adnotacji, gdy wyróżnianie jest niedostępne, i wyświetla objaśniający komunikat stanu. Highlight.
Co należy aktualizować, gdy strona zmienia układ?
Demo ponownie wykonuje pomiar po zmianie rozmiaru akapitu lub okna oraz podczas przewijania. ResizeObserver wykrywa zmiany wymiarów elementu, także te niezwiązane ze zmianą rozmiaru okna przeglądarki. Jego procedura zwrotna przesuwa tutaj tylko osobną etykietę, unikając pętli sprzężenia zwrotnego, która wielokrotnie zmieniałaby rozmiar mierzonego akapitu. ResizeObserver.
Ładowanie fontów wymaga osobnej aktualizacji. Program wykonuje pomiar od razu, a następnie ponownie, gdy rozwiąże się document.fonts.ready. MDN podaje, że ta obietnica rozwiązuje się po zakończeniu ładowania fontów dokumentu i powiązanych operacji układu. Późniejsze zmiany fontów lub treści inicjowane przez aplikację powinny jawnie wywoływać kolejny pomiar. FontFaceSet: ready property.
Używaj jednego układu współrzędnych. Demo umieszcza etykietę w pozycji stałej na pustej stronie i bezpośrednio stosuje współrzędne klienta. Dodanie przesunięć przewijania dokumentu zmieniłoby je na współrzędne dokumentu. MDN wyjaśnia, że prostokąty klienta są określane względem obszaru widoku, a dodanie przesunięć przewijania służy do uzyskania współrzędnych względem dokumentu. Element: getBoundingClientRect() method.
Ten niewielki przykład ogranicza położenie w poziomie, ale nie implementuje unikania kolizji w pionie, ukrywania elementów poza obszarem widoku ani obsługi dowolnych przekształconych kontenerów. Nie przywraca też adnotacji po ponownym załadowaniu strony ani całkowitym zastąpieniu tekstu. W aplikacji zachowaj sposób identyfikowania zamierzonego fragmentu, odtwórz jego zakres po renderowaniu, a następnie ponownie wykonaj pomiar.
Kiedy używać Floating UI?
Użyj Floating UI, gdy etykieta wymaga pozycjonowania uwzględniającego przycinanie oraz zarządzanego cyklu aktualizacji. Udokumentowany interfejs wirtualnego odnośnika obsługuje zakres za pomocą dwóch metod: getBoundingClientRect(), zwracającej prostokąt obwiedni zakresu, i getClientRects(), zwracającej jego prostokąty fragmentów. Przekaż contextElement rzeczywisty element zawierający tekst, aby wykrywanie przycinania i aktualizacji miało kontekst elementu. Virtual Elements.
Przekaż ten wirtualny odnośnik do computePosition() i umieść inline() blisko początku tablicy mechanizmów pośredniczących, przed flip(). Dopasuj pozycjonowanie CSS elementu pływającego do wybranej strategii. Dokumentacja opisuje flip() jako mechanizm zmiany strony, gdy brakuje miejsca, oraz shift() jako mechanizm przesuwania etykiety w granicach jej obszaru przycinania. inline, computePosition.
Uruchamiaj autoUpdate() tylko wtedy, gdy adnotacja jest zamontowana, a przy usuwaniu etykiety wywołuj zwróconą funkcję sprzątającą. Udokumentowane mechanizmy aktualizacji obejmują przewijanie elementów nadrzędnych, zmianę rozmiaru i przesunięcia układu. Nadal wywołuj aktualizację po zmianie aplikacji, która modyfikuje fragmenty wybranego tekstu, ale nie zmienia wymiarów obserwowanego kontenera. autoUpdate.
Diagnozowanie adnotacji, która nadal wygląda nieprawidłowo
Wykonuj kolejno te czynności, aby problem z geometrią nie przesłonił problemu z wyborem tekstu.
- Potwierdź frazę. Sprawdź
range.toString()i oba punkty końcowe, zanim zaczniesz korygować współrzędne. - Sprawdź fragmenty. Zaloguj
Array.from(range.getClientRects()). Wiele nakładających się wpisów może wynikać z zagnieżdżonej treści inline; nie traktuj ich liczby jako liczby wierszy. - Sprawdź regułę umieszczania. Prostokąt obwiedni, ostatni fragment i fragment wybrany wskaźnikiem służą różnym rodzajom adnotacji.
- Sprawdź układ współrzędnych. W demo z pustą stroną umieść etykietę stałą, używając współrzędnych klienta.
- Powtórz pomiar po zmianach układu. Zmień szerokość, przewiń, zmień rozmiar okna i zaczekaj na zakończenie ładowania fontów. Odtwórz zakres, jeśli aplikacja zastąpi jego węzły tekstowe.
- Sprawdź założenia dotyczące układu. W przypadku pisma pionowego, mieszanych kierunków, kolumn lub przekształconych kontenerów określ i przetestuj odpowiednią regułę wyboru fragmentu, zanim ponownie użyjesz skrótu z ostatnim fragmentem z tego demo.
Niezawodna adnotacja zaczyna się od dokładnego zakresu tekstu i jawnej reguły umieszczania. Pozwól przeglądarce odpowiadać za zawijanie i kształtowanie tekstu, a następnie ponownie mierz wyrenderowane fragmenty za każdym razem, gdy zmieni się odpowiedni układ. Dzięki temu notatka pozostaje stabilnie powiązana z frazą, a słowa mogą naturalnie przepływać.
Źródła
- DOM Standard — WHATWG
- CSSOM View Module — W3C
- CSS Text Module Level 3 — W3C
- Range: getClientRects() method — MDN
- Element: getBoundingClientRect() method — MDN
- CSS custom highlight API — MDN
- Highlight — MDN
- ResizeObserver — MDN
- FontFaceSet: ready property — MDN
- Web Console — Mozilla
- inline — Floating UI
- Virtual Elements — Floating UI
- computePosition — Floating UI
- autoUpdate — Floating UI