Refine BIM comment modal controls

This commit is contained in:
CODEX 2026-06-20 17:40:15 +03:00
parent d70f6e0886
commit 405113fa4f
4 changed files with 256 additions and 67 deletions

View File

@ -1583,6 +1583,7 @@ header {
.comment-card-modal { .comment-card-modal {
position: fixed; position: fixed;
--env-control-h: 42px; --env-control-h: 42px;
--ops-comment-accent: var(--comment-target-active, #c4ff67);
--ops-detail-bg: rgba(18, 18, 20, 0.96); --ops-detail-bg: rgba(18, 18, 20, 0.96);
--ops-detail-surface: rgba(28, 28, 31, 0.76); --ops-detail-surface: rgba(28, 28, 31, 0.76);
--ops-detail-surface-strong: rgba(32, 32, 35, 0.92); --ops-detail-surface-strong: rgba(32, 32, 35, 0.92);
@ -1607,6 +1608,15 @@ header {
-webkit-backdrop-filter: blur(30px) saturate(1.08); -webkit-backdrop-filter: blur(30px) saturate(1.08);
} }
.comment-card-modal.is-expanded {
top: 16px !important;
left: 16px !important;
width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
height: calc(100vh - 32px);
border-radius: 26px;
}
.comment-card-modal.hidden { .comment-card-modal.hidden {
display: none; display: none;
} }
@ -1614,7 +1624,7 @@ header {
.comment-card-modal__header { .comment-card-modal__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-end;
min-height: 64px; min-height: 64px;
padding: 20px 28px 8px; padding: 20px 28px 8px;
cursor: grab; cursor: grab;
@ -1794,6 +1804,14 @@ header {
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);
} }
.bim-subitem-menu {
z-index: 30070;
display: grid;
gap: 0.18rem;
min-width: 17.25rem;
max-width: min(22rem, calc(100vw - 24px));
}
.ops-comment-subitems, .ops-comment-subitems,
.comment-attachments, .comment-attachments,
.comment-thread { .comment-thread {
@ -1830,13 +1848,19 @@ header {
background: rgba(255, 255, 255, 0.026); background: rgba(255, 255, 255, 0.026);
} }
.ops-comment-subitem[data-kind="text"] {
grid-template-columns: minmax(0, 1fr) 28px;
align-items: start;
}
.ops-comment-subitem input[type="checkbox"] { .ops-comment-subitem input[type="checkbox"] {
width: 16px; width: 16px;
height: 16px; height: 16px;
accent-color: var(--accent); accent-color: var(--ops-comment-accent);
} }
.ops-comment-subitem input[type="text"] { .ops-comment-subitem input[type="text"],
.ops-comment-subitem textarea {
min-width: 0; min-width: 0;
border: 0; border: 0;
background: transparent; background: transparent;
@ -1846,6 +1870,12 @@ header {
outline: none; outline: none;
} }
.ops-comment-subitem textarea {
min-height: 84px;
line-height: 1.45;
resize: vertical;
}
.ops-comment-subitem button { .ops-comment-subitem button {
display: grid; display: grid;
place-items: center; place-items: center;
@ -1877,12 +1907,12 @@ header {
} }
.ops-detail-dropzone.drag-over { .ops-detail-dropzone.drag-over {
background: color-mix(in srgb, var(--accent) 18%, rgba(255, 255, 255, 0.04)); background: color-mix(in srgb, var(--ops-comment-accent) 18%, rgba(255, 255, 255, 0.04));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 44%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ops-comment-accent) 44%, transparent);
} }
.ops-detail-dropzone__icon { .ops-detail-dropzone__icon {
color: var(--accent); color: var(--ops-comment-accent);
} }
.ops-detail-dropzone__copy { .ops-detail-dropzone__copy {
@ -1909,7 +1939,7 @@ header {
border: 0; border: 0;
border-radius: 13px; border-radius: 13px;
background: transparent; background: transparent;
color: var(--accent); color: var(--ops-comment-accent);
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
@ -2121,8 +2151,8 @@ header {
.comment-card-modal__footer { .comment-card-modal__footer {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
display: flex; display: grid;
justify-content: flex-end; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px; gap: 10px;
padding: 14px 32px 18px; padding: 14px 32px 18px;
background: var(--ops-detail-bg); background: var(--ops-detail-bg);
@ -2131,7 +2161,17 @@ header {
.comment-card-modal__footer .primary-btn, .comment-card-modal__footer .primary-btn,
.comment-card-modal__footer .secondary-btn { .comment-card-modal__footer .secondary-btn {
min-width: 148px; min-width: 0;
}
.comment-card-modal .primary-btn {
background: var(--ops-comment-accent);
color: var(--toolbar-bg-active-contrast, #15170f);
box-shadow: 0 8px 24px color-mix(in srgb, var(--ops-comment-accent) 24%, transparent);
}
.comment-card-modal .primary-btn:not(:disabled):hover {
box-shadow: 0 10px 30px color-mix(in srgb, var(--ops-comment-accent) 30%, transparent);
} }
.comment-card-modal .error-text { .comment-card-modal .error-text {

View File

@ -56,6 +56,7 @@ const commentModalBackdrop = document.getElementById("commentModalBackdrop");
const commentModal = document.getElementById("commentModal"); const commentModal = document.getElementById("commentModal");
const commentModalHeader = document.getElementById("commentModalHeader"); const commentModalHeader = document.getElementById("commentModalHeader");
const commentModalClose = document.getElementById("commentModalClose"); const commentModalClose = document.getElementById("commentModalClose");
const commentModalExpandButton = document.getElementById("commentModalExpandButton");
const commentModalSubtitle = document.getElementById("commentModalSubtitle"); const commentModalSubtitle = document.getElementById("commentModalSubtitle");
const commentModalEditedMeta = document.getElementById("commentModalEditedMeta"); const commentModalEditedMeta = document.getElementById("commentModalEditedMeta");
const commentTitleInput = document.getElementById("commentTitleInput"); const commentTitleInput = document.getElementById("commentTitleInput");
@ -67,6 +68,7 @@ const commentAttachmentGrid = document.getElementById("commentAttachmentGrid");
const commentSubitemsSection = document.getElementById("commentSubitemsSection"); const commentSubitemsSection = document.getElementById("commentSubitemsSection");
const commentSubitemsList = document.getElementById("commentSubitemsList"); const commentSubitemsList = document.getElementById("commentSubitemsList");
const commentAddSubitemButton = document.getElementById("commentAddSubitemButton"); const commentAddSubitemButton = document.getElementById("commentAddSubitemButton");
const commentSubitemMenu = document.getElementById("commentSubitemMenu");
const commentThreadList = document.getElementById("commentThreadList"); const commentThreadList = document.getElementById("commentThreadList");
const commentReplyInput = document.getElementById("commentReplyInput"); const commentReplyInput = document.getElementById("commentReplyInput");
const commentReplyAttachButton = document.getElementById("commentReplyAttachButton"); const commentReplyAttachButton = document.getElementById("commentReplyAttachButton");
@ -75,6 +77,7 @@ const commentReplyAttachmentGrid = document.getElementById("commentReplyAttachme
const commentModalError = document.getElementById("commentModalError"); const commentModalError = document.getElementById("commentModalError");
const commentCancelButton = document.getElementById("commentCancelButton"); const commentCancelButton = document.getElementById("commentCancelButton");
const commentApplyButton = document.getElementById("commentApplyButton"); const commentApplyButton = document.getElementById("commentApplyButton");
const commentSaveButton = document.getElementById("commentSaveButton");
const commentApplyInlineButton = document.getElementById("commentApplyInlineButton"); const commentApplyInlineButton = document.getElementById("commentApplyInlineButton");
const treeContainer = document.getElementById("treeContainer"); const treeContainer = document.getElementById("treeContainer");
const displayModeControl = document.getElementById("displayModeControl"); const displayModeControl = document.getElementById("displayModeControl");
@ -299,6 +302,7 @@ let commentSourcesLoaded = new Set();
let commentContextTarget = null; let commentContextTarget = null;
let commentModalMode = "create"; let commentModalMode = "create";
let commentModalCommentId = null; let commentModalCommentId = null;
let commentModalExpanded = false;
let activeCommentId = null; let activeCommentId = null;
let hoverCommentId = null; let hoverCommentId = null;
let commentDraftAttachments = []; let commentDraftAttachments = [];
@ -1058,27 +1062,94 @@ const renderModalAttachmentInputs = () => {
}); });
}; };
const setCommentModalExpanded = (expanded) => {
commentModalExpanded = !!expanded;
commentModal?.classList.toggle("is-expanded", commentModalExpanded);
if (commentModalExpandButton) {
commentModalExpandButton.setAttribute("aria-label", commentModalExpanded ? "Свернуть" : "Развернуть");
commentModalExpandButton.title = commentModalExpanded ? "Свернуть" : "Развернуть";
}
};
const hideCommentSubitemMenu = () => {
commentSubitemMenu?.classList.add("hidden");
};
const showCommentSubitemMenu = () => {
if (!commentSubitemMenu || !commentAddSubitemButton) return;
commentSubitemMenu.classList.remove("hidden");
const buttonRect = commentAddSubitemButton.getBoundingClientRect();
const width = commentSubitemMenu.offsetWidth || 276;
const height = commentSubitemMenu.offsetHeight || 94;
const x = Math.max(8, Math.min(window.innerWidth - width - 8, buttonRect.left + (buttonRect.width - width) / 2));
const y = Math.max(8, Math.min(window.innerHeight - height - 8, buttonRect.bottom + 8));
commentSubitemMenu.style.left = `${x}px`;
commentSubitemMenu.style.top = `${y}px`;
};
const toggleCommentSubitemMenu = () => {
if (!commentSubitemMenu) return;
if (commentSubitemMenu.classList.contains("hidden")) {
showCommentSubitemMenu();
} else {
hideCommentSubitemMenu();
}
};
const addCommentSubitem = (kind) => {
const nextKind = kind === "text" ? "text" : "checker";
commentDraftSubitems.push({
id: `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`,
kind: nextKind,
title: "",
body: "",
done: false,
});
renderCommentSubitems();
window.requestAnimationFrame(() => {
const selector = nextKind === "text"
? ".ops-comment-subitem[data-kind='text'] textarea"
: ".ops-comment-subitem[data-kind='checker'] input[type='text']";
const inputs = commentSubitemsList?.querySelectorAll?.(selector);
inputs?.[inputs.length - 1]?.focus?.();
});
};
const renderCommentSubitems = () => { const renderCommentSubitems = () => {
if (!commentSubitemsList || !commentSubitemsSection) return; if (!commentSubitemsList || !commentSubitemsSection) return;
commentSubitemsList.replaceChildren(); commentSubitemsList.replaceChildren();
const subitems = Array.isArray(commentDraftSubitems) ? commentDraftSubitems : []; const subitems = Array.isArray(commentDraftSubitems) ? commentDraftSubitems : [];
commentSubitemsSection.classList.toggle("hidden", !subitems.length); commentSubitemsSection.classList.toggle("hidden", !subitems.length);
subitems.forEach((subitem, index) => { subitems.forEach((subitem, index) => {
const row = document.createElement("label"); const kind = subitem.kind === "text" ? "text" : "checker";
const row = document.createElement(kind === "text" ? "div" : "label");
row.className = "ops-comment-subitem"; row.className = "ops-comment-subitem";
row.dataset.kind = kind;
let editor = null;
if (kind === "text") {
editor = document.createElement("textarea");
editor.rows = 3;
editor.value = subitem.body || subitem.title || "";
editor.placeholder = "Текстовый блок";
editor.addEventListener("input", () => {
commentDraftSubitems[index] = { ...commentDraftSubitems[index], kind, body: editor.value, title: editor.value };
});
} else {
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
checkbox.checked = !!subitem.done; checkbox.checked = !!subitem.done;
checkbox.addEventListener("change", () => { checkbox.addEventListener("change", () => {
commentDraftSubitems[index] = { ...commentDraftSubitems[index], done: checkbox.checked }; commentDraftSubitems[index] = { ...commentDraftSubitems[index], kind, done: checkbox.checked };
}); });
const input = document.createElement("input"); editor = document.createElement("input");
input.type = "text"; editor.type = "text";
input.value = subitem.title || ""; editor.value = subitem.title || subitem.body || "";
input.placeholder = "Новый подэлемент"; editor.placeholder = "Новый чекер";
input.addEventListener("input", () => { editor.addEventListener("input", () => {
commentDraftSubitems[index] = { ...commentDraftSubitems[index], title: input.value }; commentDraftSubitems[index] = { ...commentDraftSubitems[index], kind, title: editor.value };
}); });
row.appendChild(checkbox);
}
const remove = document.createElement("button"); const remove = document.createElement("button");
remove.type = "button"; remove.type = "button";
remove.setAttribute("aria-label", "Удалить подэлемент"); remove.setAttribute("aria-label", "Удалить подэлемент");
@ -1087,13 +1158,14 @@ const renderCommentSubitems = () => {
commentDraftSubitems.splice(index, 1); commentDraftSubitems.splice(index, 1);
renderCommentSubitems(); renderCommentSubitems();
}); });
row.append(checkbox, input, remove); row.append(editor, remove);
commentSubitemsList.appendChild(row); commentSubitemsList.appendChild(row);
}); });
}; };
const openCommentModalForTarget = () => { const openCommentModalForTarget = () => {
if (!commentContextTarget || !commentModal) return; if (!commentContextTarget || !commentModal) return;
hideCommentSubitemMenu();
commentsModeActive = true; commentsModeActive = true;
commentsCollapsed = false; commentsCollapsed = false;
commentModalMode = "create"; commentModalMode = "create";
@ -1124,6 +1196,8 @@ const openCommentModalForTarget = () => {
const openCommentModalForComment = (commentId) => { const openCommentModalForComment = (commentId) => {
const comment = commentRecords.find((item) => item.id === commentId); const comment = commentRecords.find((item) => item.id === commentId);
if (!comment || !commentModal) return; if (!comment || !commentModal) return;
hideCommentSubitemMenu();
commentContextTarget = null;
commentModalMode = "edit"; commentModalMode = "edit";
commentModalCommentId = comment.id; commentModalCommentId = comment.id;
activeCommentId = comment.id; activeCommentId = comment.id;
@ -1150,8 +1224,10 @@ const openCommentModalForComment = (commentId) => {
}; };
const closeCommentModal = () => { const closeCommentModal = () => {
hideCommentSubitemMenu();
commentModalBackdrop?.classList.add("hidden"); commentModalBackdrop?.classList.add("hidden");
commentModal?.classList.add("hidden"); commentModal?.classList.add("hidden");
setCommentModalExpanded(false);
if (commentModalMode === "create") { if (commentModalMode === "create") {
commentContextTarget = null; commentContextTarget = null;
} }
@ -1165,12 +1241,18 @@ const closeCommentModal = () => {
const normalizedSubitemsDraft = () => ( const normalizedSubitemsDraft = () => (
(commentDraftSubitems || []) (commentDraftSubitems || [])
.map((item) => ({ .map((item) => {
const kind = item.kind === "text" ? "text" : "checker";
const content = String(kind === "text" ? (item.body || item.title || "") : (item.title || item.body || "")).trim();
return {
id: item.id || `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`, id: item.id || `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`,
title: String(item.title || "").trim(), kind,
done: !!item.done, title: content,
})) body: kind === "text" ? content : "",
.filter((item) => item.title) done: kind === "checker" && item.done === true,
};
})
.filter((item) => item.title || item.body)
); );
const buildCommentPayload = ({ title, body, replyBody } = {}) => { const buildCommentPayload = ({ title, body, replyBody } = {}) => {
@ -1184,7 +1266,32 @@ const buildCommentPayload = ({ title, body, replyBody } = {}) => {
}; };
}; };
const applyCommentModal = async () => { const syncCommentModalAfterSave = (comment) => {
if (!comment) return;
commentModalMode = "edit";
commentModalCommentId = comment.id;
commentContextTarget = null;
activeCommentId = comment.id;
commentTitleInput && (commentTitleInput.value = comment.title || "");
commentBodyInput && (commentBodyInput.value = comment.body || "");
commentReplyInput && (commentReplyInput.value = "");
commentDraftAttachments = [];
commentReplyAttachments = [];
commentDraftSubitems = Array.isArray(comment.subitems)
? comment.subitems.map((item) => ({ ...item }))
: [];
if (commentModalSubtitle) {
commentModalSubtitle.textContent = comment.id || "BIM-COMMENT";
}
if (commentModalEditedMeta) {
commentModalEditedMeta.textContent = `Последнее редактирование: ${formatDateTime(comment.updatedAt || comment.createdAt)}`;
}
renderModalAttachmentInputs();
renderCommentSubitems();
renderCommentThread(comment);
};
const applyCommentModal = async ({ closeOnSave = false } = {}) => {
if (isGuestMode()) return; if (isGuestMode()) return;
const title = commentTitleInput?.value?.trim() || ""; const title = commentTitleInput?.value?.trim() || "";
const body = commentBodyInput?.value?.trim() || ""; const body = commentBodyInput?.value?.trim() || "";
@ -1197,6 +1304,10 @@ const applyCommentModal = async () => {
commentApplyButton.disabled = true; commentApplyButton.disabled = true;
commentApplyButton.textContent = "Сохранение..."; commentApplyButton.textContent = "Сохранение...";
} }
if (commentSaveButton) {
commentSaveButton.disabled = true;
commentSaveButton.textContent = "Сохранение...";
}
if (commentApplyInlineButton) { if (commentApplyInlineButton) {
commentApplyInlineButton.disabled = true; commentApplyInlineButton.disabled = true;
} }
@ -1211,18 +1322,23 @@ const applyCommentModal = async () => {
} else if (commentModalCommentId) { } else if (commentModalCommentId) {
const original = commentRecords.find((item) => item.id === commentModalCommentId); const original = commentRecords.find((item) => item.id === commentModalCommentId);
const subitems = normalizedSubitemsDraft(); const subitems = normalizedSubitemsDraft();
const messageAttachments = [...commentDraftAttachments, ...commentReplyAttachments].slice(0, 8);
const patchPayload = {}; const patchPayload = {};
if (!original || title !== (original.title || "")) patchPayload.title = title; if (!original || title !== (original.title || "")) patchPayload.title = title;
if (!original || body !== (original.body || "")) patchPayload.body = body; if (!original || body !== (original.body || "")) patchPayload.body = body;
if (JSON.stringify(subitems) !== JSON.stringify(original?.subitems || [])) patchPayload.subitems = subitems; if (JSON.stringify(subitems) !== JSON.stringify(original?.subitems || [])) patchPayload.subitems = subitems;
if (replyBody || commentReplyAttachments.length) { if (replyBody || messageAttachments.length) {
patchPayload.message = { patchPayload.message = {
body: replyBody, body: replyBody,
attachments: commentReplyAttachments, attachments: messageAttachments,
}; };
} }
if (!Object.keys(patchPayload).length) { if (!Object.keys(patchPayload).length) {
if (closeOnSave) {
closeCommentModal(); closeCommentModal();
} else {
setStatus("Изменений нет");
}
return; return;
} }
payload = await fetchJSON(`${COMMENTS_API}/${encodeURIComponent(commentModalCommentId)}`, { payload = await fetchJSON(`${COMMENTS_API}/${encodeURIComponent(commentModalCommentId)}`, {
@ -1238,7 +1354,10 @@ const applyCommentModal = async () => {
commentContextTarget = null; commentContextTarget = null;
commentsModeActive = true; commentsModeActive = true;
commentsCollapsed = false; commentsCollapsed = false;
syncCommentModalAfterSave(saved);
if (closeOnSave) {
closeCommentModal(); closeCommentModal();
}
updateCommentsVisibility(); updateCommentsVisibility();
setStatus("Комментарий сохранён"); setStatus("Комментарий сохранён");
} }
@ -1250,6 +1369,10 @@ const applyCommentModal = async () => {
commentApplyButton.disabled = false; commentApplyButton.disabled = false;
commentApplyButton.textContent = "Применить"; commentApplyButton.textContent = "Применить";
} }
if (commentSaveButton) {
commentSaveButton.disabled = false;
commentSaveButton.textContent = "Сохранить";
}
if (commentApplyInlineButton) { if (commentApplyInlineButton) {
commentApplyInlineButton.disabled = false; commentApplyInlineButton.disabled = false;
} }
@ -5076,22 +5199,32 @@ const loadProjectSnapshot = async (project) => {
}); });
commentModalClose?.addEventListener("click", closeCommentModal); commentModalClose?.addEventListener("click", closeCommentModal);
commentCancelButton?.addEventListener("click", closeCommentModal); commentCancelButton?.addEventListener("click", closeCommentModal);
commentApplyButton?.addEventListener("click", applyCommentModal); commentModalExpandButton?.addEventListener("click", () => setCommentModalExpanded(!commentModalExpanded));
commentApplyInlineButton?.addEventListener("click", applyCommentModal); commentApplyButton?.addEventListener("click", () => applyCommentModal({ closeOnSave: false }));
commentSaveButton?.addEventListener("click", () => applyCommentModal({ closeOnSave: true }));
commentApplyInlineButton?.addEventListener("click", () => applyCommentModal({ closeOnSave: false }));
commentModalBackdrop?.addEventListener("click", (event) => { commentModalBackdrop?.addEventListener("click", (event) => {
if (event.target === commentModalBackdrop) closeCommentModal(); if (event.target === commentModalBackdrop) closeCommentModal();
}); });
commentAttachButton?.addEventListener("click", () => commentAttachmentInput?.click()); commentAttachButton?.addEventListener("click", () => commentAttachmentInput?.click());
commentReplyAttachButton?.addEventListener("click", () => commentReplyAttachmentInput?.click()); commentReplyAttachButton?.addEventListener("click", () => commentReplyAttachmentInput?.click());
commentAddSubitemButton?.addEventListener("click", () => { commentAddSubitemButton?.addEventListener("click", (event) => {
commentDraftSubitems.push({ event.preventDefault();
id: `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`, event.stopPropagation();
title: "", toggleCommentSubitemMenu();
done: false,
}); });
renderCommentSubitems(); commentSubitemMenu?.addEventListener("click", (event) => {
const inputs = commentSubitemsList?.querySelectorAll?.(".ops-comment-subitem input[type='text']"); const button = event.target?.closest?.("[data-comment-subitem-kind]");
inputs?.[inputs.length - 1]?.focus?.(); if (!button) return;
event.preventDefault();
event.stopPropagation();
addCommentSubitem(button.dataset.commentSubitemKind);
hideCommentSubitemMenu();
});
document.addEventListener("click", (event) => {
if (!commentSubitemMenu || commentSubitemMenu.classList.contains("hidden")) return;
if (commentSubitemMenu.contains(event.target) || commentAddSubitemButton?.contains(event.target)) return;
hideCommentSubitemMenu();
}); });
commentAttachmentInput?.addEventListener("change", async (event) => { commentAttachmentInput?.addEventListener("change", async (event) => {
const files = await readAttachmentFiles(event.target.files); const files = await readAttachmentFiles(event.target.files);
@ -5306,6 +5439,12 @@ const loadProjectSnapshot = async (project) => {
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return; if (e.key !== "Escape") return;
if (commentSubitemMenu && !commentSubitemMenu.classList.contains("hidden")) {
hideCommentSubitemMenu();
e.preventDefault();
e.stopPropagation();
return;
}
if (commentModal && !commentModal.classList.contains("hidden")) { if (commentModal && !commentModal.classList.contains("hidden")) {
closeCommentModal(); closeCommentModal();
e.preventDefault(); e.preventDefault();

View File

@ -134,7 +134,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./dcViewer.css?v=25"> <link rel="stylesheet" href="./dcViewer.css?v=26">
</head> </head>
<body> <body>
<header> <header>
@ -295,18 +295,6 @@
<div id="commentModalBackdrop" class="modal-backdrop hidden"></div> <div id="commentModalBackdrop" class="modal-backdrop hidden"></div>
<div id="commentModal" class="comment-card-modal ops-comment-detail hidden" role="dialog" aria-modal="true" aria-label="Комментарий к модели"> <div id="commentModal" class="comment-card-modal ops-comment-detail hidden" role="dialog" aria-modal="true" aria-label="Комментарий к модели">
<div class="comment-card-modal__header" id="commentModalHeader"> <div class="comment-card-modal__header" id="commentModalHeader">
<div class="ops-detail-nav">
<button class="ops-detail-icon-btn" type="button" aria-label="Назад" title="Назад">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M14 7 9 12l5 5"></path>
</svg>
</button>
<button class="ops-detail-icon-btn" type="button" aria-label="Развернуть" title="Развернуть">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M14 5h5v5M19 5l-7 7M10 19H5v-5M5 19l7-7"></path>
</svg>
</button>
</div>
<div class="ops-detail-actions"> <div class="ops-detail-actions">
<button class="ops-detail-icon-btn" type="button" aria-label="Уведомления" title="Уведомления"> <button class="ops-detail-icon-btn" type="button" aria-label="Уведомления" title="Уведомления">
<svg viewBox="0 0 24 24" aria-hidden="true"> <svg viewBox="0 0 24 24" aria-hidden="true">
@ -314,9 +302,9 @@
<path d="M10 21h4"></path> <path d="M10 21h4"></path>
</svg> </svg>
</button> </button>
<button class="ops-detail-icon-btn" type="button" aria-label="Ещё" title="Ещё"> <button class="ops-detail-icon-btn" id="commentModalExpandButton" type="button" aria-label="Развернуть" title="Развернуть">
<svg viewBox="0 0 24 24" aria-hidden="true"> <svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 12h.01M18 12h.01M6 12h.01"></path> <path d="M14 5h5v5M19 5l-7 7M10 19H5v-5M5 19l7-7"></path>
</svg> </svg>
</button> </button>
<button class="ops-detail-icon-btn modal-close" id="commentModalClose" type="button" aria-label="Закрыть" title="Закрыть"> <button class="ops-detail-icon-btn modal-close" id="commentModalClose" type="button" aria-label="Закрыть" title="Закрыть">
@ -346,6 +334,21 @@
<span>Добавить подэлемент</span> <span>Добавить подэлемент</span>
</button> </button>
</div> </div>
<div id="commentSubitemMenu" class="nodedc-dropdown-surface bim-subitem-menu hidden" role="menu" aria-label="Тип подэлемента">
<button class="nodedc-dropdown-option" data-comment-subitem-kind="text" type="button" role="menuitem">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6h16M4 12h10M4 18h12"></path>
</svg>
<span>Создать текстовый блок</span>
</button>
<button class="nodedc-dropdown-option" data-comment-subitem-kind="checker" type="button" role="menuitem">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 11l2 2 4-5"></path>
<path d="M4 5h16M4 12h3M4 19h16"></path>
</svg>
<span>Создать чекер</span>
</button>
</div>
</section> </section>
<section class="ops-comment-subitems hidden" id="commentSubitemsSection"> <section class="ops-comment-subitems hidden" id="commentSubitemsSection">
@ -418,6 +421,7 @@
<div class="comment-card-modal__footer"> <div class="comment-card-modal__footer">
<button class="secondary-btn" id="commentCancelButton" type="button">Отменить</button> <button class="secondary-btn" id="commentCancelButton" type="button">Отменить</button>
<button class="primary-btn" id="commentApplyButton" type="button">Применить</button> <button class="primary-btn" id="commentApplyButton" type="button">Применить</button>
<button class="primary-btn" id="commentSaveButton" type="button">Сохранить</button>
</div> </div>
</div> </div>
<div id="saveProjectBackdrop" class="modal-backdrop hidden"></div> <div id="saveProjectBackdrop" class="modal-backdrop hidden"></div>
@ -569,6 +573,6 @@
<input id="fileInput" class="hidden" type="file" <input id="fileInput" class="hidden" type="file"
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl"> accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
<script type="module" src="./dcViewer.js?v=29"></script> <script type="module" src="./dcViewer.js?v=30"></script>
</body> </body>
</html> </html>

View File

@ -1606,14 +1606,20 @@ const sanitizeCommentSubitems = (value) => {
if (!item || typeof item !== "object") { if (!item || typeof item !== "object") {
return []; return [];
} }
const title = sanitizeCommentText(item.title ?? item.text ?? item.name, 300); const kind = item.kind === "text" ? "text" : "checker";
if (!title) { const rawContent = kind === "text"
? (item.body ?? item.title ?? item.text ?? item.name)
: (item.title ?? item.text ?? item.name ?? item.body);
const content = sanitizeCommentText(rawContent, kind === "text" ? 4000 : 300);
if (!content) {
return []; return [];
} }
return [{ return [{
id: stringOrNull(item.id) || `sub_${randomBase64Url(8)}`, id: stringOrNull(item.id) || `sub_${randomBase64Url(8)}`,
title, kind,
done: item.done === true, title: content,
body: kind === "text" ? content : "",
done: kind === "checker" && item.done === true,
createdAt: stringOrNull(item.createdAt) || nowIso() createdAt: stringOrNull(item.createdAt) || nowIso()
}]; }];
}); });