commenting-visual-explainers/src/widget/index.ts
2026-03-20 13:30:40 +09:00

159 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Icons: Lucide (https://lucide.dev)
* ISC License - Copyright (c) Lucide Contributors 2026
*/
// document.currentScript は IIFE 先頭でキャプチャ必須esbuild の import 巻き上げ前に実行される)
const SCRIPT = document.currentScript as HTMLScriptElement | null;
const API_BASE = SCRIPT ? SCRIPT.src.replace(/\/widget\.js.*$/, '') : '';
const API_TOKEN = SCRIPT ? (SCRIPT.dataset.token || '') : '';
import type { Priority } from '../shared/types';
import { USERNAME_KEY, PRIORITY_CYCLE } from '../shared/constants';
import { generateId } from '../shared/slug';
import { initApi, api } from './api';
import { state, slug, type FbComment } from './state';
import { injectStyles } from './styles';
import { render, toggleSidebar, setRenderDeps } from './render/index';
import { setSidebarActions } from './render/sidebar';
import { applyHighlights } from './highlight';
import { scrollToQuote, scrollToCard } from './scroll';
import { setupTextSelection } from './selection';
initApi(API_BASE, API_TOKEN);
function loadComments(): Promise<void> {
return api('GET', { slug }).then((c) => {
if (Array.isArray(c)) state.comments = c;
render();
applyHighlights(onClickHighlight);
}).catch(() => {
render();
applyHighlights(onClickHighlight);
});
}
function closePopup(): void {
state.selectedText = '';
state.selectedRect = null;
state.popupContent = '';
state.popupPriority = 'must';
render();
}
function submitComment(priority: Priority): void {
if (!state.username || !state.selectedText) return;
const c: FbComment = {
id: generateId(), author: state.username, type: 'comment',
quote: state.selectedText, quoteContext: state.selectedQuoteContext,
content: state.popupContent.trim(),
priority, parentId: null, pageUrl: window.location.href,
projectSlug: slug, timestamp: Date.now(),
resolved: false, resolvedBy: null, resolvedAt: null, updatedAt: null,
};
state.comments.push(c);
closePopup();
applyHighlights(onClickHighlight);
api('POST', c as unknown as Record<string, unknown>).then(loadComments);
}
function resolveComment(id: string): void {
const c = state.comments.find((x) => x.id === id);
if (!c) return;
const now = !c.resolved;
c.resolved = now;
c.resolvedBy = now ? state.username : null;
c.resolvedAt = now ? Date.now() : null;
render(); applyHighlights(onClickHighlight);
api('PUT', { id, action: 'resolve', resolved: now, resolvedBy: c.resolvedBy, resolvedAt: c.resolvedAt });
}
function cyclePriority(id: string): void {
const c = state.comments.find((x) => x.id === id);
if (!c || c.author !== state.username) return;
c.priority = PRIORITY_CYCLE[c.priority] || 'must';
render(); applyHighlights(onClickHighlight);
api('PUT', { id, action: 'cyclePriority', priority: c.priority });
}
function deleteComment(id: string): void {
state.comments = state.comments.filter((c) => c.id !== id && c.parentId !== id);
render(); applyHighlights(onClickHighlight);
api('DELETE', { id });
}
function deleteReply(id: string): void {
state.comments = state.comments.filter((c) => c.id !== id);
render();
api('DELETE', { id });
}
function saveEdit(id: string): void {
const c = state.comments.find((x) => x.id === id);
if (!c) return;
c.content = state.editContent;
c.priority = state.editPriority;
state.editingId = null;
render(); applyHighlights(onClickHighlight);
api('PUT', { id, action: 'edit', content: c.content, priority: c.priority });
}
function submitReply(parentId: string): void {
if (!state.replyText.trim() || !state.username) return;
const r: FbComment = {
id: generateId(), author: state.username, type: 'comment',
quote: '', quoteContext: { beforeText: '', afterText: '' },
content: state.replyText.trim(), priority: 'want',
parentId, pageUrl: window.location.href,
projectSlug: slug, timestamp: Date.now(),
resolved: false, resolvedBy: null, resolvedAt: null, updatedAt: null,
};
state.comments.push(r);
state.replyingTo = null;
state.replyText = '';
render();
api('POST', r as unknown as Record<string, unknown>);
}
function finishNameEdit(): void {
if (state.nameInput.trim() && state.nameInput.trim() !== state.username) {
const oldName = state.username;
state.username = state.nameInput.trim();
localStorage.setItem(USERNAME_KEY, state.username);
api('PUT', { id: '_rename', action: 'rename', author: state.username, oldAuthor: oldName, projectSlug: slug }).then(loadComments);
}
state.editingName = false;
render();
}
function onClickHighlight(id: string): void {
scrollToCard(id, toggleSidebar);
}
// Wire up dependencies before first render
setRenderDeps(closePopup, submitComment);
setSidebarActions({
toggleSidebar,
cyclePriority,
scrollToQuote,
resolveComment,
deleteComment,
deleteReply,
saveEdit,
submitReply,
finishNameEdit,
});
function init(): void {
injectStyles();
render();
setupTextSelection(render, closePopup);
loadComments();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}