Om een tekstbereik te verpakken terwijl bestaande inline-elementen behouden blijven, verpak elk geselecteerd tekstfragment in zijn eigen span. Deze Safari-op-Mac-walkthrough gebruikt een synthetische paragraaf om nadruk-elementen, tekstvolgorde en elementreferenties intact te houden.
Waarom één omringende span kan mislukken
Een browserbereik kan binnen een inline-element beginnen en daarbuiten eindigen. Een selectie kan bijvoorbeeld halverwege genadrukte tekst beginnen, een genest element kruisen en eindigen in de afsluitende tekst van de paragraaf. Die selectie hoeft niet noodzakelijk met één complete subtekenreeks te corresponderen.
Range.surroundContents() kan niet elke dergelijke selectie aan. Het gooit een fout wanneer het bereik een niet-tekstknooppunt deels bevat. De huidige DOM-standaard specificeert een InvalidStateError voor die voorwaarde. Een bereik dat tekst en volledig geselecteerde elementen bevat, kan deze methode gebruiken, maar het kruisen van een elementgrens vereist een andere aanpak. DOM Standard, Range: surroundContents()-methode
Het extraheren van het bereik in één nieuwe span is een andere mogelijke transformatie, maar die heeft andere bewaringsregels. extractContents() kloont deels geselecteerde voorouderelementen. Als uw taak vereist dat de oorspronkelijke inline-elementen heel blijven en hun identiteiten behouden, zijn die gekloonde takken ongeschikt. Range: extractContents()-methode
Gebruik deze beslunghulp voordat u het document wijzigt:
| Vereist resultaat | Geschikte aanpak |
|---|---|
| Één wrapper om tekst en volledig geselecteerde elementen | Overweeg surroundContents() |
| Oorspronkelijke inline-elementen blijven heel bij een deelselectie | Verpak geselecteerde tekstfragmenten apart |
| Één wrapper over deels geselecteerde elementgrenzen | Accepteer herstructurering en beoordeel extractie/kloon-gedrag |
| Visuele nadruk zonder elementen toe te voegen | Overweeg of de taak spans überhaupt nodig heeft |
Het onderstaande voorbeeld kiest voor losse wrappers. Structuur behouden betekent hier dat de bestaande elementobjecten en hun voorouderrelaties behouden blijven, terwijl spans eronder worden toegevoegd. Tekstknooppunten worden gesplitst waar de selectie begint of eindigt.
Open het synthetische voorbeeld in Safari op Mac
Open een nieuw Safaritabblad en voer about:blank in. Kies vervolgens Safari → Instellingen… → Geavanceerd en schakel Functies voor webontwikkelaars tonen in. Open Ontwikkelaar → Web Inspector tonen, of druk Option–Command–I. Dit zijn de stappen gedocumenteerd door WebKit. Web Inspector inschakelen
Selecteer het tabblad Console. Voer het onderstaande complete JavaScript-programma in de prompt uit. WebKit documenteert dat de consoleprompt JavaScript evalueert in de geselecteerde uitvoeringscontext van de geïnspecteerde pagina. Gebruik de context op het hoogste niveau voor dit voorbeeld. Web Inspector-interface
Geen bibliotheek, import, buildstap of bestaande documentinhoud is vereist. Het programma creëert zijn eigen paragraaf, verpakt een vooraf bepaald bereik, herstelt een voorwaartse selectie over de verpakte tekst en voegt een verwijderknop toe. De ondersteunde invoer is bewust smal: één paragraaf met tekst en gewone em, strong, span of code-nakomelingen, met beide bereikgrenzen binnen tekstknooppunten.
(() => {
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);
})();
Begrijpen welke tekst verpakt wordt
De paragraaf luidt Alpha bright blue gamma delta. Het bestaande strong-element bevat bright en een genest em-element dat blue bevat. Het bereik begint op offset 3 in bright en eindigt op offset 6 in gamma delta. Dit zijn illustratieve invoer gedefinieerd door het programma.
De verwachte console-uitvoer bevat drie fragmenten:
["ght ", "blue", " gamma"]
De eerste span wordt een kind van het oorspronkelijke strong-element. De tweede wordt een kind van het oorspronkelijke em-element. De derde wordt een kind van de paragraaf. Het lezen van hun tekst in documentvolgorde reproduceert de geselecteerde string, ght blue gamma. Het niet-geselecteerde voorvoegsel bri en achtervoegsel delta. blijven buiten de wrappers.
De traversie gebruikt createTreeWalker() met NodeFilter.SHOW_TEXT om afstammelijke tekstknooppunten te bezoeken. intersectsNode() identificeert kandidaten, en de grens-offsets knippen de eerste en laatste kandidaten. Nul-lengte fragmenten worden uitgesloten. Document: createTreeWalker()-methode, Range: intersectsNode()-methode
Het programma registreert alle fragmentgrenzen voordat er iets gemuteerd wordt. Dit is belangrijk omdat DOM-bereiken live zijn: het splitten, invoegen of verwijderen van knooppunten kan hun grenspunten bijwerken. Een gekloonde Range zou ook live zijn; het opslaan van de knooppuntreferenties en numerieke offsets levert de stabiele werklijst die hier gebruikt wordt. DOM Standard: Ranges
Voor elk fragment splitst het programma eerst de eindgrens, dan de begingrens. splitText() laat het voorvoegsel in het oorspronkelijke tekstknooppunt en retourneert de overige tekst als een broerknooppunt. Hierdoor wordt de geselecteerde substring geïsoleerd zonder de omringende elementen opnieuw te creëren. Text: splitText()-methode
De vier fragment-planningsgevallen—kruis-element tekst, een substring binnen één knooppunt, een samengevouwen bereik en grenzen die geen tekens selecteren—waren deterministisch gecontroleerd in Node.js. Browser DOM-mutaties en Safari-interacties werden tijdens de voorbereiding niet uitgevoerd; het programma bevat assertions om die resultaten te controleren bij uitvoering.
De juiste structuur en levenscycluscontracten behouden
De bestaande strong en em elementen worden door dit programma nooit verwijderd, vervangen of gekloond. Hun elementreferenties blijven bruikbaar, en de listener die aan strong is gekoppeld, blijft aan datzelfde object gekoppeld. Het voorbeeld controleert zowel de ouderrelaties als een synthetische event-dispatch na het verpakken.
Tekstknooppuntidentiteit heeft een ander contract. Grens-splitsing introduceert nieuwe tekstknooppunten, en elk geselecteerd tekstfragment verplaatst naar een nieuwe span. Elke applicatie die tekstknooppuntreferenties vasthoudt, moet rekening houden met die verandering. De methode behoudt tekstinhoud en de oorspronkelijke elementhiërarchie; het behoudt niet elke oorspronkelijke tekstknooppuntgrens.
Het toevoegen van spans verandert ook de directe ouderrelaties voor tekst. Voordat u deze transformatie in een bestaand document adopteert, controleer code die verwacht dat het eerste kind van een element een tekstknooppunt is, CSS-selectors die afhangen van directe kinderen, en event-handlers die het event-target inspecteren. Het behouden van een elementobject alleen garandeert niet dat elke omringende applicatie-aanname nog geldt.
Het voorbeeld herstelt expliciet een voorwaartse selectie na het verpakken. Het behoudt geen achterwaartse selectierichting en biedt geen editor-undo-transactie. Behandel die als afzonderlijke vereisten bij integratie in een bewerkinterface. De wrapper-functie retourneert zijn spans zonder een bepaalde browser-selectietoestand te beloven.
Klikken op Spans verwijderen pakt alleen de spans uit die tijdens die run zijn gemaakt en wist de selectie. Het laat de gesplitste tekstknooppunten apart. Die verwijderactie herstelt de oorspronkelijke elementhiërarchie en tekst, maar is geen exacte omkering van tekstknooppuntidentiteiten. Voeg geen automatische normalisatie toe als andere code afhankelijk is van die referenties.
Het voorbeeld aanpassen zonder de scope stilzwijgend uit te breiden
Voor een door de gebruiker gesleepte selectie, verkrijg het bereik pas na controle of window.getSelection() een selectie retourneerde en of rangeCount ongelijk is aan nul. getRangeAt(0) retourneert het eerste bereik; het opvragen van een index buiten het bereik van de selectie produceert een fout. Selection: getRangeAt()-methode
Geef dat bereik door aan dezelfde helper alleen wanneer beide grenscontainers tekstknooppunten zijn binnen de bedoelde paragraaf. Een selectie wiens grenscontainer een element is, wordt vóór mutatie verworpen. Het ondersteunen van dergelijke grenzen vereist een extra, expliciet gedefinieerde conversie naar afstammelijke tekstposities; het enkel wegnemen van de bewaking implementeert dat niet.
De helper verwijst ook niet-ondersteunde nakomelingen en bestaande gemarkeerde wrappers voordat wijzigingen worden aangebracht. Voor herhaalde of overlappende annotaties, definieer eerst of nieuwe spans moeten nesten, samenvoegen of eerdere vervangen. Het voorbeeld laat die keuze bewust open in plaats van wrappers zonder beleid op te hopen.
Offsets worden gemeten in UTF-16-code-eenheden. Voor berekende posities in tekst met multi-unit karakters, zorg dat grenzen de beoogde karaktersreeks niet verdelen. Het synthetische voorbeeld gebruikt simpele Latijnse tekst, dus de numerieke offsets zijn onmiskenbaar. Text: splitText()-methode
Voor deze taak is de nuttige invariant eenvoudig: het samenvoegen van de tekst van de nieuwe spans moet gelijk zijn aan de oorspronkelijke geselecteerde string, de volledige tekst van de paragraaf moet ongewijzigd blijven, en de bestaande inline-elementen moeten hun identiteiten en voorouderrelaties behouden. Controleer alle drie bij het aanpassen van het voorbeeld; samen beschrijven ze wat succesvolle behoud betekent.
Bronnen
- DOM Standard — WHATWG.
- Web Inspector inschakelen — WebKit.
- Web Inspector-interface — WebKit.
- Range: surroundContents()-methode — MDN Web Docs.
- Range: extractContents()-methode — MDN Web Docs.
- Document: createTreeWalker()-methode — MDN Web Docs.
- Range: intersectsNode()-methode — MDN Web Docs.
- Text: splitText()-methode — MDN Web Docs.
- Selection: getRangeAt()-methode — MDN Web Docs.