Merge branch 'feature/import-floating-window' into master
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
4829dce3a6
@ -156,13 +156,6 @@
|
|||||||
.window-shell:not([hidden]) {
|
.window-shell:not([hidden]) {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.window-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 39;
|
|
||||||
background: rgba(24, 28, 22, 0.42);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.window-frame {
|
.window-frame {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
@ -256,56 +249,8 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.collection-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.75rem 0.9rem;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
}
|
|
||||||
.collection-item input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.item-row-body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.title-button {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--ink);
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.title-button:hover {
|
|
||||||
transform: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.details-button {
|
|
||||||
width: 2.2rem;
|
|
||||||
min-width: 2.2rem;
|
|
||||||
padding: 0.45rem 0;
|
|
||||||
}
|
|
||||||
.collection-item .meta {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.item-year {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-width: 3rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.preview-popover {
|
.preview-popover {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
max-width: 28rem;
|
max-width: 28rem;
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@ -313,7 +258,6 @@
|
|||||||
background: rgba(255, 253, 248, 0.98);
|
background: rgba(255, 253, 248, 0.98);
|
||||||
box-shadow: 0 18px 40px rgba(35, 31, 21, 0.16);
|
box-shadow: 0 18px 40px rgba(35, 31, 21, 0.16);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
z-index: 60;
|
|
||||||
}
|
}
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -343,18 +287,17 @@
|
|||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.window-frame {
|
.modal-card {
|
||||||
width: 100vw;
|
width: calc(100vw - 1rem);
|
||||||
height: 100vh;
|
margin: 0.5rem auto;
|
||||||
left: 0 !important;
|
max-height: calc(100vh - 1rem);
|
||||||
top: 0 !important;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
}
|
||||||
.window-body {
|
.modal-body {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: minmax(12rem, 1fr) minmax(12rem, 1fr);
|
grid-template-rows: minmax(12rem, 1fr) minmax(12rem, 1fr);
|
||||||
}
|
}
|
||||||
.window-toolbar {
|
.modal-header,
|
||||||
|
.modal-action-bar {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -394,35 +337,44 @@
|
|||||||
<button id="open-import-modal-button" type="button">导入文献</button>
|
<button id="open-import-modal-button" type="button">导入文献</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="import-modal" class="window-shell" hidden aria-hidden="true">
|
<div id="import-window-shell" class="window-shell" hidden aria-hidden="true">
|
||||||
<div id="import-modal-overlay" class="window-overlay" type="button" aria-label="关闭导入弹窗"></div>
|
<section id="import-window" class="window-frame" role="dialog" aria-modal="true" aria-labelledby="import-window-title">
|
||||||
<section class="window-frame" role="dialog" aria-modal="true" aria-labelledby="import-modal-title">
|
<div class="window-toolbar" id="import-window-toolbar">
|
||||||
<div class="window-toolbar">
|
<div style="display:flex;align-items:center;gap:0.6rem">
|
||||||
<button id="close-import-modal-button" type="button" class="secondary">返回主页面</button>
|
<span style="font-size:1.2rem;line-height:1">☰</span>
|
||||||
<div>
|
<div>
|
||||||
<h3 id="import-modal-title">Import From Zotero</h3>
|
<h3 id="import-window-title">Import From Zotero</h3>
|
||||||
<p id="import-modal-project-label" class="meta">未选择项目</p>
|
<p id="import-window-project-label" class="meta">未选择项目</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-toolbar-actions">
|
<div class="window-toolbar-actions">
|
||||||
<div id="modal-selected-count" class="meta">已选 0 篇</div>
|
<div id="window-selected-count" class="meta">已选 0 篇</div>
|
||||||
<button id="modal-clear-selection-button" type="button" class="danger">清空选择</button>
|
<button id="window-clear-selection-button" type="button" class="danger">清空选择</button>
|
||||||
<button id="modal-import-selected-items-button" type="button">导入所选</button>
|
<button id="window-import-selected-items-button" type="button">导入所选</button>
|
||||||
|
<button id="window-minimize-button" type="button" class="secondary" title="最小化" style="width:auto;padding:0.55rem 0.75rem">_</button>
|
||||||
|
<button id="window-maximize-button" type="button" class="secondary" title="最大化" style="width:auto;padding:0.55rem 0.75rem">□</button>
|
||||||
|
<button id="window-close-button" type="button" class="secondary" title="关闭" style="width:auto;padding:0.55rem 0.75rem">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-body">
|
<div class="window-body">
|
||||||
<div class="window-pane collections">
|
<div class="window-pane collections">
|
||||||
<small>Collections</small>
|
<small>Collections</small>
|
||||||
<div id="collection-status" class="status"></div>
|
<div id="window-collection-status" class="status"></div>
|
||||||
<div id="modal-collection-tree" class="window-scroll tree-list"></div>
|
<div id="window-collection-tree" class="window-scroll tree-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-pane items">
|
<div class="window-pane items">
|
||||||
<small>Collection Items</small>
|
<small>Collection Items</small>
|
||||||
<div id="modal-collection-items" class="window-scroll collection-items-list"></div>
|
<div id="window-collection-items" class="window-scroll collection-items-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="item-preview-popover" class="preview-popover" hidden></div>
|
<div id="item-preview-popover" class="preview-popover" hidden></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="import-window-minimized-bar" class="window-minimized-bar" hidden>
|
||||||
|
<span style="flex:1;text-align:left">导入文献</span>
|
||||||
|
<button id="window-restore-button" type="button" class="secondary" style="width:auto;padding:0.3rem 0.5rem;font-size:0.8rem">□</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@ -472,9 +424,22 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ABSTRACT_PREVIEW_DELAY_MS = 3000;
|
const WINDOW_SESSION_STORAGE_KEY = "zotero-kb.import-window";
|
||||||
const IMPORT_SESSION_STORAGE_KEY = "zotero-kb.import-modal";
|
const IMPORT_SESSION_STORAGE_KEY = "zotero-kb.import-modal";
|
||||||
|
|
||||||
|
function readWindowSessionState() {
|
||||||
|
try {
|
||||||
|
const raw = window.sessionStorage.getItem(WINDOW_SESSION_STORAGE_KEY);
|
||||||
|
if (!raw) return {};
|
||||||
|
const payload = JSON.parse(raw);
|
||||||
|
return typeof payload === "object" && payload ? payload : {};
|
||||||
|
} catch (_error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowSessionState = readWindowSessionState();
|
||||||
|
|
||||||
function readImportSessionState() {
|
function readImportSessionState() {
|
||||||
try {
|
try {
|
||||||
const raw = window.sessionStorage.getItem(IMPORT_SESSION_STORAGE_KEY);
|
const raw = window.sessionStorage.getItem(IMPORT_SESSION_STORAGE_KEY);
|
||||||
@ -490,27 +455,12 @@
|
|||||||
|
|
||||||
const importSessionState = readImportSessionState();
|
const importSessionState = readImportSessionState();
|
||||||
|
|
||||||
const WINDOW_SESSION_STORAGE_KEY = "zotero-kb.import-window";
|
|
||||||
|
|
||||||
function readWindowSessionState() {
|
|
||||||
try {
|
|
||||||
const raw = window.sessionStorage.getItem(WINDOW_SESSION_STORAGE_KEY);
|
|
||||||
if (!raw) return {};
|
|
||||||
const payload = JSON.parse(raw);
|
|
||||||
return typeof payload === "object" && payload ? payload : {};
|
|
||||||
} catch (_error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowSessionState = readWindowSessionState();
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
projects: [],
|
projects: [],
|
||||||
currentProjectId: null,
|
currentProjectId: null,
|
||||||
currentCards: [],
|
currentCards: [],
|
||||||
collectionTree: [],
|
collectionTree: [],
|
||||||
isImportModalOpen: false,
|
isImportWindowOpen: false,
|
||||||
isImportWindowOpen: false,
|
isImportWindowOpen: false,
|
||||||
windowX: typeof windowSessionState.x === "number" ? windowSessionState.x : null,
|
windowX: typeof windowSessionState.x === "number" ? windowSessionState.x : null,
|
||||||
windowY: typeof windowSessionState.y === "number" ? windowSessionState.y : null,
|
windowY: typeof windowSessionState.y === "number" ? windowSessionState.y : null,
|
||||||
@ -541,9 +491,6 @@
|
|||||||
loadingCollectionKey: null,
|
loadingCollectionKey: null,
|
||||||
nextCollectionItemsRequestToken: 0,
|
nextCollectionItemsRequestToken: 0,
|
||||||
activeCollectionItemsRequestToken: 0,
|
activeCollectionItemsRequestToken: 0,
|
||||||
hoveredItemKey: null,
|
|
||||||
hoverTimerId: null,
|
|
||||||
previewItemKey: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
@ -553,12 +500,12 @@
|
|||||||
importWindowShell: document.getElementById("import-window-shell"),
|
importWindowShell: document.getElementById("import-window-shell"),
|
||||||
importWindow: document.getElementById("import-window"),
|
importWindow: document.getElementById("import-window"),
|
||||||
windowToolbar: document.getElementById("import-window-toolbar"),
|
windowToolbar: document.getElementById("import-window-toolbar"),
|
||||||
|
openImportModalButton: document.getElementById("open-import-modal-button"),
|
||||||
windowMinimizeButton: document.getElementById("window-minimize-button"),
|
windowMinimizeButton: document.getElementById("window-minimize-button"),
|
||||||
windowMaximizeButton: document.getElementById("window-maximize-button"),
|
windowMaximizeButton: document.getElementById("window-maximize-button"),
|
||||||
windowCloseButton: document.getElementById("window-close-button"),
|
windowCloseButton: document.getElementById("window-close-button"),
|
||||||
windowRestoreButton: document.getElementById("window-restore-button"),
|
windowRestoreButton: document.getElementById("window-restore-button"),
|
||||||
importWindowMinimizedBar: document.getElementById("import-window-minimized-bar"),
|
importWindowMinimizedBar: document.getElementById("import-window-minimized-bar"),
|
||||||
openImportModalButton: document.getElementById("open-import-modal-button"),
|
|
||||||
windowProjectLabel: document.getElementById("import-window-project-label"),
|
windowProjectLabel: document.getElementById("import-window-project-label"),
|
||||||
windowSelectedCount: document.getElementById("window-selected-count"),
|
windowSelectedCount: document.getElementById("window-selected-count"),
|
||||||
windowClearSelectionButton: document.getElementById("window-clear-selection-button"),
|
windowClearSelectionButton: document.getElementById("window-clear-selection-button"),
|
||||||
@ -566,7 +513,6 @@
|
|||||||
windowCollectionStatus: document.getElementById("window-collection-status"),
|
windowCollectionStatus: document.getElementById("window-collection-status"),
|
||||||
windowCollectionTree: document.getElementById("window-collection-tree"),
|
windowCollectionTree: document.getElementById("window-collection-tree"),
|
||||||
windowCollectionItems: document.getElementById("window-collection-items"),
|
windowCollectionItems: document.getElementById("window-collection-items"),
|
||||||
previewPopover: document.getElementById("item-preview-popover"),
|
|
||||||
currentProjectLabel: document.getElementById("current-project-label"),
|
currentProjectLabel: document.getElementById("current-project-label"),
|
||||||
cardList: document.getElementById("card-list"),
|
cardList: document.getElementById("card-list"),
|
||||||
cardDetail: document.getElementById("card-detail"),
|
cardDetail: document.getElementById("card-detail"),
|
||||||
@ -616,6 +562,110 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyWindowPositionAndSize() {
|
||||||
|
const fw = elements.importWindow;
|
||||||
|
if (!fw) return;
|
||||||
|
fw.style.left = state.windowX !== null ? `${state.windowX}px` : "";
|
||||||
|
fw.style.top = state.windowY !== null ? `${state.windowY}px` : "";
|
||||||
|
fw.style.width = state.windowWidth !== null ? `${state.windowWidth}px` : "";
|
||||||
|
fw.style.height = state.windowHeight !== null ? `${state.windowHeight}px` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWindowFromSession() {
|
||||||
|
const fw = elements.importWindow;
|
||||||
|
const bar = elements.importWindowMinimizedBar;
|
||||||
|
if (state.isWindowMinimized) {
|
||||||
|
fw.classList.add("minimized");
|
||||||
|
fw.classList.remove("maximized");
|
||||||
|
bar.hidden = false;
|
||||||
|
} else if (state.isWindowMaximized) {
|
||||||
|
fw.classList.add("maximized");
|
||||||
|
fw.classList.remove("minimized");
|
||||||
|
fw.style.width = "100vw";
|
||||||
|
fw.style.height = "100vh";
|
||||||
|
fw.style.left = "0";
|
||||||
|
fw.style.top = "0";
|
||||||
|
bar.hidden = true;
|
||||||
|
} else {
|
||||||
|
if (state.windowX !== null) {
|
||||||
|
applyWindowPositionAndSize();
|
||||||
|
}
|
||||||
|
fw.classList.remove("minimized", "maximized");
|
||||||
|
bar.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWindowDrag(event) {
|
||||||
|
const fw = elements.importWindow;
|
||||||
|
if (!fw || fw.classList.contains("maximized") || fw.classList.contains("minimized")) return;
|
||||||
|
event.preventDefault();
|
||||||
|
state.isWindowDragging = true;
|
||||||
|
state.windowDragStartX = event.clientX;
|
||||||
|
state.windowDragStartY = event.clientY;
|
||||||
|
state.windowDragStartLeft = parseFloat(fw.style.left) || 0;
|
||||||
|
state.windowDragStartTop = parseFloat(fw.style.top) || 0;
|
||||||
|
fw.style.transition = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveWindowDrag(event) {
|
||||||
|
if (!state.isWindowDragging) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const fw = elements.importWindow;
|
||||||
|
const dx = event.clientX - state.windowDragStartX;
|
||||||
|
const dy = event.clientY - state.windowDragStartY;
|
||||||
|
let newLeft = state.windowDragStartLeft + dx;
|
||||||
|
let newTop = state.windowDragStartTop + dy;
|
||||||
|
const maxLeft = window.innerWidth - (parseFloat(fw.style.width) || fw.offsetWidth);
|
||||||
|
const maxTop = window.innerHeight - (parseFloat(fw.style.height) || fw.offsetHeight);
|
||||||
|
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
||||||
|
newTop = Math.max(0, Math.min(newTop, maxTop));
|
||||||
|
fw.style.left = `${newLeft}px`;
|
||||||
|
fw.style.top = `${newTop}px`;
|
||||||
|
state.windowX = newLeft;
|
||||||
|
state.windowY = newTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endWindowDrag() {
|
||||||
|
if (!state.isWindowDragging) return;
|
||||||
|
state.isWindowDragging = false;
|
||||||
|
persistWindowSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function minimizeWindow() {
|
||||||
|
state.isWindowMinimized = true;
|
||||||
|
state.isWindowMaximized = false;
|
||||||
|
elements.importWindow.classList.add("minimized");
|
||||||
|
elements.importWindow.classList.remove("maximized");
|
||||||
|
elements.importWindowMinimizedBar.hidden = false;
|
||||||
|
persistWindowSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function maximizeWindow() {
|
||||||
|
state.isWindowMaximized = !state.isWindowMaximized;
|
||||||
|
if (state.isWindowMaximized) {
|
||||||
|
state.isWindowMinimized = false;
|
||||||
|
elements.importWindow.classList.add("maximized");
|
||||||
|
elements.importWindow.classList.remove("minimized");
|
||||||
|
elements.importWindowMinimizedBar.hidden = true;
|
||||||
|
elements.importWindow.style.width = "100vw";
|
||||||
|
elements.importWindow.style.height = "100vh";
|
||||||
|
elements.importWindow.style.left = "0";
|
||||||
|
elements.importWindow.style.top = "0";
|
||||||
|
} else {
|
||||||
|
elements.importWindow.classList.remove("maximized");
|
||||||
|
applyWindowPositionAndSize();
|
||||||
|
}
|
||||||
|
persistWindowSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreWindow() {
|
||||||
|
state.isWindowMinimized = false;
|
||||||
|
elements.importWindow.classList.remove("minimized");
|
||||||
|
elements.importWindowMinimizedBar.hidden = true;
|
||||||
|
applyWindowPositionAndSize();
|
||||||
|
persistWindowSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
function setStatus(target, message, isError = false) {
|
function setStatus(target, message, isError = false) {
|
||||||
target.textContent = message || "";
|
target.textContent = message || "";
|
||||||
target.classList.toggle("error", Boolean(isError));
|
target.classList.toggle("error", Boolean(isError));
|
||||||
@ -661,7 +711,7 @@
|
|||||||
function renderCards(cards) {
|
function renderCards(cards) {
|
||||||
state.currentCards = cards || [];
|
state.currentCards = cards || [];
|
||||||
if (!state.currentCards.length) {
|
if (!state.currentCards.length) {
|
||||||
elements.cardList.innerHTML = '<div class="empty">当前项目还没有卡片。先从 Zotero 导入文献。</div>';
|
elements.cardList.innerHTML = '<div class="empty">当前项目还没有卡片。先导入 Zotero 选中文献。</div>';
|
||||||
elements.cardDetail.innerHTML = '<div class="empty">导入后点击卡片查看详情。</div>';
|
elements.cardDetail.innerHTML = '<div class="empty">导入后点击卡片查看详情。</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -731,75 +781,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectedCount() {
|
function renderSelectedCount() {
|
||||||
elements.selectedCount.textContent = `已选 ${state.selectedItemKeys.size} 篇`;
|
elements.windowSelectedCount.textContent = `已选 ${state.selectedItemKeys.size} 篇`;
|
||||||
}
|
|
||||||
|
|
||||||
function clearAbstractPreviewTimer() {
|
|
||||||
if (state.hoverTimerId !== null) {
|
|
||||||
window.clearTimeout(state.hoverTimerId);
|
|
||||||
state.hoverTimerId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelAbstractPreview() {
|
|
||||||
clearAbstractPreviewTimer();
|
|
||||||
state.hoveredItemKey = null;
|
|
||||||
state.previewItemKey = null;
|
|
||||||
elements.previewPopover.hidden = true;
|
|
||||||
elements.previewPopover.setAttribute("aria-hidden", "true");
|
|
||||||
elements.previewPopover.textContent = "";
|
|
||||||
elements.previewPopover.style.top = "";
|
|
||||||
elements.previewPopover.style.left = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function positionAbstractPreview(target) {
|
|
||||||
const preview = elements.previewPopover;
|
|
||||||
const targetRect = target.getBoundingClientRect();
|
|
||||||
const maxWidth = Math.min(448, Math.max(260, window.innerWidth - 32));
|
|
||||||
preview.style.maxWidth = `${maxWidth}px`;
|
|
||||||
const previewRect = preview.getBoundingClientRect();
|
|
||||||
let left = targetRect.right + 12;
|
|
||||||
if (left + previewRect.width > window.innerWidth - 12) {
|
|
||||||
left = Math.max(12, targetRect.left - previewRect.width - 12);
|
|
||||||
}
|
|
||||||
let top = targetRect.top;
|
|
||||||
if (top + previewRect.height > window.innerHeight - 12) {
|
|
||||||
top = Math.max(12, window.innerHeight - previewRect.height - 12);
|
|
||||||
}
|
|
||||||
preview.style.left = `${Math.round(left)}px`;
|
|
||||||
preview.style.top = `${Math.round(top)}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAbstractPreview(item, target) {
|
|
||||||
clearAbstractPreviewTimer();
|
|
||||||
const abstract = typeof item.abstract === "string" ? item.abstract.trim() : "";
|
|
||||||
if (!abstract) {
|
|
||||||
cancelAbstractPreview();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.previewItemKey = item.item_key;
|
|
||||||
elements.previewPopover.textContent = abstract;
|
|
||||||
elements.previewPopover.hidden = false;
|
|
||||||
elements.previewPopover.setAttribute("aria-hidden", "false");
|
|
||||||
positionAbstractPreview(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleAbstractPreview(item, target) {
|
|
||||||
if (!window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const abstract = typeof item.abstract === "string" ? item.abstract.trim() : "";
|
|
||||||
if (!abstract) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cancelAbstractPreview();
|
|
||||||
state.hoveredItemKey = item.item_key;
|
|
||||||
state.hoverTimerId = window.setTimeout(() => {
|
|
||||||
state.hoverTimerId = null;
|
|
||||||
if (state.hoveredItemKey === item.item_key) {
|
|
||||||
showAbstractPreview(item, target);
|
|
||||||
}
|
|
||||||
}, ABSTRACT_PREVIEW_DELAY_MS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCollectionItemsLoadingState(clearItems = false) {
|
function resetCollectionItemsLoadingState(clearItems = false) {
|
||||||
@ -816,20 +798,20 @@
|
|||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
const label = `当前项目:${currentProject.name}`;
|
const label = `当前项目:${currentProject.name}`;
|
||||||
elements.currentProjectLabel.textContent = label;
|
elements.currentProjectLabel.textContent = label;
|
||||||
elements.importModalProjectLabel.textContent = label;
|
elements.windowProjectLabel.textContent = label;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
elements.currentProjectLabel.textContent = "当前未选择项目。";
|
elements.currentProjectLabel.textContent = "当前未选择项目。";
|
||||||
elements.importModalProjectLabel.textContent = "未选择项目";
|
elements.windowProjectLabel.textContent = "未选择项目";
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateImportActionState() {
|
function updateImportActionState() {
|
||||||
const hasProject = Boolean(state.currentProjectId);
|
const hasProject = Boolean(state.currentProjectId);
|
||||||
const hasSelection = state.selectedItemKeys.size > 0;
|
const hasSelection = state.selectedItemKeys.size > 0;
|
||||||
syncCurrentProjectLabels();
|
syncCurrentProjectLabels();
|
||||||
elements.importSelectedItemsButton.disabled = !hasProject || !hasSelection;
|
elements.windowImportSelectedItemsButton.disabled = !hasProject || !hasSelection;
|
||||||
elements.importSelectedItemsButton.title = !hasProject ? "请先选择项目" : (hasSelection ? "" : "请先选择文献");
|
elements.windowImportSelectedItemsButton.title = !hasProject ? "请先选择项目" : (hasSelection ? "" : "请先选择文献");
|
||||||
elements.clearSelectionButton.disabled = !state.selectedItemKeys.size;
|
elements.windowClearSelectionButton.disabled = !state.selectedItemKeys.size;
|
||||||
renderSelectedCount();
|
renderSelectedCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -873,11 +855,11 @@
|
|||||||
|
|
||||||
function renderCollectionTree() {
|
function renderCollectionTree() {
|
||||||
if (!state.collectionTree.length) {
|
if (!state.collectionTree.length) {
|
||||||
elements.collectionTree.innerHTML = '<div class="empty">暂无 collection。</div>';
|
elements.windowCollectionTree.innerHTML = '<div class="empty">暂无 collection。</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.collectionTree.innerHTML = "";
|
elements.windowCollectionTree.innerHTML = "";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
function appendNode(node, depth) {
|
function appendNode(node, depth) {
|
||||||
@ -918,11 +900,11 @@
|
|||||||
button.style.textAlign = "left";
|
button.style.textAlign = "left";
|
||||||
button.innerHTML = `
|
button.innerHTML = `
|
||||||
<strong>${node.name}</strong>
|
<strong>${node.name}</strong>
|
||||||
<div class="meta">${node.direct_item_count} 篇</div>
|
<div class="meta">${node.direct_item_count} 篇直接文献 · ${node.descendant_item_count} 篇含子目录</div>
|
||||||
`;
|
`;
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
loadCollectionItems(node.collection_key).catch((error) => {
|
loadCollectionItems(node.collection_key).catch((error) => {
|
||||||
setStatus(elements.collectionStatus, error.message, true);
|
setStatus(elements.windowCollectionStatus, error.message, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
row.appendChild(button);
|
row.appendChild(button);
|
||||||
@ -939,74 +921,51 @@
|
|||||||
for (const node of state.collectionTree) {
|
for (const node of state.collectionTree) {
|
||||||
appendNode(node, 0);
|
appendNode(node, 0);
|
||||||
}
|
}
|
||||||
elements.collectionTree.appendChild(fragment);
|
elements.windowCollectionTree.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCollectionItems() {
|
function renderCollectionItems() {
|
||||||
cancelAbstractPreview();
|
|
||||||
if (!state.selectedCollectionKey) {
|
if (!state.selectedCollectionKey) {
|
||||||
elements.collectionItems.innerHTML = '<div class="empty">先选择一个 collection。</div>';
|
elements.windowCollectionItems.innerHTML = '<div class="empty">先选择一个 collection。</div>';
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!state.collectionItemsLoaded) {
|
if (!state.collectionItemsLoaded) {
|
||||||
elements.collectionItems.innerHTML = state.loadingCollectionKey === state.selectedCollectionKey
|
elements.windowCollectionItems.innerHTML = state.loadingCollectionKey === state.selectedCollectionKey
|
||||||
? '<div class="empty">正在加载当前 collection 文献。</div>'
|
? '<div class="empty">正在加载当前 collection 文献。</div>'
|
||||||
: '<div class="empty">当前 collection 文献尚未加载。</div>';
|
: '<div class="empty">当前 collection 文献尚未加载。</div>';
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!state.visibleCollectionItems.length) {
|
if (!state.visibleCollectionItems.length) {
|
||||||
elements.collectionItems.innerHTML = '<div class="empty">当前目录下没有可导入文献。</div>';
|
elements.windowCollectionItems.innerHTML = '<div class="empty">当前目录及子目录下没有可导入文献。</div>';
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.collectionItems.innerHTML = "";
|
elements.windowCollectionItems.innerHTML = "";
|
||||||
for (const item of state.visibleCollectionItems) {
|
for (const item of state.visibleCollectionItems) {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("label");
|
||||||
row.className = "collection-item";
|
row.className = "project-item";
|
||||||
const hasAbstract = typeof item.abstract === "string" && item.abstract.trim().length > 0;
|
row.style.display = "grid";
|
||||||
|
row.style.gridTemplateColumns = "auto 1fr";
|
||||||
|
row.style.gap = "0.75rem";
|
||||||
|
row.style.alignItems = "start";
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<input type="checkbox" ${state.selectedItemKeys.has(item.item_key) ? "checked" : ""} />
|
<input type="checkbox" ${state.selectedItemKeys.has(item.item_key) ? "checked" : ""} />
|
||||||
<div class="item-row-body">
|
<div>
|
||||||
<button type="button" class="title-button">${item.title}</button>
|
<strong>${item.title}</strong>
|
||||||
<div class="meta item-year">${item.year || "-"}</div>
|
<div class="meta">${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="secondary details-button"
|
|
||||||
aria-label="${hasAbstract ? "查看摘要" : "暂无摘要"}"
|
|
||||||
${hasAbstract ? "" : "disabled"}
|
|
||||||
>i</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const checkbox = row.querySelector("input");
|
row.querySelector("input").addEventListener("change", () => toggleItemSelection(item.item_key));
|
||||||
const titleButton = row.querySelector(".title-button");
|
elements.windowCollectionItems.appendChild(row);
|
||||||
const detailsButton = row.querySelector(".details-button");
|
|
||||||
checkbox.addEventListener("change", () => {
|
|
||||||
cancelAbstractPreview();
|
|
||||||
toggleItemSelection(item.item_key);
|
|
||||||
});
|
|
||||||
titleButton.addEventListener("mouseenter", () => scheduleAbstractPreview(item, titleButton));
|
|
||||||
titleButton.addEventListener("mouseleave", cancelAbstractPreview);
|
|
||||||
detailsButton.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!hasAbstract) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.previewItemKey === item.item_key && !elements.previewPopover.hidden) {
|
|
||||||
cancelAbstractPreview();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showAbstractPreview(item, detailsButton);
|
|
||||||
});
|
|
||||||
elements.collectionItems.appendChild(row);
|
|
||||||
}
|
}
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCollectionTree() {
|
async function loadCollectionTree() {
|
||||||
setStatus(elements.collectionStatus, "正在加载 collection...");
|
setStatus(elements.windowCollectionStatus, "正在加载 collection...");
|
||||||
const payload = await api("/api/zotero/collections/tree");
|
const payload = await api("/api/zotero/collections/tree");
|
||||||
state.collectionTree = Array.isArray(payload.collections) ? payload.collections : [];
|
state.collectionTree = Array.isArray(payload.collections) ? payload.collections : [];
|
||||||
normalizeSelectedCollectionKey();
|
normalizeSelectedCollectionKey();
|
||||||
@ -1020,11 +979,10 @@
|
|||||||
state.visibleCollectionItems = [];
|
state.visibleCollectionItems = [];
|
||||||
resetCollectionItemsLoadingState();
|
resetCollectionItemsLoadingState();
|
||||||
renderCollectionItems();
|
renderCollectionItems();
|
||||||
setStatus(elements.collectionStatus, "");
|
setStatus(elements.windowCollectionStatus, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCollectionItems(collectionKey) {
|
async function loadCollectionItems(collectionKey) {
|
||||||
cancelAbstractPreview();
|
|
||||||
if (!state.collectionItemsLoaded && state.loadingCollectionKey === collectionKey) {
|
if (!state.collectionItemsLoaded && state.loadingCollectionKey === collectionKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1033,7 +991,7 @@
|
|||||||
resetCollectionItemsLoadingState(true);
|
resetCollectionItemsLoadingState(true);
|
||||||
renderCollectionTree();
|
renderCollectionTree();
|
||||||
renderCollectionItems();
|
renderCollectionItems();
|
||||||
setStatus(elements.collectionStatus, state.selectedCollectionKey ? "已切换到可用 collection。" : "");
|
setStatus(elements.windowCollectionStatus, state.selectedCollectionKey ? "已切换到可用 collection。" : "");
|
||||||
if (state.selectedCollectionKey) {
|
if (state.selectedCollectionKey) {
|
||||||
await loadCollectionItems(state.selectedCollectionKey);
|
await loadCollectionItems(state.selectedCollectionKey);
|
||||||
}
|
}
|
||||||
@ -1048,9 +1006,9 @@
|
|||||||
state.nextCollectionItemsRequestToken = requestToken;
|
state.nextCollectionItemsRequestToken = requestToken;
|
||||||
state.activeCollectionItemsRequestToken = requestToken;
|
state.activeCollectionItemsRequestToken = requestToken;
|
||||||
renderCollectionItems();
|
renderCollectionItems();
|
||||||
setStatus(elements.collectionStatus, "正在加载文献...");
|
setStatus(elements.windowCollectionStatus, "正在加载文献...");
|
||||||
try {
|
try {
|
||||||
const payload = await api(`/api/zotero/collections/${collectionKey}/items?include_descendants=false`);
|
const payload = await api(`/api/zotero/collections/${collectionKey}/items`);
|
||||||
if (state.activeCollectionItemsRequestToken !== requestToken || state.selectedCollectionKey !== collectionKey) {
|
if (state.activeCollectionItemsRequestToken !== requestToken || state.selectedCollectionKey !== collectionKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1060,8 +1018,8 @@
|
|||||||
renderCollectionItems();
|
renderCollectionItems();
|
||||||
const visibleCount = state.visibleCollectionItems.length;
|
const visibleCount = state.visibleCollectionItems.length;
|
||||||
setStatus(
|
setStatus(
|
||||||
elements.collectionStatus,
|
elements.windowCollectionStatus,
|
||||||
visibleCount ? `当前目录共 ${visibleCount} 篇文献。` : "当前目录下没有可导入文献。"
|
visibleCount ? `当前目录及子目录共 ${visibleCount} 篇文献。` : "当前目录及子目录下没有可导入文献。"
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (state.activeCollectionItemsRequestToken === requestToken) {
|
if (state.activeCollectionItemsRequestToken === requestToken) {
|
||||||
@ -1073,7 +1031,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleItemSelection(itemKey) {
|
function toggleItemSelection(itemKey) {
|
||||||
cancelAbstractPreview();
|
|
||||||
if (state.selectedItemKeys.has(itemKey)) {
|
if (state.selectedItemKeys.has(itemKey)) {
|
||||||
state.selectedItemKeys.delete(itemKey);
|
state.selectedItemKeys.delete(itemKey);
|
||||||
} else {
|
} else {
|
||||||
@ -1084,8 +1041,16 @@
|
|||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectVisibleItems() {
|
||||||
|
for (const item of state.visibleCollectionItems) {
|
||||||
|
state.selectedItemKeys.add(item.item_key);
|
||||||
|
}
|
||||||
|
persistImportSessionState();
|
||||||
|
renderCollectionItems();
|
||||||
|
updateImportActionState();
|
||||||
|
}
|
||||||
|
|
||||||
function clearSelectedItems() {
|
function clearSelectedItems() {
|
||||||
cancelAbstractPreview();
|
|
||||||
state.selectedItemKeys.clear();
|
state.selectedItemKeys.clear();
|
||||||
persistImportSessionState();
|
persistImportSessionState();
|
||||||
renderCollectionItems();
|
renderCollectionItems();
|
||||||
@ -1093,12 +1058,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function importSelectedItems() {
|
async function importSelectedItems() {
|
||||||
cancelAbstractPreview();
|
|
||||||
if (!state.currentProjectId || !state.selectedItemKeys.size) {
|
if (!state.currentProjectId || !state.selectedItemKeys.size) {
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus(elements.collectionStatus, "正在导入...");
|
setStatus(elements.windowCollectionStatus, "正在导入...");
|
||||||
const payload = await api(`/api/projects/${state.currentProjectId}/imports/item-keys`, {
|
const payload = await api(`/api/projects/${state.currentProjectId}/imports/item-keys`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ item_keys: Array.from(state.selectedItemKeys) }),
|
body: JSON.stringify({ item_keys: Array.from(state.selectedItemKeys) }),
|
||||||
@ -1107,23 +1071,24 @@
|
|||||||
persistImportSessionState();
|
persistImportSessionState();
|
||||||
renderCollectionItems();
|
renderCollectionItems();
|
||||||
renderCards(payload.project_view?.cards || []);
|
renderCards(payload.project_view?.cards || []);
|
||||||
setStatus(elements.collectionStatus, "导入完成。");
|
setStatus(elements.windowCollectionStatus, "导入完成。");
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
closeImportModal();
|
closeImportWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openImportModal() {
|
async function openImportWindow() {
|
||||||
state.isImportModalOpen = true;
|
state.isImportWindowOpen = true;
|
||||||
elements.importModal.hidden = false;
|
elements.importWindowShell.hidden = false;
|
||||||
elements.importModal.setAttribute("aria-hidden", "false");
|
elements.importWindowShell.setAttribute("aria-hidden", "false");
|
||||||
document.body.classList.toggle("modal-open", true);
|
document.body.classList.toggle("modal-open", true);
|
||||||
cancelAbstractPreview();
|
cancelAbstractPreview();
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
|
initWindowFromSession();
|
||||||
if (!state.collectionTree.length) {
|
if (!state.collectionTree.length) {
|
||||||
try {
|
try {
|
||||||
await loadCollectionTree();
|
await loadCollectionTree();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(elements.collectionStatus, error.message, true);
|
setStatus(elements.windowCollectionStatus, error.message, true);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
state.selectedCollectionKey &&
|
state.selectedCollectionKey &&
|
||||||
@ -1132,7 +1097,7 @@
|
|||||||
try {
|
try {
|
||||||
await loadCollectionItems(state.selectedCollectionKey);
|
await loadCollectionItems(state.selectedCollectionKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(elements.collectionStatus, error.message, true);
|
setStatus(elements.windowCollectionStatus, error.message, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
renderCollectionTree();
|
renderCollectionTree();
|
||||||
@ -1140,10 +1105,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeImportModal() {
|
function closeImportWindow() {
|
||||||
state.isImportModalOpen = false;
|
state.isImportWindowOpen = false;
|
||||||
elements.importModal.hidden = true;
|
elements.importWindowShell.hidden = true;
|
||||||
elements.importModal.setAttribute("aria-hidden", "true");
|
elements.importWindowShell.setAttribute("aria-hidden", "true");
|
||||||
document.body.classList.toggle("modal-open", false);
|
document.body.classList.toggle("modal-open", false);
|
||||||
cancelAbstractPreview();
|
cancelAbstractPreview();
|
||||||
}
|
}
|
||||||
@ -1212,20 +1177,28 @@
|
|||||||
elements.refreshProjectsButton.addEventListener("click", loadProjects);
|
elements.refreshProjectsButton.addEventListener("click", loadProjects);
|
||||||
|
|
||||||
elements.openImportModalButton.addEventListener("click", () => {
|
elements.openImportModalButton.addEventListener("click", () => {
|
||||||
openImportModal();
|
openImportWindow();
|
||||||
});
|
});
|
||||||
elements.closeImportModalButton.addEventListener("click", closeImportModal);
|
elements.windowCloseButton.addEventListener("click", closeImportWindow);
|
||||||
elements.importModalOverlay.addEventListener("click", closeImportModal);
|
elements.windowMinimizeButton.addEventListener("click", minimizeWindow);
|
||||||
elements.collectionItems.addEventListener("scroll", cancelAbstractPreview);
|
elements.windowMaximizeButton.addEventListener("click", maximizeWindow);
|
||||||
elements.clearSelectionButton.addEventListener("click", clearSelectedItems);
|
elements.windowRestoreButton.addEventListener("click", restoreWindow);
|
||||||
elements.importSelectedItemsButton.addEventListener("click", () => {
|
elements.importWindowMinimizedBar.addEventListener("click", (e) => {
|
||||||
|
if (e.target === elements.windowRestoreButton) return;
|
||||||
|
restoreWindow();
|
||||||
|
});
|
||||||
|
elements.windowToolbar.addEventListener("pointerdown", startWindowDrag);
|
||||||
|
document.addEventListener("pointermove", moveWindowDrag);
|
||||||
|
document.addEventListener("pointerup", endWindowDrag);
|
||||||
|
elements.windowClearSelectionButton.addEventListener("click", clearSelectedItems);
|
||||||
|
elements.windowImportSelectedItemsButton.addEventListener("click", () => {
|
||||||
importSelectedItems().catch((error) => {
|
importSelectedItems().catch((error) => {
|
||||||
setStatus(elements.collectionStatus, error.message, true);
|
setStatus(elements.windowCollectionStatus, error.message, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape" && state.isImportModalOpen) {
|
if (event.key === "Escape" && state.isImportWindowOpen) {
|
||||||
closeImportModal();
|
closeImportWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1280,7 +1253,7 @@
|
|||||||
elements.recommendTab.addEventListener("click", () => activateTab("recommend"));
|
elements.recommendTab.addEventListener("click", () => activateTab("recommend"));
|
||||||
elements.planTab.addEventListener("click", () => activateTab("plan"));
|
elements.planTab.addEventListener("click", () => activateTab("plan"));
|
||||||
|
|
||||||
closeImportModal();
|
closeImportWindow();
|
||||||
updateImportActionState();
|
updateImportActionState();
|
||||||
loadProjects().catch((error) => {
|
loadProjects().catch((error) => {
|
||||||
setStatus(elements.projectStatus, error.message, true);
|
setStatus(elements.projectStatus, error.message, true);
|
||||||
|
|||||||
@ -26,21 +26,42 @@ def test_index_contains_base_page_forms(tmp_path) -> None:
|
|||||||
assert 'id="plan-form"' in html
|
assert 'id="plan-form"' in html
|
||||||
|
|
||||||
|
|
||||||
def test_index_contains_import_modal_controls(tmp_path) -> None:
|
def test_index_contains_import_window_controls(tmp_path) -> None:
|
||||||
html = _get_index_html(tmp_path)
|
html = _get_index_html(tmp_path)
|
||||||
|
|
||||||
assert 'id="open-import-modal-button"' in html
|
assert 'id="open-import-modal-button"' in html
|
||||||
assert 'id="import-modal"' in html
|
assert 'id="import-window-shell"' in html
|
||||||
assert 'id="close-import-modal-button"' in html
|
assert 'id="window-minimize-button"' in html
|
||||||
assert 'id="modal-collection-tree"' in html
|
assert 'id="window-maximize-button"' in html
|
||||||
assert 'id="modal-collection-items"' in html
|
assert 'id="window-close-button"' in html
|
||||||
assert 'id="modal-selected-count"' in html
|
assert 'id="window-restore-button"' in html
|
||||||
assert 'id="modal-select-descendants-button"' in html
|
assert 'id="import-window-minimized-bar"' in html
|
||||||
assert 'id="modal-clear-selection-button"' in html
|
assert 'id="import-window-project-label"' in html
|
||||||
assert 'id="modal-import-selected-items-button"' in html
|
assert 'id="window-collection-tree"' in html
|
||||||
|
assert 'id="window-collection-items"' in html
|
||||||
|
assert 'id="window-selected-count"' in html
|
||||||
|
assert 'id="window-clear-selection-button"' in html
|
||||||
|
assert 'id="window-import-selected-items-button"' in html
|
||||||
assert 'id="item-preview-popover"' in html
|
assert 'id="item-preview-popover"' in html
|
||||||
assert 'isImportModalOpen: false' in html
|
assert 'isImportWindowOpen: false' in html
|
||||||
assert "function openImportModal()" in html
|
assert "function openImportWindow()" in html
|
||||||
assert "function closeImportModal()" in html
|
assert "function closeImportWindow()" in html
|
||||||
assert "function updateImportActionState()" in html
|
assert "function updateImportActionState()" in html
|
||||||
assert 'document.body.classList.toggle("modal-open"' in html
|
assert 'document.body.classList.toggle("modal-open"' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_window_frame_css(tmp_path) -> None:
|
||||||
|
html = _get_index_html(tmp_path)
|
||||||
|
|
||||||
|
assert ".window-frame" in html
|
||||||
|
assert ".window-shell" in html
|
||||||
|
assert ".window-toolbar" in html
|
||||||
|
assert ".window-body" in html
|
||||||
|
assert ".window-minimized-bar" in html
|
||||||
|
assert '"zotero-kb.import-window"' in html
|
||||||
|
assert "startWindowDrag" in html
|
||||||
|
assert "minimizeWindow" in html
|
||||||
|
assert "maximizeWindow" in html
|
||||||
|
assert "restoreWindow" in html
|
||||||
|
assert "initWindowFromSession" in html
|
||||||
|
assert "persistWindowSessionState" in html
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user