Refine BIM comment modal controls
This commit is contained in:
parent
d70f6e0886
commit
405113fa4f
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue