Mevcut satır içi öğeleri koruyarak bir metin aralığını sarmak için seçilen her metin parçasını kendi span’iyle sarın. Safari on Mac için bu kılavuz, vurgu öğelerini, metin sırasını ve öğe referanslarını korumak amacıyla sentetik bir paragraf kullanır.
Tek bir sarmalayıcı span neden başarısız olabilir
Bir tarayıcı aralığı bir satır içi öğenin içinde başlayıp onun dışında bitebilir. Örneğin seçim, vurgulanmış metnin ortasında başlayabilir, iç içe geçmiş bir öğeden geçebilir ve paragrafın sonundaki metinde bitebilir. Böyle bir seçim, tek bir tam alt ağaçla her zaman örtüşmez.
Range.surroundContents() bu tür seçimlerin tümünü işleyemez. Aralık metin olmayan bir düğümü kısmen içerdiğinde hata verir. Geçerli DOM Standardı bu durum için InvalidStateError belirtir. Metin ve tamamen seçilmiş öğeler içeren bir aralık bu yöntemi kullanabilir; ancak bir öğe sınırını aşmak farklı bir yaklaşım gerektirir. DOM Standardı, Range: surroundContents() yöntemi
Aralığı tek bir yeni span içine çıkarmak başka bir olası dönüştürme yöntemidir, ancak koruma kuralları farklıdır. extractContents(), kısmen seçilmiş üst öğeleri klonlar. Göreviniz özgün satır içi öğelerin bütün kalmasını ve kimliklerini korumasını gerektiriyorsa bu klonlanmış dallar uygun değildir. Range: extractContents() yöntemi
Belgeyi değiştirmeden önce şu karar tablosunu kullanın:
| Gerekli sonuç | Uygun yaklaşım |
|---|---|
| Metin ve tamamen seçilmiş öğelerin çevresinde tek sarmalayıcı | surroundContents() yöntemini değerlendirin |
| Kısmi bir seçim boyunca özgün satır içi öğeler bütün kalır | Seçili metin parçalarını ayrı ayrı sarın |
| Kısmen seçilmiş öğe sınırları boyunca tek sarmalayıcı | Yeniden yapılandırmayı kabul edin ve çıkarma/klonlama davranışını inceleyin |
| Öğe eklemeden görsel vurgu | Görevin gerçekten span gerektirip gerektirmediğini yeniden değerlendirin |
Aşağıdaki örnek ayrı sarmalayıcıları seçer. Burada yapıyı korumak, altlarına span’ler eklerken mevcut öğe nesnelerini ve üst öğe ilişkilerini korumak anlamına gelir. Seçimin başladığı veya bittiği yerde metin düğümleri bölünür.
Sentetik örneği Mac’te Safari’de açın
Yeni bir Safari sekmesi açıp about:blank yazın. Ardından Safari → Settings… → Advanced yolunu izleyip Show features for web developers seçeneğini etkinleştirin. Develop → Show Web Inspector yolunu izleyin veya Option–Command–I tuşlarına basın. Bu adımlar WebKit tarafından belgelenmiştir. Web Inspector’ı etkinleştirme
Console sekmesini seçin. Aşağıdaki JavaScript programının tamamını komut isteminde çalıştırın. WebKit, konsol komut isteminin incelenen sayfanın seçili yürütme bağlamında JavaScript değerlendirdiğini belgeliyor. Bu örnekte üst düzey sayfa bağlamını kullanın. Web Inspector arayüzü
Herhangi bir kitaplık, içe aktarma, derleme adımı veya mevcut belge içeriği gerekmez. Program kendi paragrafını oluşturur, önceden belirlenmiş bir aralığı sarar, sarılan metin üzerinde ileri yönlü bir seçimi yeniden oluşturur ve bir kaldırma düğmesi ekler. Desteklenen girdi özellikle sınırlıdır: metin ve sıradan em, strong, span veya code alt öğeleri içeren tek bir paragraf; aralığın her iki sınırı da metin düğümlerinin içinde olmalıdır.
(() => {
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);
})();
Hangi metnin sarılacağını anlayın
Paragrafta Alpha bright blue gamma delta. yazar. Mevcut strong öğesi bright metnini, iç içe geçmiş em öğesi ise blue metnini içerir. Aralık, bright metnindeki 3 ofsetinde başlar ve gamma delta. metnindeki 6 ofsetinde biter. Bunlar programda tanımlanmış örnek girdilerdir.
Beklenen konsol çıktısı üç parça içerir:
["ght ", "blue", " gamma"]
İlk span, özgün strong öğesinin alt öğesi olur. İkincisi, özgün em öğesinin alt öğesi olur. Üçüncüsü paragrafın alt öğesi olur. Metinleri belge sırasına göre okunduğunda seçilen ght blue gamma dizgesi elde edilir. Seçilmeyen bri öneki ve delta. son eki sarmalayıcıların dışında kalır.
Geçiş, alt metin düğümlerini ziyaret etmek için NodeFilter.SHOW_TEXT ile createTreeWalker() kullanır. intersectsNode() adayları belirler; sınır ofsetleri ilk ve son adayları kırpar. Uzunluğu sıfır olan parçalar dışarıda bırakılır. Document: createTreeWalker() yöntemi, Range: intersectsNode() yöntemi
Program, herhangi bir değişiklik yapmadan önce tüm parça sınırlarını kaydeder. Bu önemlidir; çünkü DOM aralıkları canlıdır: düğümleri bölmek, eklemek veya kaldırmak sınır noktalarını güncelleyebilir. Klonlanmış bir Range de canlı olurdu; burada kullanılan sabit iş listesini düğüm referanslarını ve sayısal ofsetleri kaydetmek sağlar. DOM Standardı: Aralıklar
Her parça için program önce bitiş sınırını, ardından başlangıç sınırını böler. splitText(), öneki özgün metin düğümünde bırakır ve kalan metni bir kardeş düğüm olarak döndürür. Böylece seçili alt dize, onu çevreleyen öğeler yeniden oluşturulmadan ayrıştırılır. Text: splitText() yöntemi
Dört parça planlama durumu—öğeler arası metin, tek düğüm içindeki bir alt dize, daraltılmış bir aralık ve hiçbir karakter seçmeyen sınırlar—Node.js’te belirlenimci biçimde denetlendi. Hazırlık sırasında tarayıcı DOM değişiklikleri ve Safari etkileşimi çalıştırılmadı; program, çalıştırıldığında bu sonuçları denetlemek için onaylamalar içeriyor.
Doğru yapıyı ve yaşam döngüsü sözleşmelerini koruyun
Mevcut strong ve em öğeleri bu program tarafından hiçbir zaman kaldırılmaz, değiştirilmez veya klonlanmaz. Öğe referansları kullanılabilir durumda kalır ve strong öğesine eklenmiş dinleyici aynı nesneye bağlı kalır. Örnek, sarmalama işleminden sonra hem üst öğe ilişkilerini hem de sentetik bir olay dağıtımını denetler.
Metin düğümü kimliğinin sözleşmesi farklıdır. Sınırları bölmek yeni metin düğümleri oluşturur ve seçilen her metin parçası yeni bir span’e taşınır. Metin düğümü referanslarını elinde tutan uygulamalar bu değişikliği hesaba katmalıdır. Yöntem metin içeriğini ve özgün öğe hiyerarşisini korur; her özgün metin düğümü sınırını korumaz.
Span eklemek, metnin doğrudan üst öğe ilişkilerini de değiştirir. Bu dönüştürmeyi mevcut bir belgede uygulamadan önce, bir öğenin ilk alt öğesinin metin düğümü olmasını bekleyen kodları, doğrudan alt öğelere bağlı CSS seçicilerini ve olay hedefini inceleyen olay işleyicilerini denetleyin. Bir öğe nesnesini korumak, çevresindeki tüm uygulama varsayımlarının geçerliliğini sürdürdüğü anlamına gelmez.
Örnek, sarmalama işleminden sonra ileri yönlü bir seçimi açıkça yeniden oluşturur. Geriye doğru seçim yönünü korumaz ve bir düzenleyici geri alma işlemi sunmaz. Düzenleme arayüzüne entegre ederken bunları ayrı gereksinimler olarak ele alın. Sarmalayıcı işlevi span’lerini döndürür; tarayıcı seçim durumunun belirli olacağını taahhüt etmez.
Remove spans düğmesine tıklandığında yalnızca o çalıştırmada oluşturulan span’lerin sarmalaması kaldırılır ve seçim temizlenir. Bölünmüş metin düğümleri ayrı kalır. Bu kaldırma işlemi özgün öğe hiyerarşisini ve metni geri yükler, ancak metin düğümü kimliklerini tam olarak geri yüklemez. Başka kodlar bu referanslara bağlıysa otomatik normalleştirme eklemekten kaçının.
Örneği kapsamını fark ettirmeden genişletmeden uyarlayın
Kullanıcının fareyle sürükleyerek yaptığı bir seçim için aralığı yalnızca window.getSelection() bir seçim döndürdükten ve rangeCount sıfırdan farklı olduktan sonra alın. getRangeAt(0) ilk aralığı döndürür; seçimin aralık sayısının dışındaki bir dizini istemek hata oluşturur. Selection: getRangeAt() yöntemi
Bu aralığı aynı yardımcı işleve yalnızca her iki sınır kapsayıcısı da hedeflenen paragraf içindeki metin düğümleri olduğunda iletin. Sınır kapsayıcısı bir öğe olan seçim, değişiklik yapılmadan önce reddedilir. Bu tür sınırları desteklemek için alt metin konumlarına açıkça tanımlanmış ek bir dönüştürme gerekir; korumayı yalnızca kaldırmak bu işlevi uygulamaz.
Yardımcı işlev ayrıca değişiklik yapmadan önce desteklenmeyen alt öğeleri ve mevcut işaretli sarmalayıcıları reddeder. Yinelenen veya çakışan açıklamalar için yeni span’lerin iç içe geçmesi, birleştirilmesi ya da önceki span’lerin yerini alması gerekip gerekmediğini önce tanımlayın. Örnek, bir politika belirlemeden sarmalayıcıları biriktirmek yerine bu kararı açıkça ortaya koyar.
Ofsetler UTF-16 kod birimleriyle ölçülür. Birden fazla birim kullanan karakterler içeren metinde hesaplanmış konumlar için sınırların amaçlanan karakter dizisini bölmediğinden emin olun. Sentetik örnekte basit Latin metni kullanıldığından sayısal ofsetler belirsiz değildir. Text: splitText() yöntemi
Bu görev için yararlı değişmezler basittir: yeni span’lerin metinlerini birleştirmek özgün seçili dizeye eşit olmalı, paragrafın tüm metni değişmeden kalmalı ve mevcut satır içi öğeler kimliklerini ve üst öğe ilişkilerini korumalıdır. Örneği uyarlarken üçünü de denetleyin; birlikte başarılı korumanın ne anlama geldiğini tanımlarlar.
Kaynaklar
- DOM Standardı — WHATWG.
- Web Inspector’ı etkinleştirme — WebKit.
- Web Inspector arayüzü — WebKit.
- Range: surroundContents() yöntemi — MDN Web Docs.
- Range: extractContents() yöntemi — MDN Web Docs.
- Document: createTreeWalker() yöntemi — MDN Web Docs.
- Range: intersectsNode() yöntemi — MDN Web Docs.
- Text: splitText() yöntemi — MDN Web Docs.
- Selection: getRangeAt() yöntemi — MDN Web Docs.