stimulus-pdf-viewer-rails 0.3.0 → 0.3.1
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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fe6b668cbffa8acaf9ade9ef15614aad4d7677e3dc505bebe4065ba8ccdb726
|
|
4
|
+
data.tar.gz: f8f51d4e310e36beb6dd51efc9dd3501daaa15067b2a853912d074427a04b9f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad37d63e9f0c26f373b293737818fd85121bf78945f8e97ab06371f75d2aa3ac6b8b4b9976a27cb5553b5cb3f738b51258a82541761bb78531116e34fa27e90f
|
|
7
|
+
data.tar.gz: 41b654cae115f12ef9a8f33c80d5d0645657cbccc7b70ee94651dd557b2ea416b38ace501b7a0e43cf9387dc8b16237c2f7e442915d338e1c03dd0ee3108ad45
|
|
@@ -2578,12 +2578,12 @@ class AnnotationEditToolbar {
|
|
|
2578
2578
|
});
|
|
2579
2579
|
|
|
2580
2580
|
// Handle keyboard shortcuts
|
|
2581
|
-
|
|
2581
|
+
this._keydownHandler = (e) => {
|
|
2582
2582
|
if (this.element.classList.contains("hidden")) return
|
|
2583
2583
|
|
|
2584
2584
|
// Don't intercept if user is typing
|
|
2585
2585
|
const activeEl = document.activeElement;
|
|
2586
|
-
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA")) return
|
|
2586
|
+
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
|
|
2587
2587
|
|
|
2588
2588
|
if (e.key === "Escape") {
|
|
2589
2589
|
if (this.colorDropdownOpen) {
|
|
@@ -2613,7 +2613,8 @@ class AnnotationEditToolbar {
|
|
|
2613
2613
|
this.onComment(this.currentAnnotation);
|
|
2614
2614
|
}
|
|
2615
2615
|
}
|
|
2616
|
-
}
|
|
2616
|
+
};
|
|
2617
|
+
document.addEventListener("keydown", this._keydownHandler);
|
|
2617
2618
|
}
|
|
2618
2619
|
|
|
2619
2620
|
_toggleColorDropdown() {
|
|
@@ -2722,6 +2723,347 @@ class AnnotationEditToolbar {
|
|
|
2722
2723
|
}
|
|
2723
2724
|
|
|
2724
2725
|
destroy() {
|
|
2726
|
+
if (this._keydownHandler) {
|
|
2727
|
+
document.removeEventListener("keydown", this._keydownHandler);
|
|
2728
|
+
}
|
|
2729
|
+
this.element.remove();
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
class AnnotationDetailPanel {
|
|
2734
|
+
constructor(options = {}) {
|
|
2735
|
+
this.container = options.container;
|
|
2736
|
+
this.onColorChange = options.onColorChange;
|
|
2737
|
+
this.onDelete = options.onDelete;
|
|
2738
|
+
this.onEdit = options.onEdit;
|
|
2739
|
+
this.onComment = options.onComment;
|
|
2740
|
+
this.onClose = options.onClose;
|
|
2741
|
+
this.colors = options.colors || ColorPicker.COLORS.map(c => c.value);
|
|
2742
|
+
|
|
2743
|
+
this.currentAnnotation = null;
|
|
2744
|
+
this.anchorElement = null;
|
|
2745
|
+
this.colorDropdownOpen = false;
|
|
2746
|
+
|
|
2747
|
+
this._createPanel();
|
|
2748
|
+
this._setupEventListeners();
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
_createPanel() {
|
|
2752
|
+
this.element = document.createElement("div");
|
|
2753
|
+
this.element.className = "annotation-detail-panel hidden";
|
|
2754
|
+
this.element.innerHTML = `
|
|
2755
|
+
<div class="annotation-detail-header">
|
|
2756
|
+
<div class="annotation-detail-header-info">
|
|
2757
|
+
<span class="annotation-detail-type"></span>
|
|
2758
|
+
</div>
|
|
2759
|
+
<div class="annotation-detail-header-actions">
|
|
2760
|
+
<button class="toolbar-btn comment-btn hidden" title="Add Comment (C)">
|
|
2761
|
+
${Icons.comment}
|
|
2762
|
+
</button>
|
|
2763
|
+
<button class="color-picker-btn" title="Change color" aria-haspopup="true" aria-expanded="false">
|
|
2764
|
+
<span class="color-swatch"></span>
|
|
2765
|
+
${Icons.chevronDown}
|
|
2766
|
+
</button>
|
|
2767
|
+
<div class="color-dropdown hidden">
|
|
2768
|
+
${this.colors.map(color => `
|
|
2769
|
+
<button class="color-option" data-color="${color}" aria-selected="false">
|
|
2770
|
+
<span class="color-swatch" style="background-color: ${color}"></span>
|
|
2771
|
+
</button>
|
|
2772
|
+
`).join("")}
|
|
2773
|
+
</div>
|
|
2774
|
+
<button class="toolbar-btn edit-btn hidden" title="Edit (E)">
|
|
2775
|
+
${Icons.edit}
|
|
2776
|
+
</button>
|
|
2777
|
+
<div class="toolbar-divider"></div>
|
|
2778
|
+
<button class="toolbar-btn delete-btn" title="Delete (Delete)">
|
|
2779
|
+
${Icons.delete}
|
|
2780
|
+
</button>
|
|
2781
|
+
<div class="toolbar-divider"></div>
|
|
2782
|
+
<button class="annotation-detail-close" title="Close (Escape)">
|
|
2783
|
+
${Icons.close}
|
|
2784
|
+
</button>
|
|
2785
|
+
</div>
|
|
2786
|
+
</div>
|
|
2787
|
+
<div class="annotation-detail-body">
|
|
2788
|
+
<div class="annotation-detail-text hidden"></div>
|
|
2789
|
+
<div class="annotation-detail-content-slot"></div>
|
|
2790
|
+
</div>
|
|
2791
|
+
`;
|
|
2792
|
+
|
|
2793
|
+
this.commentBtn = this.element.querySelector(".comment-btn");
|
|
2794
|
+
this.editBtn = this.element.querySelector(".edit-btn");
|
|
2795
|
+
this.typeLabel = this.element.querySelector(".annotation-detail-type");
|
|
2796
|
+
this.textContent = this.element.querySelector(".annotation-detail-text");
|
|
2797
|
+
this.contentSlot = this.element.querySelector(".annotation-detail-content-slot");
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
_setupEventListeners() {
|
|
2801
|
+
// Prevent pointer/click events inside the panel from triggering annotation tools
|
|
2802
|
+
// Tools listen on pointerdown on the pages container, so we must stop all phases
|
|
2803
|
+
this.element.addEventListener("pointerdown", (e) => {
|
|
2804
|
+
e.stopPropagation();
|
|
2805
|
+
});
|
|
2806
|
+
this.element.addEventListener("click", (e) => {
|
|
2807
|
+
e.stopPropagation();
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
// Close button
|
|
2811
|
+
this.element.querySelector(".annotation-detail-close").addEventListener("click", (e) => {
|
|
2812
|
+
e.stopPropagation();
|
|
2813
|
+
this.onClose?.();
|
|
2814
|
+
});
|
|
2815
|
+
|
|
2816
|
+
// Color picker button
|
|
2817
|
+
const colorBtn = this.element.querySelector(".color-picker-btn");
|
|
2818
|
+
colorBtn.addEventListener("click", (e) => {
|
|
2819
|
+
e.stopPropagation();
|
|
2820
|
+
this._toggleColorDropdown();
|
|
2821
|
+
});
|
|
2822
|
+
|
|
2823
|
+
// Color options
|
|
2824
|
+
this.element.querySelectorAll(".color-option").forEach(option => {
|
|
2825
|
+
option.addEventListener("click", (e) => {
|
|
2826
|
+
e.stopPropagation();
|
|
2827
|
+
this._selectColor(option.dataset.color);
|
|
2828
|
+
});
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
// Comment button
|
|
2832
|
+
this.commentBtn.addEventListener("click", (e) => {
|
|
2833
|
+
e.stopPropagation();
|
|
2834
|
+
if (this.currentAnnotation && this.onComment) {
|
|
2835
|
+
this.onComment(this.currentAnnotation);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
|
|
2839
|
+
// Edit button
|
|
2840
|
+
this.editBtn.addEventListener("click", (e) => {
|
|
2841
|
+
e.stopPropagation();
|
|
2842
|
+
if (this.currentAnnotation && this.onEdit) {
|
|
2843
|
+
this.onEdit(this.currentAnnotation);
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
|
|
2847
|
+
// Delete button (hide is handled by _deselectAnnotation cascade)
|
|
2848
|
+
this.element.querySelector(".delete-btn").addEventListener("click", (e) => {
|
|
2849
|
+
e.stopPropagation();
|
|
2850
|
+
if (this.currentAnnotation && this.onDelete) {
|
|
2851
|
+
this.onDelete(this.currentAnnotation);
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
// Close color dropdown on outside click
|
|
2856
|
+
document.addEventListener("click", (e) => {
|
|
2857
|
+
if (this.colorDropdownOpen && !this.element.contains(e.target)) {
|
|
2858
|
+
this._closeColorDropdown();
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
// Keyboard shortcuts
|
|
2863
|
+
this._keydownHandler = (e) => {
|
|
2864
|
+
if (this.element.classList.contains("hidden")) return
|
|
2865
|
+
|
|
2866
|
+
const activeEl = document.activeElement;
|
|
2867
|
+
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
|
|
2868
|
+
|
|
2869
|
+
if (e.key === "Escape") {
|
|
2870
|
+
if (this.colorDropdownOpen) {
|
|
2871
|
+
this._closeColorDropdown();
|
|
2872
|
+
} else {
|
|
2873
|
+
this.onClose?.();
|
|
2874
|
+
}
|
|
2875
|
+
e.preventDefault();
|
|
2876
|
+
} else if (e.key === "Delete" || e.key === "Backspace") {
|
|
2877
|
+
e.preventDefault();
|
|
2878
|
+
if (this.currentAnnotation && this.onDelete) {
|
|
2879
|
+
this.onDelete(this.currentAnnotation);
|
|
2880
|
+
}
|
|
2881
|
+
} else if (e.key === "e" || e.key === "E") {
|
|
2882
|
+
if (this.currentAnnotation?.annotation_type === "note" && this.onEdit) {
|
|
2883
|
+
e.preventDefault();
|
|
2884
|
+
this.onEdit(this.currentAnnotation);
|
|
2885
|
+
}
|
|
2886
|
+
} else if (e.key === "c" || e.key === "C") {
|
|
2887
|
+
const supportsComment = ["highlight", "line", "ink"].includes(this.currentAnnotation?.annotation_type);
|
|
2888
|
+
if (supportsComment && this.onComment) {
|
|
2889
|
+
e.preventDefault();
|
|
2890
|
+
this.onComment(this.currentAnnotation);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
document.addEventListener("keydown", this._keydownHandler);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
_toggleColorDropdown() {
|
|
2898
|
+
if (this.colorDropdownOpen) {
|
|
2899
|
+
this._closeColorDropdown();
|
|
2900
|
+
} else {
|
|
2901
|
+
this._openColorDropdown();
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
_openColorDropdown() {
|
|
2906
|
+
const dropdown = this.element.querySelector(".color-dropdown");
|
|
2907
|
+
const btn = this.element.querySelector(".color-picker-btn");
|
|
2908
|
+
dropdown.classList.remove("hidden");
|
|
2909
|
+
btn.setAttribute("aria-expanded", "true");
|
|
2910
|
+
this.colorDropdownOpen = true;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
_closeColorDropdown() {
|
|
2914
|
+
const dropdown = this.element.querySelector(".color-dropdown");
|
|
2915
|
+
const btn = this.element.querySelector(".color-picker-btn");
|
|
2916
|
+
dropdown.classList.add("hidden");
|
|
2917
|
+
btn.setAttribute("aria-expanded", "false");
|
|
2918
|
+
this.colorDropdownOpen = false;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
_selectColor(color) {
|
|
2922
|
+
if (this.currentAnnotation && this.onColorChange) {
|
|
2923
|
+
this.onColorChange(this.currentAnnotation, color);
|
|
2924
|
+
}
|
|
2925
|
+
this._updateSelectedColor(color);
|
|
2926
|
+
this._closeColorDropdown();
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
_updateSelectedColor(color) {
|
|
2930
|
+
const swatch = this.element.querySelector(".color-picker-btn .color-swatch");
|
|
2931
|
+
swatch.style.backgroundColor = color;
|
|
2932
|
+
|
|
2933
|
+
this.element.querySelectorAll(".color-option").forEach(option => {
|
|
2934
|
+
option.setAttribute("aria-selected", option.dataset.color === color ? "true" : "false");
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
_getTypeLabel(annotationType) {
|
|
2939
|
+
const labels = {
|
|
2940
|
+
highlight: "Highlight",
|
|
2941
|
+
line: "Underline",
|
|
2942
|
+
note: "Note",
|
|
2943
|
+
ink: "Drawing"
|
|
2944
|
+
};
|
|
2945
|
+
return labels[annotationType] || "Annotation"
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
show(annotation, anchorElement, options = {}) {
|
|
2949
|
+
this.currentAnnotation = annotation;
|
|
2950
|
+
this.anchorElement = anchorElement;
|
|
2951
|
+
|
|
2952
|
+
// Update header info
|
|
2953
|
+
const page = annotation.page || 1;
|
|
2954
|
+
this.typeLabel.textContent = `${this._getTypeLabel(annotation.annotation_type)} \u2014 Page ${page}`;
|
|
2955
|
+
|
|
2956
|
+
// Update color swatch
|
|
2957
|
+
const color = annotation.color || ColorPicker.DEFAULT_HIGHLIGHT_COLOR;
|
|
2958
|
+
this._updateSelectedColor(color);
|
|
2959
|
+
|
|
2960
|
+
// Show/hide buttons based on annotation type
|
|
2961
|
+
const isNote = annotation.annotation_type === "note";
|
|
2962
|
+
const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
|
|
2963
|
+
this.commentBtn.classList.toggle("hidden", !supportsComment);
|
|
2964
|
+
this.editBtn.classList.toggle("hidden", !isNote);
|
|
2965
|
+
|
|
2966
|
+
if (supportsComment) {
|
|
2967
|
+
const hasComment = annotation.contents && annotation.contents.trim();
|
|
2968
|
+
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// Show annotation text content if present
|
|
2972
|
+
if (annotation.contents) {
|
|
2973
|
+
this.textContent.textContent = annotation.contents;
|
|
2974
|
+
this.textContent.classList.remove("hidden");
|
|
2975
|
+
} else {
|
|
2976
|
+
this.textContent.textContent = "";
|
|
2977
|
+
this.textContent.classList.add("hidden");
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// Inject custom content if provided (HTML string or DOM element)
|
|
2981
|
+
if (options.content) {
|
|
2982
|
+
if (typeof options.content === "string") {
|
|
2983
|
+
this.contentSlot.innerHTML = options.content;
|
|
2984
|
+
} else {
|
|
2985
|
+
this.contentSlot.innerHTML = "";
|
|
2986
|
+
this.contentSlot.appendChild(options.content);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// Position and show the panel
|
|
2991
|
+
this._positionPanel(anchorElement);
|
|
2992
|
+
this.element.classList.remove("hidden");
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
_positionPanel(anchorElement) {
|
|
2996
|
+
const pageContainer = anchorElement.closest(".pdf-page");
|
|
2997
|
+
if (!pageContainer) return
|
|
2998
|
+
|
|
2999
|
+
// Append to page container so it scrolls with the page
|
|
3000
|
+
pageContainer.appendChild(this.element);
|
|
3001
|
+
|
|
3002
|
+
// Get annotation position relative to the page
|
|
3003
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
3004
|
+
const pageRect = pageContainer.getBoundingClientRect();
|
|
3005
|
+
|
|
3006
|
+
const anchorTop = anchorRect.top - pageRect.top;
|
|
3007
|
+
const anchorLeft = anchorRect.left - pageRect.left;
|
|
3008
|
+
const anchorRight = anchorLeft + anchorRect.width;
|
|
3009
|
+
const pageWidth = pageRect.width;
|
|
3010
|
+
|
|
3011
|
+
const panelWidth = parseInt(
|
|
3012
|
+
getComputedStyle(this.element).getPropertyValue("--panel-width") || "320", 10
|
|
3013
|
+
);
|
|
3014
|
+
|
|
3015
|
+
// Try to position to the right of the annotation
|
|
3016
|
+
if (anchorRight + panelWidth + 12 <= pageWidth) {
|
|
3017
|
+
this.element.style.left = `${anchorRight + 8}px`;
|
|
3018
|
+
this.element.style.right = "auto";
|
|
3019
|
+
}
|
|
3020
|
+
// Fall back to the left side
|
|
3021
|
+
else if (anchorLeft - panelWidth - 12 >= 0) {
|
|
3022
|
+
this.element.style.left = `${anchorLeft - panelWidth - 8}px`;
|
|
3023
|
+
this.element.style.right = "auto";
|
|
3024
|
+
}
|
|
3025
|
+
// Fall back to below the annotation (centered)
|
|
3026
|
+
else {
|
|
3027
|
+
const centerX = anchorLeft + anchorRect.width / 2;
|
|
3028
|
+
this.element.style.left = `${Math.max(8, centerX - panelWidth / 2)}px`;
|
|
3029
|
+
this.element.style.right = "auto";
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Vertical alignment: align top of panel with top of annotation
|
|
3033
|
+
this.element.style.top = `${anchorTop}px`;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
hide() {
|
|
3037
|
+
this._closeColorDropdown();
|
|
3038
|
+
this.element.classList.add("hidden");
|
|
3039
|
+
this.currentAnnotation = null;
|
|
3040
|
+
this.anchorElement = null;
|
|
3041
|
+
|
|
3042
|
+
// Clear text content
|
|
3043
|
+
this.textContent.textContent = "";
|
|
3044
|
+
this.textContent.classList.add("hidden");
|
|
3045
|
+
|
|
3046
|
+
// Clear injected content (but preserve the slot container)
|
|
3047
|
+
this.contentSlot.innerHTML = "";
|
|
3048
|
+
|
|
3049
|
+
// Remove from parent when hidden
|
|
3050
|
+
if (this.element.parentNode) {
|
|
3051
|
+
this.element.parentNode.removeChild(this.element);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
isVisible() {
|
|
3056
|
+
return !this.element.classList.contains("hidden")
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
getContentContainer() {
|
|
3060
|
+
return this.contentSlot
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
destroy() {
|
|
3064
|
+
if (this._keydownHandler) {
|
|
3065
|
+
document.removeEventListener("keydown", this._keydownHandler);
|
|
3066
|
+
}
|
|
2725
3067
|
this.element.remove();
|
|
2726
3068
|
}
|
|
2727
3069
|
}
|
|
@@ -6772,6 +7114,18 @@ class PdfViewer {
|
|
|
6772
7114
|
onDeselect: this._deselectAnnotation.bind(this)
|
|
6773
7115
|
});
|
|
6774
7116
|
|
|
7117
|
+
// Detail panel (opt-in: replaces edit toolbar when enabled)
|
|
7118
|
+
if (this.options.detailPanel && this.bodyContainer) {
|
|
7119
|
+
this.annotationDetailPanel = new AnnotationDetailPanel({
|
|
7120
|
+
container: this.bodyContainer,
|
|
7121
|
+
onColorChange: this._onAnnotationColorChange.bind(this),
|
|
7122
|
+
onDelete: this._onAnnotationDelete.bind(this),
|
|
7123
|
+
onEdit: this._onAnnotationEdit.bind(this),
|
|
7124
|
+
onComment: this._onAnnotationComment.bind(this),
|
|
7125
|
+
onClose: this._deselectAnnotation.bind(this)
|
|
7126
|
+
});
|
|
7127
|
+
}
|
|
7128
|
+
|
|
6775
7129
|
this.undoBar = new UndoBar(this.undoBarContainer, {
|
|
6776
7130
|
onUndo: this._onAnnotationUndo.bind(this)
|
|
6777
7131
|
});
|
|
@@ -6911,8 +7265,8 @@ class PdfViewer {
|
|
|
6911
7265
|
|
|
6912
7266
|
// Deselect annotation when clicking outside
|
|
6913
7267
|
this.pagesContainer.addEventListener("click", (e) => {
|
|
6914
|
-
// Don't deselect if clicking on an annotation or
|
|
6915
|
-
if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar")) {
|
|
7268
|
+
// Don't deselect if clicking on an annotation, edit toolbar, or detail panel
|
|
7269
|
+
if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar") || e.target.closest(".annotation-detail-panel")) {
|
|
6916
7270
|
return
|
|
6917
7271
|
}
|
|
6918
7272
|
// Skip if an annotation was just created (click follows pointerup from text selection)
|
|
@@ -7127,9 +7481,9 @@ class PdfViewer {
|
|
|
7127
7481
|
// Remember if this annotation was selected
|
|
7128
7482
|
const wasSelected = this.selectedAnnotation && this.selectedAnnotation.id === annotation.id;
|
|
7129
7483
|
|
|
7130
|
-
// Hide
|
|
7484
|
+
// Hide annotation UI before re-render (it will be re-shown after)
|
|
7131
7485
|
if (wasSelected) {
|
|
7132
|
-
this.
|
|
7486
|
+
this._hideAnnotationUI();
|
|
7133
7487
|
}
|
|
7134
7488
|
|
|
7135
7489
|
this._renderAnnotationsForPage(annotation.page, this.viewer.getPageContainer(annotation.page));
|
|
@@ -7147,7 +7501,7 @@ class PdfViewer {
|
|
|
7147
7501
|
this.selectedAnnotation = updatedAnnotation;
|
|
7148
7502
|
this.selectedAnnotationElement = element;
|
|
7149
7503
|
element.classList.add("selected");
|
|
7150
|
-
this.
|
|
7504
|
+
this._showAnnotationUI(updatedAnnotation, element);
|
|
7151
7505
|
}
|
|
7152
7506
|
}
|
|
7153
7507
|
}
|
|
@@ -7794,7 +8148,7 @@ class PdfViewer {
|
|
|
7794
8148
|
if (this.selectedAnnotationElement !== element) {
|
|
7795
8149
|
this.selectedAnnotationElement = element;
|
|
7796
8150
|
element.classList.add("selected");
|
|
7797
|
-
this.
|
|
8151
|
+
this._showAnnotationUI(annotation, element, pageHeight);
|
|
7798
8152
|
}
|
|
7799
8153
|
return
|
|
7800
8154
|
}
|
|
@@ -7807,8 +8161,8 @@ class PdfViewer {
|
|
|
7807
8161
|
this.selectedAnnotationElement = element;
|
|
7808
8162
|
element.classList.add("selected");
|
|
7809
8163
|
|
|
7810
|
-
// Show the
|
|
7811
|
-
this.
|
|
8164
|
+
// Show the appropriate annotation UI
|
|
8165
|
+
this._showAnnotationUI(annotation, element, pageHeight);
|
|
7812
8166
|
|
|
7813
8167
|
this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-selected", {
|
|
7814
8168
|
bubbles: true,
|
|
@@ -7816,6 +8170,33 @@ class PdfViewer {
|
|
|
7816
8170
|
}));
|
|
7817
8171
|
}
|
|
7818
8172
|
|
|
8173
|
+
_showAnnotationUI(annotation, element, pageHeight) {
|
|
8174
|
+
if (this.annotationDetailPanel) {
|
|
8175
|
+
this.annotationDetailPanel.show(annotation, element);
|
|
8176
|
+
this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-opened", {
|
|
8177
|
+
bubbles: true,
|
|
8178
|
+
detail: { annotation, element }
|
|
8179
|
+
}));
|
|
8180
|
+
} else {
|
|
8181
|
+
this.annotationEditToolbar.show(annotation, element, pageHeight);
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
8184
|
+
|
|
8185
|
+
_hideAnnotationUI() {
|
|
8186
|
+
if (this.annotationDetailPanel) {
|
|
8187
|
+
const annotation = this.annotationDetailPanel.currentAnnotation;
|
|
8188
|
+
this.annotationDetailPanel.hide();
|
|
8189
|
+
if (annotation) {
|
|
8190
|
+
this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-closed", {
|
|
8191
|
+
bubbles: true,
|
|
8192
|
+
detail: { annotation, annotationId: annotation.id }
|
|
8193
|
+
}));
|
|
8194
|
+
}
|
|
8195
|
+
} else {
|
|
8196
|
+
this.annotationEditToolbar.hide();
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
|
|
7819
8200
|
_deselectAnnotation() {
|
|
7820
8201
|
const previousAnnotation = this.selectedAnnotation;
|
|
7821
8202
|
|
|
@@ -7825,8 +8206,8 @@ class PdfViewer {
|
|
|
7825
8206
|
this.selectedAnnotation = null;
|
|
7826
8207
|
this.selectedAnnotationElement = null;
|
|
7827
8208
|
|
|
7828
|
-
// Hide the edit toolbar
|
|
7829
|
-
this.
|
|
8209
|
+
// Hide the annotation UI (detail panel or edit toolbar)
|
|
8210
|
+
this._hideAnnotationUI();
|
|
7830
8211
|
|
|
7831
8212
|
if (previousAnnotation) {
|
|
7832
8213
|
this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-deselected", {
|
|
@@ -8001,6 +8382,7 @@ class PdfViewer {
|
|
|
8001
8382
|
|
|
8002
8383
|
this.viewer.destroy();
|
|
8003
8384
|
this.annotationEditToolbar.destroy();
|
|
8385
|
+
this.annotationDetailPanel?.destroy();
|
|
8004
8386
|
this.undoBar.destroy();
|
|
8005
8387
|
this.thumbnailSidebar?.destroy();
|
|
8006
8388
|
this.annotationSidebar?.destroy();
|
|
@@ -8027,7 +8409,8 @@ class pdf_viewer_controller extends Controller {
|
|
|
8027
8409
|
trackingUrl: String,
|
|
8028
8410
|
initialPage: Number,
|
|
8029
8411
|
initialAnnotation: String,
|
|
8030
|
-
autoHeight: { type: Boolean, default: true }
|
|
8412
|
+
autoHeight: { type: Boolean, default: true },
|
|
8413
|
+
detailPanel: { type: Boolean, default: false }
|
|
8031
8414
|
}
|
|
8032
8415
|
|
|
8033
8416
|
initialize() {
|
|
@@ -8048,7 +8431,8 @@ class pdf_viewer_controller extends Controller {
|
|
|
8048
8431
|
userName: this.userNameValue,
|
|
8049
8432
|
documentId: this.documentIdValue,
|
|
8050
8433
|
initialPage: this.initialPageValue || 1,
|
|
8051
|
-
initialAnnotation: this.initialAnnotationValue
|
|
8434
|
+
initialAnnotation: this.initialAnnotationValue,
|
|
8435
|
+
detailPanel: this.detailPanelValue
|
|
8052
8436
|
});
|
|
8053
8437
|
|
|
8054
8438
|
// Set up the toolbar
|
|
@@ -2598,12 +2598,12 @@
|
|
|
2598
2598
|
});
|
|
2599
2599
|
|
|
2600
2600
|
// Handle keyboard shortcuts
|
|
2601
|
-
|
|
2601
|
+
this._keydownHandler = (e) => {
|
|
2602
2602
|
if (this.element.classList.contains("hidden")) return
|
|
2603
2603
|
|
|
2604
2604
|
// Don't intercept if user is typing
|
|
2605
2605
|
const activeEl = document.activeElement;
|
|
2606
|
-
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA")) return
|
|
2606
|
+
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
|
|
2607
2607
|
|
|
2608
2608
|
if (e.key === "Escape") {
|
|
2609
2609
|
if (this.colorDropdownOpen) {
|
|
@@ -2633,7 +2633,8 @@
|
|
|
2633
2633
|
this.onComment(this.currentAnnotation);
|
|
2634
2634
|
}
|
|
2635
2635
|
}
|
|
2636
|
-
}
|
|
2636
|
+
};
|
|
2637
|
+
document.addEventListener("keydown", this._keydownHandler);
|
|
2637
2638
|
}
|
|
2638
2639
|
|
|
2639
2640
|
_toggleColorDropdown() {
|
|
@@ -2742,6 +2743,347 @@
|
|
|
2742
2743
|
}
|
|
2743
2744
|
|
|
2744
2745
|
destroy() {
|
|
2746
|
+
if (this._keydownHandler) {
|
|
2747
|
+
document.removeEventListener("keydown", this._keydownHandler);
|
|
2748
|
+
}
|
|
2749
|
+
this.element.remove();
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
class AnnotationDetailPanel {
|
|
2754
|
+
constructor(options = {}) {
|
|
2755
|
+
this.container = options.container;
|
|
2756
|
+
this.onColorChange = options.onColorChange;
|
|
2757
|
+
this.onDelete = options.onDelete;
|
|
2758
|
+
this.onEdit = options.onEdit;
|
|
2759
|
+
this.onComment = options.onComment;
|
|
2760
|
+
this.onClose = options.onClose;
|
|
2761
|
+
this.colors = options.colors || ColorPicker.COLORS.map(c => c.value);
|
|
2762
|
+
|
|
2763
|
+
this.currentAnnotation = null;
|
|
2764
|
+
this.anchorElement = null;
|
|
2765
|
+
this.colorDropdownOpen = false;
|
|
2766
|
+
|
|
2767
|
+
this._createPanel();
|
|
2768
|
+
this._setupEventListeners();
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
_createPanel() {
|
|
2772
|
+
this.element = document.createElement("div");
|
|
2773
|
+
this.element.className = "annotation-detail-panel hidden";
|
|
2774
|
+
this.element.innerHTML = `
|
|
2775
|
+
<div class="annotation-detail-header">
|
|
2776
|
+
<div class="annotation-detail-header-info">
|
|
2777
|
+
<span class="annotation-detail-type"></span>
|
|
2778
|
+
</div>
|
|
2779
|
+
<div class="annotation-detail-header-actions">
|
|
2780
|
+
<button class="toolbar-btn comment-btn hidden" title="Add Comment (C)">
|
|
2781
|
+
${Icons.comment}
|
|
2782
|
+
</button>
|
|
2783
|
+
<button class="color-picker-btn" title="Change color" aria-haspopup="true" aria-expanded="false">
|
|
2784
|
+
<span class="color-swatch"></span>
|
|
2785
|
+
${Icons.chevronDown}
|
|
2786
|
+
</button>
|
|
2787
|
+
<div class="color-dropdown hidden">
|
|
2788
|
+
${this.colors.map(color => `
|
|
2789
|
+
<button class="color-option" data-color="${color}" aria-selected="false">
|
|
2790
|
+
<span class="color-swatch" style="background-color: ${color}"></span>
|
|
2791
|
+
</button>
|
|
2792
|
+
`).join("")}
|
|
2793
|
+
</div>
|
|
2794
|
+
<button class="toolbar-btn edit-btn hidden" title="Edit (E)">
|
|
2795
|
+
${Icons.edit}
|
|
2796
|
+
</button>
|
|
2797
|
+
<div class="toolbar-divider"></div>
|
|
2798
|
+
<button class="toolbar-btn delete-btn" title="Delete (Delete)">
|
|
2799
|
+
${Icons.delete}
|
|
2800
|
+
</button>
|
|
2801
|
+
<div class="toolbar-divider"></div>
|
|
2802
|
+
<button class="annotation-detail-close" title="Close (Escape)">
|
|
2803
|
+
${Icons.close}
|
|
2804
|
+
</button>
|
|
2805
|
+
</div>
|
|
2806
|
+
</div>
|
|
2807
|
+
<div class="annotation-detail-body">
|
|
2808
|
+
<div class="annotation-detail-text hidden"></div>
|
|
2809
|
+
<div class="annotation-detail-content-slot"></div>
|
|
2810
|
+
</div>
|
|
2811
|
+
`;
|
|
2812
|
+
|
|
2813
|
+
this.commentBtn = this.element.querySelector(".comment-btn");
|
|
2814
|
+
this.editBtn = this.element.querySelector(".edit-btn");
|
|
2815
|
+
this.typeLabel = this.element.querySelector(".annotation-detail-type");
|
|
2816
|
+
this.textContent = this.element.querySelector(".annotation-detail-text");
|
|
2817
|
+
this.contentSlot = this.element.querySelector(".annotation-detail-content-slot");
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
_setupEventListeners() {
|
|
2821
|
+
// Prevent pointer/click events inside the panel from triggering annotation tools
|
|
2822
|
+
// Tools listen on pointerdown on the pages container, so we must stop all phases
|
|
2823
|
+
this.element.addEventListener("pointerdown", (e) => {
|
|
2824
|
+
e.stopPropagation();
|
|
2825
|
+
});
|
|
2826
|
+
this.element.addEventListener("click", (e) => {
|
|
2827
|
+
e.stopPropagation();
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
// Close button
|
|
2831
|
+
this.element.querySelector(".annotation-detail-close").addEventListener("click", (e) => {
|
|
2832
|
+
e.stopPropagation();
|
|
2833
|
+
this.onClose?.();
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
// Color picker button
|
|
2837
|
+
const colorBtn = this.element.querySelector(".color-picker-btn");
|
|
2838
|
+
colorBtn.addEventListener("click", (e) => {
|
|
2839
|
+
e.stopPropagation();
|
|
2840
|
+
this._toggleColorDropdown();
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
// Color options
|
|
2844
|
+
this.element.querySelectorAll(".color-option").forEach(option => {
|
|
2845
|
+
option.addEventListener("click", (e) => {
|
|
2846
|
+
e.stopPropagation();
|
|
2847
|
+
this._selectColor(option.dataset.color);
|
|
2848
|
+
});
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
// Comment button
|
|
2852
|
+
this.commentBtn.addEventListener("click", (e) => {
|
|
2853
|
+
e.stopPropagation();
|
|
2854
|
+
if (this.currentAnnotation && this.onComment) {
|
|
2855
|
+
this.onComment(this.currentAnnotation);
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
// Edit button
|
|
2860
|
+
this.editBtn.addEventListener("click", (e) => {
|
|
2861
|
+
e.stopPropagation();
|
|
2862
|
+
if (this.currentAnnotation && this.onEdit) {
|
|
2863
|
+
this.onEdit(this.currentAnnotation);
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
// Delete button (hide is handled by _deselectAnnotation cascade)
|
|
2868
|
+
this.element.querySelector(".delete-btn").addEventListener("click", (e) => {
|
|
2869
|
+
e.stopPropagation();
|
|
2870
|
+
if (this.currentAnnotation && this.onDelete) {
|
|
2871
|
+
this.onDelete(this.currentAnnotation);
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
|
|
2875
|
+
// Close color dropdown on outside click
|
|
2876
|
+
document.addEventListener("click", (e) => {
|
|
2877
|
+
if (this.colorDropdownOpen && !this.element.contains(e.target)) {
|
|
2878
|
+
this._closeColorDropdown();
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
// Keyboard shortcuts
|
|
2883
|
+
this._keydownHandler = (e) => {
|
|
2884
|
+
if (this.element.classList.contains("hidden")) return
|
|
2885
|
+
|
|
2886
|
+
const activeEl = document.activeElement;
|
|
2887
|
+
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
|
|
2888
|
+
|
|
2889
|
+
if (e.key === "Escape") {
|
|
2890
|
+
if (this.colorDropdownOpen) {
|
|
2891
|
+
this._closeColorDropdown();
|
|
2892
|
+
} else {
|
|
2893
|
+
this.onClose?.();
|
|
2894
|
+
}
|
|
2895
|
+
e.preventDefault();
|
|
2896
|
+
} else if (e.key === "Delete" || e.key === "Backspace") {
|
|
2897
|
+
e.preventDefault();
|
|
2898
|
+
if (this.currentAnnotation && this.onDelete) {
|
|
2899
|
+
this.onDelete(this.currentAnnotation);
|
|
2900
|
+
}
|
|
2901
|
+
} else if (e.key === "e" || e.key === "E") {
|
|
2902
|
+
if (this.currentAnnotation?.annotation_type === "note" && this.onEdit) {
|
|
2903
|
+
e.preventDefault();
|
|
2904
|
+
this.onEdit(this.currentAnnotation);
|
|
2905
|
+
}
|
|
2906
|
+
} else if (e.key === "c" || e.key === "C") {
|
|
2907
|
+
const supportsComment = ["highlight", "line", "ink"].includes(this.currentAnnotation?.annotation_type);
|
|
2908
|
+
if (supportsComment && this.onComment) {
|
|
2909
|
+
e.preventDefault();
|
|
2910
|
+
this.onComment(this.currentAnnotation);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
document.addEventListener("keydown", this._keydownHandler);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
_toggleColorDropdown() {
|
|
2918
|
+
if (this.colorDropdownOpen) {
|
|
2919
|
+
this._closeColorDropdown();
|
|
2920
|
+
} else {
|
|
2921
|
+
this._openColorDropdown();
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
_openColorDropdown() {
|
|
2926
|
+
const dropdown = this.element.querySelector(".color-dropdown");
|
|
2927
|
+
const btn = this.element.querySelector(".color-picker-btn");
|
|
2928
|
+
dropdown.classList.remove("hidden");
|
|
2929
|
+
btn.setAttribute("aria-expanded", "true");
|
|
2930
|
+
this.colorDropdownOpen = true;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
_closeColorDropdown() {
|
|
2934
|
+
const dropdown = this.element.querySelector(".color-dropdown");
|
|
2935
|
+
const btn = this.element.querySelector(".color-picker-btn");
|
|
2936
|
+
dropdown.classList.add("hidden");
|
|
2937
|
+
btn.setAttribute("aria-expanded", "false");
|
|
2938
|
+
this.colorDropdownOpen = false;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
_selectColor(color) {
|
|
2942
|
+
if (this.currentAnnotation && this.onColorChange) {
|
|
2943
|
+
this.onColorChange(this.currentAnnotation, color);
|
|
2944
|
+
}
|
|
2945
|
+
this._updateSelectedColor(color);
|
|
2946
|
+
this._closeColorDropdown();
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
_updateSelectedColor(color) {
|
|
2950
|
+
const swatch = this.element.querySelector(".color-picker-btn .color-swatch");
|
|
2951
|
+
swatch.style.backgroundColor = color;
|
|
2952
|
+
|
|
2953
|
+
this.element.querySelectorAll(".color-option").forEach(option => {
|
|
2954
|
+
option.setAttribute("aria-selected", option.dataset.color === color ? "true" : "false");
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
_getTypeLabel(annotationType) {
|
|
2959
|
+
const labels = {
|
|
2960
|
+
highlight: "Highlight",
|
|
2961
|
+
line: "Underline",
|
|
2962
|
+
note: "Note",
|
|
2963
|
+
ink: "Drawing"
|
|
2964
|
+
};
|
|
2965
|
+
return labels[annotationType] || "Annotation"
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
show(annotation, anchorElement, options = {}) {
|
|
2969
|
+
this.currentAnnotation = annotation;
|
|
2970
|
+
this.anchorElement = anchorElement;
|
|
2971
|
+
|
|
2972
|
+
// Update header info
|
|
2973
|
+
const page = annotation.page || 1;
|
|
2974
|
+
this.typeLabel.textContent = `${this._getTypeLabel(annotation.annotation_type)} \u2014 Page ${page}`;
|
|
2975
|
+
|
|
2976
|
+
// Update color swatch
|
|
2977
|
+
const color = annotation.color || ColorPicker.DEFAULT_HIGHLIGHT_COLOR;
|
|
2978
|
+
this._updateSelectedColor(color);
|
|
2979
|
+
|
|
2980
|
+
// Show/hide buttons based on annotation type
|
|
2981
|
+
const isNote = annotation.annotation_type === "note";
|
|
2982
|
+
const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
|
|
2983
|
+
this.commentBtn.classList.toggle("hidden", !supportsComment);
|
|
2984
|
+
this.editBtn.classList.toggle("hidden", !isNote);
|
|
2985
|
+
|
|
2986
|
+
if (supportsComment) {
|
|
2987
|
+
const hasComment = annotation.contents && annotation.contents.trim();
|
|
2988
|
+
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// Show annotation text content if present
|
|
2992
|
+
if (annotation.contents) {
|
|
2993
|
+
this.textContent.textContent = annotation.contents;
|
|
2994
|
+
this.textContent.classList.remove("hidden");
|
|
2995
|
+
} else {
|
|
2996
|
+
this.textContent.textContent = "";
|
|
2997
|
+
this.textContent.classList.add("hidden");
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// Inject custom content if provided (HTML string or DOM element)
|
|
3001
|
+
if (options.content) {
|
|
3002
|
+
if (typeof options.content === "string") {
|
|
3003
|
+
this.contentSlot.innerHTML = options.content;
|
|
3004
|
+
} else {
|
|
3005
|
+
this.contentSlot.innerHTML = "";
|
|
3006
|
+
this.contentSlot.appendChild(options.content);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// Position and show the panel
|
|
3011
|
+
this._positionPanel(anchorElement);
|
|
3012
|
+
this.element.classList.remove("hidden");
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
_positionPanel(anchorElement) {
|
|
3016
|
+
const pageContainer = anchorElement.closest(".pdf-page");
|
|
3017
|
+
if (!pageContainer) return
|
|
3018
|
+
|
|
3019
|
+
// Append to page container so it scrolls with the page
|
|
3020
|
+
pageContainer.appendChild(this.element);
|
|
3021
|
+
|
|
3022
|
+
// Get annotation position relative to the page
|
|
3023
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
3024
|
+
const pageRect = pageContainer.getBoundingClientRect();
|
|
3025
|
+
|
|
3026
|
+
const anchorTop = anchorRect.top - pageRect.top;
|
|
3027
|
+
const anchorLeft = anchorRect.left - pageRect.left;
|
|
3028
|
+
const anchorRight = anchorLeft + anchorRect.width;
|
|
3029
|
+
const pageWidth = pageRect.width;
|
|
3030
|
+
|
|
3031
|
+
const panelWidth = parseInt(
|
|
3032
|
+
getComputedStyle(this.element).getPropertyValue("--panel-width") || "320", 10
|
|
3033
|
+
);
|
|
3034
|
+
|
|
3035
|
+
// Try to position to the right of the annotation
|
|
3036
|
+
if (anchorRight + panelWidth + 12 <= pageWidth) {
|
|
3037
|
+
this.element.style.left = `${anchorRight + 8}px`;
|
|
3038
|
+
this.element.style.right = "auto";
|
|
3039
|
+
}
|
|
3040
|
+
// Fall back to the left side
|
|
3041
|
+
else if (anchorLeft - panelWidth - 12 >= 0) {
|
|
3042
|
+
this.element.style.left = `${anchorLeft - panelWidth - 8}px`;
|
|
3043
|
+
this.element.style.right = "auto";
|
|
3044
|
+
}
|
|
3045
|
+
// Fall back to below the annotation (centered)
|
|
3046
|
+
else {
|
|
3047
|
+
const centerX = anchorLeft + anchorRect.width / 2;
|
|
3048
|
+
this.element.style.left = `${Math.max(8, centerX - panelWidth / 2)}px`;
|
|
3049
|
+
this.element.style.right = "auto";
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
// Vertical alignment: align top of panel with top of annotation
|
|
3053
|
+
this.element.style.top = `${anchorTop}px`;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
hide() {
|
|
3057
|
+
this._closeColorDropdown();
|
|
3058
|
+
this.element.classList.add("hidden");
|
|
3059
|
+
this.currentAnnotation = null;
|
|
3060
|
+
this.anchorElement = null;
|
|
3061
|
+
|
|
3062
|
+
// Clear text content
|
|
3063
|
+
this.textContent.textContent = "";
|
|
3064
|
+
this.textContent.classList.add("hidden");
|
|
3065
|
+
|
|
3066
|
+
// Clear injected content (but preserve the slot container)
|
|
3067
|
+
this.contentSlot.innerHTML = "";
|
|
3068
|
+
|
|
3069
|
+
// Remove from parent when hidden
|
|
3070
|
+
if (this.element.parentNode) {
|
|
3071
|
+
this.element.parentNode.removeChild(this.element);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
isVisible() {
|
|
3076
|
+
return !this.element.classList.contains("hidden")
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
getContentContainer() {
|
|
3080
|
+
return this.contentSlot
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
destroy() {
|
|
3084
|
+
if (this._keydownHandler) {
|
|
3085
|
+
document.removeEventListener("keydown", this._keydownHandler);
|
|
3086
|
+
}
|
|
2745
3087
|
this.element.remove();
|
|
2746
3088
|
}
|
|
2747
3089
|
}
|
|
@@ -6792,6 +7134,18 @@
|
|
|
6792
7134
|
onDeselect: this._deselectAnnotation.bind(this)
|
|
6793
7135
|
});
|
|
6794
7136
|
|
|
7137
|
+
// Detail panel (opt-in: replaces edit toolbar when enabled)
|
|
7138
|
+
if (this.options.detailPanel && this.bodyContainer) {
|
|
7139
|
+
this.annotationDetailPanel = new AnnotationDetailPanel({
|
|
7140
|
+
container: this.bodyContainer,
|
|
7141
|
+
onColorChange: this._onAnnotationColorChange.bind(this),
|
|
7142
|
+
onDelete: this._onAnnotationDelete.bind(this),
|
|
7143
|
+
onEdit: this._onAnnotationEdit.bind(this),
|
|
7144
|
+
onComment: this._onAnnotationComment.bind(this),
|
|
7145
|
+
onClose: this._deselectAnnotation.bind(this)
|
|
7146
|
+
});
|
|
7147
|
+
}
|
|
7148
|
+
|
|
6795
7149
|
this.undoBar = new UndoBar(this.undoBarContainer, {
|
|
6796
7150
|
onUndo: this._onAnnotationUndo.bind(this)
|
|
6797
7151
|
});
|
|
@@ -6931,8 +7285,8 @@
|
|
|
6931
7285
|
|
|
6932
7286
|
// Deselect annotation when clicking outside
|
|
6933
7287
|
this.pagesContainer.addEventListener("click", (e) => {
|
|
6934
|
-
// Don't deselect if clicking on an annotation or
|
|
6935
|
-
if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar")) {
|
|
7288
|
+
// Don't deselect if clicking on an annotation, edit toolbar, or detail panel
|
|
7289
|
+
if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar") || e.target.closest(".annotation-detail-panel")) {
|
|
6936
7290
|
return
|
|
6937
7291
|
}
|
|
6938
7292
|
// Skip if an annotation was just created (click follows pointerup from text selection)
|
|
@@ -7147,9 +7501,9 @@
|
|
|
7147
7501
|
// Remember if this annotation was selected
|
|
7148
7502
|
const wasSelected = this.selectedAnnotation && this.selectedAnnotation.id === annotation.id;
|
|
7149
7503
|
|
|
7150
|
-
// Hide
|
|
7504
|
+
// Hide annotation UI before re-render (it will be re-shown after)
|
|
7151
7505
|
if (wasSelected) {
|
|
7152
|
-
this.
|
|
7506
|
+
this._hideAnnotationUI();
|
|
7153
7507
|
}
|
|
7154
7508
|
|
|
7155
7509
|
this._renderAnnotationsForPage(annotation.page, this.viewer.getPageContainer(annotation.page));
|
|
@@ -7167,7 +7521,7 @@
|
|
|
7167
7521
|
this.selectedAnnotation = updatedAnnotation;
|
|
7168
7522
|
this.selectedAnnotationElement = element;
|
|
7169
7523
|
element.classList.add("selected");
|
|
7170
|
-
this.
|
|
7524
|
+
this._showAnnotationUI(updatedAnnotation, element);
|
|
7171
7525
|
}
|
|
7172
7526
|
}
|
|
7173
7527
|
}
|
|
@@ -7814,7 +8168,7 @@
|
|
|
7814
8168
|
if (this.selectedAnnotationElement !== element) {
|
|
7815
8169
|
this.selectedAnnotationElement = element;
|
|
7816
8170
|
element.classList.add("selected");
|
|
7817
|
-
this.
|
|
8171
|
+
this._showAnnotationUI(annotation, element, pageHeight);
|
|
7818
8172
|
}
|
|
7819
8173
|
return
|
|
7820
8174
|
}
|
|
@@ -7827,8 +8181,8 @@
|
|
|
7827
8181
|
this.selectedAnnotationElement = element;
|
|
7828
8182
|
element.classList.add("selected");
|
|
7829
8183
|
|
|
7830
|
-
// Show the
|
|
7831
|
-
this.
|
|
8184
|
+
// Show the appropriate annotation UI
|
|
8185
|
+
this._showAnnotationUI(annotation, element, pageHeight);
|
|
7832
8186
|
|
|
7833
8187
|
this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-selected", {
|
|
7834
8188
|
bubbles: true,
|
|
@@ -7836,6 +8190,33 @@
|
|
|
7836
8190
|
}));
|
|
7837
8191
|
}
|
|
7838
8192
|
|
|
8193
|
+
_showAnnotationUI(annotation, element, pageHeight) {
|
|
8194
|
+
if (this.annotationDetailPanel) {
|
|
8195
|
+
this.annotationDetailPanel.show(annotation, element);
|
|
8196
|
+
this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-opened", {
|
|
8197
|
+
bubbles: true,
|
|
8198
|
+
detail: { annotation, element }
|
|
8199
|
+
}));
|
|
8200
|
+
} else {
|
|
8201
|
+
this.annotationEditToolbar.show(annotation, element, pageHeight);
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
8204
|
+
|
|
8205
|
+
_hideAnnotationUI() {
|
|
8206
|
+
if (this.annotationDetailPanel) {
|
|
8207
|
+
const annotation = this.annotationDetailPanel.currentAnnotation;
|
|
8208
|
+
this.annotationDetailPanel.hide();
|
|
8209
|
+
if (annotation) {
|
|
8210
|
+
this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-closed", {
|
|
8211
|
+
bubbles: true,
|
|
8212
|
+
detail: { annotation, annotationId: annotation.id }
|
|
8213
|
+
}));
|
|
8214
|
+
}
|
|
8215
|
+
} else {
|
|
8216
|
+
this.annotationEditToolbar.hide();
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
|
|
7839
8220
|
_deselectAnnotation() {
|
|
7840
8221
|
const previousAnnotation = this.selectedAnnotation;
|
|
7841
8222
|
|
|
@@ -7845,8 +8226,8 @@
|
|
|
7845
8226
|
this.selectedAnnotation = null;
|
|
7846
8227
|
this.selectedAnnotationElement = null;
|
|
7847
8228
|
|
|
7848
|
-
// Hide the edit toolbar
|
|
7849
|
-
this.
|
|
8229
|
+
// Hide the annotation UI (detail panel or edit toolbar)
|
|
8230
|
+
this._hideAnnotationUI();
|
|
7850
8231
|
|
|
7851
8232
|
if (previousAnnotation) {
|
|
7852
8233
|
this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-deselected", {
|
|
@@ -8021,6 +8402,7 @@
|
|
|
8021
8402
|
|
|
8022
8403
|
this.viewer.destroy();
|
|
8023
8404
|
this.annotationEditToolbar.destroy();
|
|
8405
|
+
this.annotationDetailPanel?.destroy();
|
|
8024
8406
|
this.undoBar.destroy();
|
|
8025
8407
|
this.thumbnailSidebar?.destroy();
|
|
8026
8408
|
this.annotationSidebar?.destroy();
|
|
@@ -8047,7 +8429,8 @@
|
|
|
8047
8429
|
trackingUrl: String,
|
|
8048
8430
|
initialPage: Number,
|
|
8049
8431
|
initialAnnotation: String,
|
|
8050
|
-
autoHeight: { type: Boolean, default: true }
|
|
8432
|
+
autoHeight: { type: Boolean, default: true },
|
|
8433
|
+
detailPanel: { type: Boolean, default: false }
|
|
8051
8434
|
}
|
|
8052
8435
|
|
|
8053
8436
|
initialize() {
|
|
@@ -8068,7 +8451,8 @@
|
|
|
8068
8451
|
userName: this.userNameValue,
|
|
8069
8452
|
documentId: this.documentIdValue,
|
|
8070
8453
|
initialPage: this.initialPageValue || 1,
|
|
8071
|
-
initialAnnotation: this.initialAnnotationValue
|
|
8454
|
+
initialAnnotation: this.initialAnnotationValue,
|
|
8455
|
+
detailPanel: this.detailPanelValue
|
|
8072
8456
|
});
|
|
8073
8457
|
|
|
8074
8458
|
// Set up the toolbar
|
|
@@ -877,6 +877,214 @@
|
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
+
// Annotation Detail Panel (anchored card for annotation threads)
|
|
881
|
+
.annotation-detail-panel {
|
|
882
|
+
--panel-bg: #ffffff;
|
|
883
|
+
--panel-border: #c8c8d0;
|
|
884
|
+
--panel-shadow: 0 4px 12px rgba(58, 57, 68, 0.15);
|
|
885
|
+
--panel-header-bg: #f0f0f4;
|
|
886
|
+
--panel-fg: #2e2e56;
|
|
887
|
+
--panel-fg-secondary: #5b5b66;
|
|
888
|
+
--panel-hover-bg: #e0e0e6;
|
|
889
|
+
--panel-focus-color: #0060df;
|
|
890
|
+
--panel-width: 320px;
|
|
891
|
+
|
|
892
|
+
position: absolute;
|
|
893
|
+
z-index: 1000;
|
|
894
|
+
width: var(--panel-width);
|
|
895
|
+
display: flex;
|
|
896
|
+
flex-direction: column;
|
|
897
|
+
background: var(--panel-bg);
|
|
898
|
+
border: 1px solid var(--panel-border);
|
|
899
|
+
border-radius: 8px;
|
|
900
|
+
box-shadow: var(--panel-shadow);
|
|
901
|
+
pointer-events: auto;
|
|
902
|
+
cursor: default;
|
|
903
|
+
|
|
904
|
+
.annotation-detail-header {
|
|
905
|
+
display: flex;
|
|
906
|
+
align-items: center;
|
|
907
|
+
justify-content: space-between;
|
|
908
|
+
gap: 4px;
|
|
909
|
+
padding: 6px 4px 6px 12px;
|
|
910
|
+
background: var(--panel-header-bg);
|
|
911
|
+
border-bottom: 1px solid var(--panel-border);
|
|
912
|
+
min-height: 36px;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.annotation-detail-header-info {
|
|
916
|
+
flex: 1;
|
|
917
|
+
min-width: 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.annotation-detail-type {
|
|
921
|
+
font-size: 12px;
|
|
922
|
+
font-weight: 500;
|
|
923
|
+
color: var(--panel-fg-secondary);
|
|
924
|
+
white-space: nowrap;
|
|
925
|
+
overflow: hidden;
|
|
926
|
+
text-overflow: ellipsis;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.annotation-detail-header-actions {
|
|
930
|
+
position: relative;
|
|
931
|
+
display: flex;
|
|
932
|
+
align-items: center;
|
|
933
|
+
gap: 0;
|
|
934
|
+
flex-shrink: 0;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.toolbar-btn {
|
|
938
|
+
display: flex;
|
|
939
|
+
align-items: center;
|
|
940
|
+
justify-content: center;
|
|
941
|
+
width: 28px;
|
|
942
|
+
height: 28px;
|
|
943
|
+
padding: 0;
|
|
944
|
+
border: none;
|
|
945
|
+
border-radius: 4px;
|
|
946
|
+
background: transparent;
|
|
947
|
+
color: var(--panel-fg);
|
|
948
|
+
cursor: pointer;
|
|
949
|
+
|
|
950
|
+
&:hover {
|
|
951
|
+
background: var(--panel-hover-bg);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
&:focus-visible {
|
|
955
|
+
outline: 2px solid var(--panel-focus-color);
|
|
956
|
+
outline-offset: -2px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
svg {
|
|
960
|
+
width: 16px;
|
|
961
|
+
height: 16px;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.toolbar-divider {
|
|
966
|
+
width: 0;
|
|
967
|
+
height: 20px;
|
|
968
|
+
border-left: 1px solid var(--panel-border);
|
|
969
|
+
margin: 0 2px;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.color-picker-btn {
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
justify-content: center;
|
|
976
|
+
gap: 3px;
|
|
977
|
+
padding: 0 6px 0 8px;
|
|
978
|
+
height: 28px;
|
|
979
|
+
border: none;
|
|
980
|
+
border-radius: 4px;
|
|
981
|
+
background: transparent;
|
|
982
|
+
cursor: pointer;
|
|
983
|
+
|
|
984
|
+
&:hover {
|
|
985
|
+
background: var(--panel-hover-bg);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.color-swatch {
|
|
989
|
+
width: 14px;
|
|
990
|
+
height: 14px;
|
|
991
|
+
flex-shrink: 0;
|
|
992
|
+
border-radius: 50%;
|
|
993
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.color-dropdown {
|
|
998
|
+
position: absolute;
|
|
999
|
+
top: calc(100% + 4px);
|
|
1000
|
+
right: 0;
|
|
1001
|
+
display: flex;
|
|
1002
|
+
flex-direction: row;
|
|
1003
|
+
gap: 8px;
|
|
1004
|
+
padding: 8px;
|
|
1005
|
+
background: var(--panel-header-bg);
|
|
1006
|
+
border: 1px solid var(--panel-border);
|
|
1007
|
+
border-radius: 6px;
|
|
1008
|
+
box-shadow: var(--panel-shadow);
|
|
1009
|
+
white-space: nowrap;
|
|
1010
|
+
z-index: 1;
|
|
1011
|
+
|
|
1012
|
+
.color-option {
|
|
1013
|
+
width: auto;
|
|
1014
|
+
height: auto;
|
|
1015
|
+
padding: 0;
|
|
1016
|
+
border: none;
|
|
1017
|
+
background: transparent;
|
|
1018
|
+
cursor: pointer;
|
|
1019
|
+
display: flex;
|
|
1020
|
+
justify-content: center;
|
|
1021
|
+
|
|
1022
|
+
.color-swatch {
|
|
1023
|
+
width: 16px;
|
|
1024
|
+
height: 16px;
|
|
1025
|
+
border-radius: 50%;
|
|
1026
|
+
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
1027
|
+
outline-offset: 2px;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
&:hover .color-swatch,
|
|
1031
|
+
&:focus .color-swatch {
|
|
1032
|
+
outline: 2px solid var(--panel-focus-color);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
&[aria-selected="true"] .color-swatch {
|
|
1036
|
+
outline: 2px solid var(--panel-focus-color);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.annotation-detail-close {
|
|
1042
|
+
display: flex;
|
|
1043
|
+
align-items: center;
|
|
1044
|
+
justify-content: center;
|
|
1045
|
+
width: 28px;
|
|
1046
|
+
height: 28px;
|
|
1047
|
+
padding: 0;
|
|
1048
|
+
border: none;
|
|
1049
|
+
border-radius: 4px;
|
|
1050
|
+
background: transparent;
|
|
1051
|
+
color: var(--panel-fg-secondary);
|
|
1052
|
+
cursor: pointer;
|
|
1053
|
+
|
|
1054
|
+
&:hover {
|
|
1055
|
+
background: var(--panel-hover-bg);
|
|
1056
|
+
color: var(--panel-fg);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
svg {
|
|
1060
|
+
width: 14px;
|
|
1061
|
+
height: 14px;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.annotation-detail-body {
|
|
1066
|
+
max-height: 400px;
|
|
1067
|
+
overflow-y: auto;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.annotation-detail-text {
|
|
1071
|
+
padding: 10px 12px;
|
|
1072
|
+
font-size: 14px;
|
|
1073
|
+
line-height: 1.5;
|
|
1074
|
+
color: var(--panel-fg);
|
|
1075
|
+
white-space: pre-wrap;
|
|
1076
|
+
word-break: break-word;
|
|
1077
|
+
border-bottom: 1px solid var(--panel-border);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.annotation-detail-content-slot {
|
|
1081
|
+
// Host app injects content here (e.g. Turbo Frames)
|
|
1082
|
+
&:empty {
|
|
1083
|
+
display: none;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
880
1088
|
// Underline annotations
|
|
881
1089
|
.annotation-underline {
|
|
882
1090
|
// Container styles - dimensions set by JS
|
|
@@ -2447,6 +2655,31 @@
|
|
|
2447
2655
|
}
|
|
2448
2656
|
}
|
|
2449
2657
|
|
|
2658
|
+
// Detail panel
|
|
2659
|
+
.annotation-detail-panel {
|
|
2660
|
+
border: 1px solid CanvasText;
|
|
2661
|
+
background: Canvas;
|
|
2662
|
+
color: CanvasText;
|
|
2663
|
+
|
|
2664
|
+
.annotation-detail-header {
|
|
2665
|
+
border-bottom: 1px solid CanvasText;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
.toolbar-btn,
|
|
2669
|
+
.annotation-detail-close,
|
|
2670
|
+
.color-picker-btn {
|
|
2671
|
+
border: 1px solid ButtonText;
|
|
2672
|
+
|
|
2673
|
+
&:hover {
|
|
2674
|
+
border-color: Highlight;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
.color-dropdown .color-option .color-swatch {
|
|
2679
|
+
forced-color-adjust: none;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2450
2683
|
// Popup and dialogs
|
|
2451
2684
|
.annotation-popup {
|
|
2452
2685
|
border: 1px solid CanvasText;
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stimulus-pdf-viewer-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Baker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|