reviw 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.cjs +376 -89
- package/package.json +1 -1
package/cli.cjs
CHANGED
|
@@ -3566,7 +3566,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3566
3566
|
}, 1500);
|
|
3567
3567
|
}
|
|
3568
3568
|
|
|
3569
|
-
function openCardForSelection() {
|
|
3569
|
+
function openCardForSelection(previewElement) {
|
|
3570
3570
|
if (!selection) return;
|
|
3571
3571
|
// Don't open card while image/video modal is visible
|
|
3572
3572
|
const imageOverlay = document.getElementById('image-fullscreen');
|
|
@@ -3604,10 +3604,50 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3604
3604
|
commentInput.value = existingComment?.text || '';
|
|
3605
3605
|
|
|
3606
3606
|
card.style.display = 'block';
|
|
3607
|
+
// 常にソーステーブルの選択セル位置を基準にカードを配置
|
|
3608
|
+
// これにより、プレビューからクリックしてもソースからクリックしても
|
|
3609
|
+
// 同じ行に対しては同じ位置にダイアログが表示される
|
|
3607
3610
|
positionCardForSelection(startRow, endRow, startCol, endCol);
|
|
3608
3611
|
commentInput.focus();
|
|
3609
3612
|
}
|
|
3610
3613
|
|
|
3614
|
+
// Position card near a clicked preview element (used when clicking from preview pane)
|
|
3615
|
+
// Note: Uses viewport-relative coordinates directly since .md-left/.md-right containers scroll,
|
|
3616
|
+
// not the document. The card uses position:absolute but with no positioned ancestor,
|
|
3617
|
+
// so it's positioned relative to the initial containing block.
|
|
3618
|
+
function positionCardNearElement(element) {
|
|
3619
|
+
const cardWidth = card.offsetWidth || 380;
|
|
3620
|
+
const cardHeight = card.offsetHeight || 220;
|
|
3621
|
+
const rect = element.getBoundingClientRect();
|
|
3622
|
+
const margin = 12;
|
|
3623
|
+
const vw = window.innerWidth;
|
|
3624
|
+
const vh = window.innerHeight;
|
|
3625
|
+
|
|
3626
|
+
// Use viewport-relative coordinates directly from getBoundingClientRect()
|
|
3627
|
+
// No need to add window.scrollX/Y since the containers scroll, not the document
|
|
3628
|
+
let left = rect.right + margin;
|
|
3629
|
+
let top = rect.top;
|
|
3630
|
+
|
|
3631
|
+
// If card would go off the right edge, position it below the element
|
|
3632
|
+
if (left + cardWidth > vw - margin) {
|
|
3633
|
+
left = Math.max(rect.left, margin);
|
|
3634
|
+
left = Math.min(left, vw - cardWidth - margin);
|
|
3635
|
+
top = rect.bottom + margin;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// If card would go off the bottom, position it above the element
|
|
3639
|
+
if (top + cardHeight > vh - margin) {
|
|
3640
|
+
top = rect.top - cardHeight - margin;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// Ensure card stays within viewport
|
|
3644
|
+
top = Math.max(margin, Math.min(top, vh - cardHeight - margin));
|
|
3645
|
+
left = Math.max(margin, Math.min(left, vw - cardWidth - margin));
|
|
3646
|
+
|
|
3647
|
+
card.style.left = left + 'px';
|
|
3648
|
+
card.style.top = top + 'px';
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3611
3651
|
function positionCardForSelection(startRow, endRow, startCol, endCol) {
|
|
3612
3652
|
const cardWidth = card.offsetWidth || 380;
|
|
3613
3653
|
const cardHeight = card.offsetHeight || 220;
|
|
@@ -3632,6 +3672,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3632
3672
|
const sx = window.scrollX;
|
|
3633
3673
|
const sy = window.scrollY;
|
|
3634
3674
|
|
|
3675
|
+
// Check if the selection is within viewport
|
|
3676
|
+
const isInViewport = rect.top >= 0 && rect.top < vh && rect.bottom > 0;
|
|
3677
|
+
|
|
3635
3678
|
const spaceRight = vw - rect.right - margin;
|
|
3636
3679
|
const spaceLeft = rect.left - margin - ROW_HEADER_WIDTH; // Account for row header
|
|
3637
3680
|
const spaceBelow = vh - rect.bottom - margin;
|
|
@@ -3643,28 +3686,38 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3643
3686
|
let left = sx + rect.right + margin;
|
|
3644
3687
|
let top = sy + rect.top;
|
|
3645
3688
|
|
|
3646
|
-
//
|
|
3647
|
-
if (
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3651
|
-
} else if (spaceBelow >= cardHeight) {
|
|
3652
|
-
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3653
|
-
top = sy + rect.bottom + margin;
|
|
3654
|
-
} else if (spaceAbove >= cardHeight) {
|
|
3655
|
-
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3656
|
-
top = sy + rect.top - cardHeight - margin;
|
|
3657
|
-
} else if (spaceLeft >= cardWidth) {
|
|
3658
|
-
left = sx + rect.left - cardWidth - margin;
|
|
3659
|
-
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3689
|
+
// If selection is outside viewport, position card in center of viewport
|
|
3690
|
+
if (!isInViewport) {
|
|
3691
|
+
left = sx + Math.max(vw / 2 - cardWidth / 2, minLeft);
|
|
3692
|
+
top = sy + Math.max(vh / 2 - cardHeight / 2, margin);
|
|
3660
3693
|
} else {
|
|
3661
|
-
//
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3694
|
+
// Priority: right > below > above > left > fallback right
|
|
3695
|
+
if (spaceRight >= cardWidth) {
|
|
3696
|
+
// Prefer right side of selection
|
|
3697
|
+
left = sx + rect.right + margin;
|
|
3698
|
+
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3699
|
+
} else if (spaceBelow >= cardHeight) {
|
|
3700
|
+
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3701
|
+
top = sy + rect.bottom + margin;
|
|
3702
|
+
} else if (spaceAbove >= cardHeight) {
|
|
3703
|
+
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3704
|
+
top = sy + rect.top - cardHeight - margin;
|
|
3705
|
+
} else if (spaceLeft >= cardWidth) {
|
|
3706
|
+
left = sx + rect.left - cardWidth - margin;
|
|
3707
|
+
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3708
|
+
} else {
|
|
3709
|
+
// Fallback: place to right side even if it means going off screen
|
|
3710
|
+
// Position card at right edge of selection, clamped to viewport
|
|
3711
|
+
left = sx + Math.max(rect.right + margin, minLeft);
|
|
3712
|
+
left = Math.min(left, sx + vw - cardWidth - margin);
|
|
3713
|
+
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3714
|
+
}
|
|
3666
3715
|
}
|
|
3667
3716
|
|
|
3717
|
+
// Final clamp to ensure card stays within viewport
|
|
3718
|
+
left = clamp(left, margin, sx + vw - cardWidth - margin);
|
|
3719
|
+
top = clamp(top, sy + margin, sy + vh - cardHeight - margin);
|
|
3720
|
+
|
|
3668
3721
|
card.style.left = left + 'px';
|
|
3669
3722
|
card.style.top = top + 'px';
|
|
3670
3723
|
}
|
|
@@ -3673,6 +3726,8 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3673
3726
|
card.style.display = 'none';
|
|
3674
3727
|
currentKey = null;
|
|
3675
3728
|
clearSelection();
|
|
3729
|
+
// Re-enable scroll sync when card is closed
|
|
3730
|
+
window._disableScrollSync = false;
|
|
3676
3731
|
}
|
|
3677
3732
|
|
|
3678
3733
|
function setDot(row, col, on) {
|
|
@@ -4277,20 +4332,56 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4277
4332
|
})();
|
|
4278
4333
|
|
|
4279
4334
|
// --- Scroll Sync for Markdown Mode ---
|
|
4335
|
+
// Global flag to temporarily disable scroll sync (used by selectSourceRange)
|
|
4336
|
+
window._disableScrollSync = false;
|
|
4337
|
+
// Global RAF ID so we can cancel pending scroll syncs from selectSourceRange
|
|
4338
|
+
window._scrollSyncRafId = null;
|
|
4339
|
+
|
|
4280
4340
|
if (MODE === 'markdown') {
|
|
4281
4341
|
const mdLeft = document.querySelector('.md-left');
|
|
4282
4342
|
const mdRight = document.querySelector('.md-right');
|
|
4283
4343
|
if (mdLeft && mdRight) {
|
|
4284
4344
|
let activePane = null;
|
|
4285
|
-
|
|
4345
|
+
|
|
4346
|
+
// Build anchor map for section-based sync
|
|
4347
|
+
// Maps line numbers to heading elements in preview
|
|
4348
|
+
const headingAnchors = [];
|
|
4349
|
+
const preview = document.querySelector('.md-preview');
|
|
4350
|
+
if (preview) {
|
|
4351
|
+
const headings = preview.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
4352
|
+
headings.forEach(h => {
|
|
4353
|
+
const text = h.textContent.trim();
|
|
4354
|
+
// Find corresponding line in source
|
|
4355
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
4356
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
4357
|
+
if (lineText.match(/^#+\\s/) && lineText.replace(/^#+\\s*/, '').trim() === text) {
|
|
4358
|
+
headingAnchors.push({
|
|
4359
|
+
line: i + 1,
|
|
4360
|
+
sourceEl: mdLeft.querySelector('td[data-row="' + (i + 1) + '"]'),
|
|
4361
|
+
previewEl: h
|
|
4362
|
+
});
|
|
4363
|
+
break;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4286
4368
|
|
|
4287
4369
|
function syncScroll(source, target, sourceName) {
|
|
4370
|
+
// Skip if scroll sync is temporarily disabled
|
|
4371
|
+
if (window._disableScrollSync) return;
|
|
4372
|
+
// Skip if scroll sync is disabled until a certain time (for preview click scroll)
|
|
4373
|
+
if (window._scrollSyncDisableUntil && Date.now() < window._scrollSyncDisableUntil) return;
|
|
4374
|
+
|
|
4288
4375
|
// Only sync if this pane initiated the scroll
|
|
4289
4376
|
if (activePane && activePane !== sourceName) return;
|
|
4290
4377
|
activePane = sourceName;
|
|
4291
4378
|
|
|
4292
|
-
if (
|
|
4293
|
-
|
|
4379
|
+
if (window._scrollSyncRafId) cancelAnimationFrame(window._scrollSyncRafId);
|
|
4380
|
+
window._scrollSyncRafId = requestAnimationFrame(() => {
|
|
4381
|
+
// Check again inside RAF in case _disableScrollSync was set after RAF was scheduled
|
|
4382
|
+
if (window._disableScrollSync) return;
|
|
4383
|
+
if (window._scrollSyncDisableUntil && Date.now() < window._scrollSyncDisableUntil) return;
|
|
4384
|
+
|
|
4294
4385
|
const sourceMax = source.scrollHeight - source.clientHeight;
|
|
4295
4386
|
const targetMax = target.scrollHeight - target.clientHeight;
|
|
4296
4387
|
|
|
@@ -4299,20 +4390,81 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4299
4390
|
// Snap to edges for precision
|
|
4300
4391
|
if (source.scrollTop <= 1) {
|
|
4301
4392
|
target.scrollTop = 0;
|
|
4302
|
-
|
|
4393
|
+
setTimeout(() => { activePane = null; }, 100);
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
if (source.scrollTop >= sourceMax - 1) {
|
|
4303
4397
|
target.scrollTop = targetMax;
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4398
|
+
setTimeout(() => { activePane = null; }, 100);
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
// Try section-based sync if anchors exist
|
|
4403
|
+
if (headingAnchors.length > 0) {
|
|
4404
|
+
const sourceRect = source.getBoundingClientRect();
|
|
4405
|
+
const viewportTop = sourceRect.top;
|
|
4406
|
+
const viewportMid = viewportTop + sourceRect.height / 3;
|
|
4407
|
+
|
|
4408
|
+
// Find the heading closest to viewport top in source
|
|
4409
|
+
let closestAnchor = null;
|
|
4410
|
+
let closestDistance = Infinity;
|
|
4411
|
+
|
|
4412
|
+
for (const anchor of headingAnchors) {
|
|
4413
|
+
const el = sourceName === 'left' ? anchor.sourceEl : anchor.previewEl;
|
|
4414
|
+
if (!el) continue;
|
|
4415
|
+
const rect = el.getBoundingClientRect();
|
|
4416
|
+
const distance = Math.abs(rect.top - viewportMid);
|
|
4417
|
+
if (distance < closestDistance) {
|
|
4418
|
+
closestDistance = distance;
|
|
4419
|
+
closestAnchor = anchor;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
if (closestAnchor) {
|
|
4424
|
+
const sourceEl = sourceName === 'left' ? closestAnchor.sourceEl : closestAnchor.previewEl;
|
|
4425
|
+
const targetEl = sourceName === 'left' ? closestAnchor.previewEl : closestAnchor.sourceEl;
|
|
4426
|
+
|
|
4427
|
+
if (sourceEl && targetEl) {
|
|
4428
|
+
const sourceElRect = sourceEl.getBoundingClientRect();
|
|
4429
|
+
const sourceOffset = sourceElRect.top - sourceRect.top;
|
|
4430
|
+
|
|
4431
|
+
// Calculate where target element should be
|
|
4432
|
+
const targetRect = target.getBoundingClientRect();
|
|
4433
|
+
const targetElRect = targetEl.getBoundingClientRect();
|
|
4434
|
+
const currentTargetOffset = targetElRect.top - targetRect.top;
|
|
4435
|
+
|
|
4436
|
+
// Adjust target scroll to align the anchor
|
|
4437
|
+
const adjustment = currentTargetOffset - sourceOffset;
|
|
4438
|
+
target.scrollTop = target.scrollTop + adjustment;
|
|
4439
|
+
|
|
4440
|
+
setTimeout(() => { activePane = null; }, 100);
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4307
4444
|
}
|
|
4308
4445
|
|
|
4446
|
+
// Fallback to ratio-based sync
|
|
4447
|
+
const ratio = source.scrollTop / sourceMax;
|
|
4448
|
+
target.scrollTop = Math.round(ratio * targetMax);
|
|
4449
|
+
|
|
4309
4450
|
// Release lock after scroll settles
|
|
4310
4451
|
setTimeout(() => { activePane = null; }, 100);
|
|
4311
4452
|
});
|
|
4312
4453
|
}
|
|
4313
4454
|
|
|
4314
|
-
|
|
4315
|
-
|
|
4455
|
+
// Store scroll handlers for temporary removal
|
|
4456
|
+
const leftScrollHandler = () => syncScroll(mdLeft, mdRight, 'left');
|
|
4457
|
+
const rightScrollHandler = () => syncScroll(mdRight, mdLeft, 'right');
|
|
4458
|
+
mdLeft.addEventListener('scroll', leftScrollHandler, { passive: true });
|
|
4459
|
+
mdRight.addEventListener('scroll', rightScrollHandler, { passive: true });
|
|
4460
|
+
|
|
4461
|
+
// Expose handlers for temporary removal during preview click
|
|
4462
|
+
window._scrollHandlers = {
|
|
4463
|
+
left: leftScrollHandler,
|
|
4464
|
+
right: rightScrollHandler,
|
|
4465
|
+
mdLeft: mdLeft,
|
|
4466
|
+
mdRight: mdRight
|
|
4467
|
+
};
|
|
4316
4468
|
}
|
|
4317
4469
|
}
|
|
4318
4470
|
|
|
@@ -4632,9 +4784,47 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4632
4784
|
const imageClose = document.getElementById('image-close');
|
|
4633
4785
|
if (!imageOverlay || !imageContainer) return;
|
|
4634
4786
|
|
|
4787
|
+
// Collect all images for navigation
|
|
4788
|
+
const allImages = Array.from(preview.querySelectorAll('img'));
|
|
4789
|
+
let currentImageIndex = -1;
|
|
4790
|
+
|
|
4791
|
+
function showImage(index) {
|
|
4792
|
+
if (index < 0 || index >= allImages.length) return;
|
|
4793
|
+
currentImageIndex = index;
|
|
4794
|
+
const img = allImages[index];
|
|
4795
|
+
|
|
4796
|
+
imageContainer.innerHTML = '';
|
|
4797
|
+
const clonedImg = img.cloneNode(true);
|
|
4798
|
+
// CSSで制御するためインラインスタイルはリセット
|
|
4799
|
+
clonedImg.style.width = '';
|
|
4800
|
+
clonedImg.style.height = '';
|
|
4801
|
+
clonedImg.style.maxWidth = '';
|
|
4802
|
+
clonedImg.style.maxHeight = '';
|
|
4803
|
+
clonedImg.style.cursor = 'default';
|
|
4804
|
+
imageContainer.appendChild(clonedImg);
|
|
4805
|
+
|
|
4806
|
+
// Show navigation hint
|
|
4807
|
+
const counter = document.createElement('div');
|
|
4808
|
+
counter.className = 'fullscreen-counter';
|
|
4809
|
+
counter.textContent = \`\${index + 1} / \${allImages.length}\`;
|
|
4810
|
+
counter.style.cssText = 'position:absolute;bottom:20px;left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,0.6);padding:8px 16px;border-radius:20px;font-size:14px;';
|
|
4811
|
+
imageContainer.appendChild(counter);
|
|
4812
|
+
|
|
4813
|
+
imageOverlay.classList.add('visible');
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4635
4816
|
function closeImageOverlay() {
|
|
4636
4817
|
imageOverlay.classList.remove('visible');
|
|
4637
4818
|
imageContainer.innerHTML = '';
|
|
4819
|
+
currentImageIndex = -1;
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
function navigateImage(direction) {
|
|
4823
|
+
if (!imageOverlay.classList.contains('visible')) return;
|
|
4824
|
+
const newIndex = currentImageIndex + direction;
|
|
4825
|
+
if (newIndex >= 0 && newIndex < allImages.length) {
|
|
4826
|
+
showImage(newIndex);
|
|
4827
|
+
}
|
|
4638
4828
|
}
|
|
4639
4829
|
|
|
4640
4830
|
if (imageClose) {
|
|
@@ -4649,30 +4839,33 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4649
4839
|
}
|
|
4650
4840
|
|
|
4651
4841
|
document.addEventListener('keydown', (e) => {
|
|
4652
|
-
if (
|
|
4653
|
-
|
|
4842
|
+
if (!imageOverlay.classList.contains('visible')) return;
|
|
4843
|
+
|
|
4844
|
+
switch (e.key) {
|
|
4845
|
+
case 'Escape':
|
|
4846
|
+
closeImageOverlay();
|
|
4847
|
+
break;
|
|
4848
|
+
case 'ArrowLeft':
|
|
4849
|
+
case 'ArrowUp':
|
|
4850
|
+
e.preventDefault();
|
|
4851
|
+
navigateImage(-1);
|
|
4852
|
+
break;
|
|
4853
|
+
case 'ArrowRight':
|
|
4854
|
+
case 'ArrowDown':
|
|
4855
|
+
e.preventDefault();
|
|
4856
|
+
navigateImage(1);
|
|
4857
|
+
break;
|
|
4654
4858
|
}
|
|
4655
4859
|
});
|
|
4656
4860
|
|
|
4657
|
-
|
|
4861
|
+
allImages.forEach((img, index) => {
|
|
4658
4862
|
img.style.cursor = 'pointer';
|
|
4659
|
-
img.title = 'Click to view fullscreen';
|
|
4863
|
+
img.title = 'Click to view fullscreen (← → to navigate)';
|
|
4660
4864
|
|
|
4661
4865
|
img.addEventListener('click', (e) => {
|
|
4662
4866
|
// Don't stop propagation - allow select to work
|
|
4663
4867
|
e.preventDefault();
|
|
4664
|
-
|
|
4665
|
-
imageContainer.innerHTML = '';
|
|
4666
|
-
const clonedImg = img.cloneNode(true);
|
|
4667
|
-
// CSSで制御するためインラインスタイルはリセット
|
|
4668
|
-
clonedImg.style.width = '';
|
|
4669
|
-
clonedImg.style.height = '';
|
|
4670
|
-
clonedImg.style.maxWidth = '';
|
|
4671
|
-
clonedImg.style.maxHeight = '';
|
|
4672
|
-
clonedImg.style.cursor = 'default';
|
|
4673
|
-
imageContainer.appendChild(clonedImg);
|
|
4674
|
-
|
|
4675
|
-
imageOverlay.classList.add('visible');
|
|
4868
|
+
showImage(index);
|
|
4676
4869
|
});
|
|
4677
4870
|
});
|
|
4678
4871
|
})();
|
|
@@ -4689,8 +4882,56 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4689
4882
|
|
|
4690
4883
|
const videoExtensions = /\\.(mp4|mov|webm|avi|mkv|m4v|ogv)$/i;
|
|
4691
4884
|
|
|
4885
|
+
// Collect all video links for navigation
|
|
4886
|
+
const allVideoLinks = Array.from(preview.querySelectorAll('a')).filter(link => {
|
|
4887
|
+
const href = link.getAttribute('href');
|
|
4888
|
+
return href && videoExtensions.test(href);
|
|
4889
|
+
});
|
|
4890
|
+
let currentVideoIndex = -1;
|
|
4891
|
+
|
|
4892
|
+
function showVideo(index) {
|
|
4893
|
+
if (index < 0 || index >= allVideoLinks.length) return;
|
|
4894
|
+
currentVideoIndex = index;
|
|
4895
|
+
const link = allVideoLinks[index];
|
|
4896
|
+
const href = link.getAttribute('href');
|
|
4897
|
+
|
|
4898
|
+
// Remove existing video if any
|
|
4899
|
+
const existingVideo = videoContainer.querySelector('video');
|
|
4900
|
+
if (existingVideo) {
|
|
4901
|
+
existingVideo.pause();
|
|
4902
|
+
existingVideo.src = '';
|
|
4903
|
+
existingVideo.remove();
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
// Remove existing counter
|
|
4907
|
+
const existingCounter = videoContainer.querySelector('.fullscreen-counter');
|
|
4908
|
+
if (existingCounter) existingCounter.remove();
|
|
4909
|
+
|
|
4910
|
+
const video = document.createElement('video');
|
|
4911
|
+
video.src = href;
|
|
4912
|
+
video.controls = true;
|
|
4913
|
+
video.autoplay = true;
|
|
4914
|
+
video.style.maxWidth = '100%';
|
|
4915
|
+
video.style.maxHeight = '100%';
|
|
4916
|
+
// Prevent click on video from closing overlay
|
|
4917
|
+
video.addEventListener('click', (e) => e.stopPropagation());
|
|
4918
|
+
videoContainer.appendChild(video);
|
|
4919
|
+
|
|
4920
|
+
// Show navigation hint
|
|
4921
|
+
if (allVideoLinks.length > 1) {
|
|
4922
|
+
const counter = document.createElement('div');
|
|
4923
|
+
counter.className = 'fullscreen-counter';
|
|
4924
|
+
counter.textContent = \`\${index + 1} / \${allVideoLinks.length}\`;
|
|
4925
|
+
counter.style.cssText = 'position:absolute;bottom:20px;left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,0.6);padding:8px 16px;border-radius:20px;font-size:14px;';
|
|
4926
|
+
videoContainer.appendChild(counter);
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
videoOverlay.classList.add('visible');
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4692
4932
|
function closeVideoOverlay() {
|
|
4693
4933
|
videoOverlay.classList.remove('visible');
|
|
4934
|
+
currentVideoIndex = -1;
|
|
4694
4935
|
// Stop and remove video
|
|
4695
4936
|
const video = videoContainer.querySelector('video');
|
|
4696
4937
|
if (video) {
|
|
@@ -4700,6 +4941,14 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4700
4941
|
}
|
|
4701
4942
|
}
|
|
4702
4943
|
|
|
4944
|
+
function navigateVideo(direction) {
|
|
4945
|
+
if (!videoOverlay.classList.contains('visible')) return;
|
|
4946
|
+
const newIndex = currentVideoIndex + direction;
|
|
4947
|
+
if (newIndex >= 0 && newIndex < allVideoLinks.length) {
|
|
4948
|
+
showVideo(newIndex);
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4703
4952
|
if (videoClose) {
|
|
4704
4953
|
videoClose.addEventListener('click', closeVideoOverlay);
|
|
4705
4954
|
}
|
|
@@ -4712,41 +4961,37 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4712
4961
|
}
|
|
4713
4962
|
|
|
4714
4963
|
document.addEventListener('keydown', (e) => {
|
|
4715
|
-
if (
|
|
4716
|
-
|
|
4964
|
+
if (!videoOverlay.classList.contains('visible')) return;
|
|
4965
|
+
|
|
4966
|
+
switch (e.key) {
|
|
4967
|
+
case 'Escape':
|
|
4968
|
+
closeVideoOverlay();
|
|
4969
|
+
break;
|
|
4970
|
+
case 'ArrowLeft':
|
|
4971
|
+
case 'ArrowUp':
|
|
4972
|
+
e.preventDefault();
|
|
4973
|
+
navigateVideo(-1);
|
|
4974
|
+
break;
|
|
4975
|
+
case 'ArrowRight':
|
|
4976
|
+
case 'ArrowDown':
|
|
4977
|
+
e.preventDefault();
|
|
4978
|
+
navigateVideo(1);
|
|
4979
|
+
break;
|
|
4717
4980
|
}
|
|
4718
4981
|
});
|
|
4719
4982
|
|
|
4720
4983
|
// Intercept video link clicks
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
link.addEventListener('click', (e) => {
|
|
4728
|
-
e.preventDefault();
|
|
4729
|
-
// Don't stop propagation - allow select to work
|
|
4730
|
-
|
|
4731
|
-
// Remove existing video if any
|
|
4732
|
-
const existingVideo = videoContainer.querySelector('video');
|
|
4733
|
-
if (existingVideo) {
|
|
4734
|
-
existingVideo.pause();
|
|
4735
|
-
existingVideo.src = '';
|
|
4736
|
-
existingVideo.remove();
|
|
4737
|
-
}
|
|
4984
|
+
allVideoLinks.forEach((link, index) => {
|
|
4985
|
+
link.style.cursor = 'pointer';
|
|
4986
|
+
link.title = allVideoLinks.length > 1
|
|
4987
|
+
? 'Click to play video fullscreen (← → to navigate)'
|
|
4988
|
+
: 'Click to play video fullscreen';
|
|
4738
4989
|
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
video.style.maxHeight = '100%';
|
|
4745
|
-
videoContainer.appendChild(video);
|
|
4746
|
-
|
|
4747
|
-
videoOverlay.classList.add('visible');
|
|
4748
|
-
});
|
|
4749
|
-
}
|
|
4990
|
+
link.addEventListener('click', (e) => {
|
|
4991
|
+
e.preventDefault();
|
|
4992
|
+
// Don't stop propagation - allow select to work
|
|
4993
|
+
showVideo(index);
|
|
4994
|
+
});
|
|
4750
4995
|
});
|
|
4751
4996
|
})();
|
|
4752
4997
|
|
|
@@ -4930,22 +5175,59 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4930
5175
|
}
|
|
4931
5176
|
|
|
4932
5177
|
// Trigger source cell selection (reuse existing comment flow)
|
|
4933
|
-
|
|
5178
|
+
// When clickedPreviewElement is provided (from preview click), position card near that element
|
|
5179
|
+
function selectSourceRange(startRow, endRow, clickedPreviewElement) {
|
|
5180
|
+
// IMMEDIATELY disable scroll sync at the very start
|
|
5181
|
+
window._disableScrollSync = true;
|
|
5182
|
+
window._scrollSyncDisableUntil = Date.now() + 2000;
|
|
5183
|
+
|
|
5184
|
+
// Cancel any pending scroll sync RAF
|
|
5185
|
+
if (window._scrollSyncRafId) {
|
|
5186
|
+
cancelAnimationFrame(window._scrollSyncRafId);
|
|
5187
|
+
window._scrollSyncRafId = null;
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// TEMPORARILY REMOVE scroll event listeners to prevent any interference
|
|
5191
|
+
const handlers = window._scrollHandlers;
|
|
5192
|
+
if (handlers) {
|
|
5193
|
+
handlers.mdLeft.removeEventListener('scroll', handlers.left);
|
|
5194
|
+
handlers.mdRight.removeEventListener('scroll', handlers.right);
|
|
5195
|
+
}
|
|
5196
|
+
|
|
4934
5197
|
selection = { startRow, endRow: endRow || startRow, startCol: 1, endCol: 1 };
|
|
4935
5198
|
updateSelectionVisual();
|
|
4936
5199
|
|
|
4937
5200
|
// Clear header selection
|
|
4938
5201
|
document.querySelectorAll('thead th.selected').forEach(el => el.classList.remove('selected'));
|
|
4939
5202
|
|
|
4940
|
-
// Scroll source
|
|
4941
|
-
const
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5203
|
+
// Scroll source pane FIRST (before opening card) to ensure target is visible
|
|
5204
|
+
const targetRow = startRow;
|
|
5205
|
+
const mdRight = document.querySelector('.md-right');
|
|
5206
|
+
const sourceTd = document.querySelector('td[data-row="' + targetRow + '"][data-col="1"]');
|
|
5207
|
+
if (mdRight && sourceTd) {
|
|
5208
|
+
const tdOffsetTop = sourceTd.offsetTop;
|
|
5209
|
+
const containerHeight = mdRight.clientHeight;
|
|
5210
|
+
const tdHeight = sourceTd.offsetHeight;
|
|
5211
|
+
const scrollTarget = tdOffsetTop - (containerHeight / 2) + (tdHeight / 2);
|
|
5212
|
+
// Use scrollTo with instant behavior to ensure immediate scroll
|
|
5213
|
+
mdRight.scrollTo({
|
|
5214
|
+
top: Math.max(0, scrollTarget),
|
|
5215
|
+
behavior: 'instant'
|
|
5216
|
+
});
|
|
4948
5217
|
}
|
|
5218
|
+
|
|
5219
|
+
// Open the card (synchronously) - now target cell should be visible for positioning
|
|
5220
|
+
openCardForSelection();
|
|
5221
|
+
|
|
5222
|
+
// Re-add scroll handlers after a delay to allow scroll to settle
|
|
5223
|
+
setTimeout(() => {
|
|
5224
|
+
if (handlers) {
|
|
5225
|
+
handlers.mdLeft.addEventListener('scroll', handlers.left, { passive: true });
|
|
5226
|
+
handlers.mdRight.addEventListener('scroll', handlers.right, { passive: true });
|
|
5227
|
+
}
|
|
5228
|
+
window._disableScrollSync = false;
|
|
5229
|
+
window._scrollSyncDisableUntil = 0;
|
|
5230
|
+
}, 500);
|
|
4949
5231
|
}
|
|
4950
5232
|
|
|
4951
5233
|
// Click on block elements
|
|
@@ -4954,7 +5236,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4954
5236
|
if (e.target.tagName === 'IMG') {
|
|
4955
5237
|
const line = findImageSourceLine(e.target.src);
|
|
4956
5238
|
if (line > 0) {
|
|
4957
|
-
selectSourceRange(line);
|
|
5239
|
+
selectSourceRange(line, null, e.target);
|
|
4958
5240
|
}
|
|
4959
5241
|
return;
|
|
4960
5242
|
}
|
|
@@ -4968,7 +5250,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4968
5250
|
const isTableCell = parentBlock.tagName === 'TD' || parentBlock.tagName === 'TH';
|
|
4969
5251
|
const line = isTableCell ? findTableSourceLine(parentBlock.textContent) : findSourceLine(parentBlock.textContent);
|
|
4970
5252
|
if (line > 0) {
|
|
4971
|
-
selectSourceRange(line);
|
|
5253
|
+
selectSourceRange(line, null, parentBlock);
|
|
4972
5254
|
}
|
|
4973
5255
|
}
|
|
4974
5256
|
// Let the link open naturally (target="_blank" is set by marked)
|
|
@@ -4985,7 +5267,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4985
5267
|
const { startLine, endLine } = findCodeBlockRange(code.textContent);
|
|
4986
5268
|
if (startLine > 0) {
|
|
4987
5269
|
e.preventDefault();
|
|
4988
|
-
selectSourceRange(startLine, endLine);
|
|
5270
|
+
selectSourceRange(startLine, endLine, pre);
|
|
4989
5271
|
}
|
|
4990
5272
|
return;
|
|
4991
5273
|
}
|
|
@@ -4999,7 +5281,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4999
5281
|
if (line <= 0) return;
|
|
5000
5282
|
|
|
5001
5283
|
e.preventDefault();
|
|
5002
|
-
selectSourceRange(line);
|
|
5284
|
+
selectSourceRange(line, null, target);
|
|
5003
5285
|
});
|
|
5004
5286
|
|
|
5005
5287
|
// Text selection to open comment for range
|
|
@@ -5019,8 +5301,13 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
5019
5301
|
|
|
5020
5302
|
if (startLine <= 0) return;
|
|
5021
5303
|
|
|
5304
|
+
// Get the element containing the selection for positioning
|
|
5305
|
+
const range = sel.getRangeAt(0);
|
|
5306
|
+
const container = range.commonAncestorContainer;
|
|
5307
|
+
const element = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
|
|
5308
|
+
|
|
5022
5309
|
sel.removeAllRanges();
|
|
5023
|
-
selectSourceRange(startLine, endLine > 0 ? endLine : startLine);
|
|
5310
|
+
selectSourceRange(startLine, endLine > 0 ? endLine : startLine, element);
|
|
5024
5311
|
}, 10);
|
|
5025
5312
|
});
|
|
5026
5313
|
})();
|