Фронт: точки переноса в длинных ссылках (<wbr>), не полагаемся на CSS

Ссылка на онлайн-курс в поле аудитории — одно длинное слово. CSS-фикс
(overflow-wrap/word-break) работает в современных движках, но на телефоне
пользователя переполнение оставалось: старый движок или закэшированный CSS.

Теперь фронтенд сам расставляет <wbr>: после служебных символов ссылок
(/, ?, &, =, ., _, -) и не реже, чем каждые 24 символа; экранирование
сохраняется. Перенос работает в любом браузере.

Проверено в headless Chromium (390px и 320px): переполнений нет как с
включёнными CSS-переносами, так и с принудительно отключёнными
(эмуляция старого движка).
This commit is contained in:
2026-09-22 15:32:43 +03:00
parent 4685301773
commit 0e62d5ca8d
2 changed files with 23 additions and 6 deletions

View File

@@ -80,6 +80,9 @@ nginx (https://schedule.our-place.ru) → 127.0.0.1:8090
заменяется на «Окно».
- Подпись недели появляется только там, где недели действительно
различаются; бейдж «Сейчас» подсвечивает текущую неделю.
- Длинные ссылки в карточке переносятся по служебным символам и каждые
24 символа (`<wbr>`), поэтому карточка не вылезает за экран даже в старых
браузерах, где нет `overflow-wrap`.
- Приложение можно «установить» на главный экран (манифест + service
worker + иконки приложения); на iOS иконку берёт `apple-touch-icon`.

View File

@@ -239,10 +239,10 @@ function entityLessonHTML(parity, entry) {
<div>
${badge}
${entry.marker ? `<span class="badge badge-kv">${escapeHTML(entry.marker)}</span>` : ""}
<span class="subject">${escapeHTML(entry.subject)}</span>
<span class="subject">${withBreaks(entry.subject)}</span>
</div>
${details.length ? `<div class="details">${escapeHTML(details.join(" · "))}</div>` : ""}
${entry.note ? `<div class="lesson-note">${escapeHTML(entry.note)}</div>` : ""}
${details.length ? `<div class="details">${withBreaks(details.join(" · "))}</div>` : ""}
${entry.note ? `<div class="lesson-note">${withBreaks(entry.note)}</div>` : ""}
</div>`;
}
@@ -277,10 +277,10 @@ function singleLessonHTML(badgeKey, lesson, full) {
<div>
${badge}
${lesson.marker ? `<span class="badge badge-kv">${escapeHTML(lesson.marker)}</span>` : ""}
<span class="subject">${escapeHTML(lesson.subject)}</span>
<span class="subject">${withBreaks(lesson.subject)}</span>
</div>
${details ? `<div class="details">${escapeHTML(details)}</div>` : ""}
${lesson.note ? `<div class="lesson-note">${escapeHTML(lesson.note)}</div>` : ""}
${details ? `<div class="details">${withBreaks(details)}</div>` : ""}
${lesson.note ? `<div class="lesson-note">${withBreaks(lesson.note)}</div>` : ""}
</div>`;
}
@@ -490,6 +490,20 @@ function escapeHTML(s) {
return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
/**
* Экранирует текст и расставляет точки переноса <wbr>.
* Длинные ссылки (и вообще слова без пробелов) переносятся в любом браузере —
* не полагаемся на поддержку overflow-wrap: anywhere старыми движками.
*/
function withBreaks(text) {
const WBR = "\u0001"; // служебный маркер, в тексте таблицы не встречается
if (!text) return "";
const marked = String(text)
.replace(/([/?&=._\-])(?=\S)/g, "$1" + WBR) // после служебных символов ссылок
.replace(/(\S{24})(?=\S)/g, "$1" + WBR); // и не реже, чем каждые 24 символа
return escapeHTML(marked).split(WBR).join("<wbr>");
}
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);