@embedpdf/viewer-chrome 3.0.0-next.13 → 3.0.0-next.15
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/README.md +175 -132
- package/dist/annotation-fonts-BqoLZ4Mp.js +80 -0
- package/dist/annotation-fonts-BqoLZ4Mp.js.map +1 -0
- package/dist/annotation-fonts-Dt0IQXhz.cjs +91 -0
- package/dist/annotation-fonts-Dt0IQXhz.cjs.map +1 -0
- package/dist/annotation-style--rPIZsuP.js +879 -0
- package/dist/annotation-style--rPIZsuP.js.map +1 -0
- package/dist/annotation-style-Bzvvp8-N.cjs +879 -0
- package/dist/annotation-style-Bzvvp8-N.cjs.map +1 -0
- package/dist/ar-D9XKAmDx.js +567 -0
- package/dist/ar-D9XKAmDx.js.map +1 -0
- package/dist/ar-DnEj5vbP.cjs +567 -0
- package/dist/ar-DnEj5vbP.cjs.map +1 -0
- package/dist/chrome.cjs +378 -221
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.d.cts +5 -34
- package/dist/chrome.d.ts +5 -34
- package/dist/chrome.js +377 -218
- package/dist/chrome.js.map +1 -1
- package/dist/commands-BmgxhJ8B.js +953 -0
- package/dist/commands-BmgxhJ8B.js.map +1 -0
- package/dist/commands-CYt4SFUX.cjs +1060 -0
- package/dist/commands-CYt4SFUX.cjs.map +1 -0
- package/dist/commands.cjs +12 -595
- package/dist/commands.d.cts +29 -4
- package/dist/commands.d.ts +29 -4
- package/dist/commands.js +2 -593
- package/dist/comments-panel-MUVgLCBI.js +514 -0
- package/dist/comments-panel-MUVgLCBI.js.map +1 -0
- package/dist/comments-panel-pEcQg4Vs.cjs +514 -0
- package/dist/comments-panel-pEcQg4Vs.cjs.map +1 -0
- package/dist/de-Bdr4aWzm.js +566 -0
- package/dist/de-Bdr4aWzm.js.map +1 -0
- package/dist/de-Dv6zvbnd.cjs +566 -0
- package/dist/de-Dv6zvbnd.cjs.map +1 -0
- package/dist/dialog-B5GBisKv.js +55 -0
- package/dist/dialog-B5GBisKv.js.map +1 -0
- package/dist/dialog-t8F13BL_.cjs +60 -0
- package/dist/dialog-t8F13BL_.cjs.map +1 -0
- package/dist/es-BP09DR9w.cjs +566 -0
- package/dist/es-BP09DR9w.cjs.map +1 -0
- package/dist/es-BSXz5tBv.js +566 -0
- package/dist/es-BSXz5tBv.js.map +1 -0
- package/dist/form-design-panel-B5MXTZ96.cjs +418 -0
- package/dist/form-design-panel-B5MXTZ96.cjs.map +1 -0
- package/dist/form-design-panel-vWWU9uB-.js +418 -0
- package/dist/form-design-panel-vWWU9uB-.js.map +1 -0
- package/dist/fr-BBQQn7tJ.cjs +566 -0
- package/dist/fr-BBQQn7tJ.cjs.map +1 -0
- package/dist/fr-Ckn3K9v1.js +566 -0
- package/dist/fr-Ckn3K9v1.js.map +1 -0
- package/dist/he-DRsG7bH7.cjs +567 -0
- package/dist/he-DRsG7bH7.cjs.map +1 -0
- package/dist/he-wdGU5PkU.js +567 -0
- package/dist/he-wdGU5PkU.js.map +1 -0
- package/dist/icons-BrK5iVL8.js +856 -0
- package/dist/icons-BrK5iVL8.js.map +1 -0
- package/dist/icons-x0Tc05bs.cjs +1010 -0
- package/dist/icons-x0Tc05bs.cjs.map +1 -0
- package/dist/index.cjs +3162 -5106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +636 -143
- package/dist/index.d.ts +636 -143
- package/dist/index.js +3083 -4975
- package/dist/index.js.map +1 -1
- package/dist/it-BuP4o0xQ.cjs +566 -0
- package/dist/it-BuP4o0xQ.cjs.map +1 -0
- package/dist/it-Us-VmvIu.js +566 -0
- package/dist/it-Us-VmvIu.js.map +1 -0
- package/dist/ja-Cizj0-5O.js +566 -0
- package/dist/ja-Cizj0-5O.js.map +1 -0
- package/dist/ja-DovEd6ts.cjs +566 -0
- package/dist/ja-DovEd6ts.cjs.map +1 -0
- package/dist/ko-B-qJEol2.cjs +566 -0
- package/dist/ko-B-qJEol2.cjs.map +1 -0
- package/dist/ko-DlrXliE1.js +566 -0
- package/dist/ko-DlrXliE1.js.map +1 -0
- package/dist/labels-BiM9wp9K.cjs +21 -0
- package/dist/labels-BiM9wp9K.cjs.map +1 -0
- package/dist/labels-C6_i87Oj.js +16 -0
- package/dist/labels-C6_i87Oj.js.map +1 -0
- package/dist/link-editor-CSjqPnj1.cjs +125 -0
- package/dist/link-editor-CSjqPnj1.cjs.map +1 -0
- package/dist/link-editor-SxM1nnJU.js +125 -0
- package/dist/link-editor-SxM1nnJU.js.map +1 -0
- package/dist/measurement-Bs61XWbJ.js +325 -0
- package/dist/measurement-Bs61XWbJ.js.map +1 -0
- package/dist/measurement-CcCsHPOX.cjs +326 -0
- package/dist/measurement-CcCsHPOX.cjs.map +1 -0
- package/dist/nl-0aF7M_0h.js +566 -0
- package/dist/nl-0aF7M_0h.js.map +1 -0
- package/dist/nl-D3KZDDiJ.cjs +566 -0
- package/dist/nl-D3KZDDiJ.cjs.map +1 -0
- package/dist/pl-BW--AaCI.js +566 -0
- package/dist/pl-BW--AaCI.js.map +1 -0
- package/dist/pl-DSFhAUqx.cjs +566 -0
- package/dist/pl-DSFhAUqx.cjs.map +1 -0
- package/dist/pt-C5S-8MC5.cjs +566 -0
- package/dist/pt-C5S-8MC5.cjs.map +1 -0
- package/dist/pt-DIhpU5pT.js +566 -0
- package/dist/pt-DIhpU5pT.js.map +1 -0
- package/dist/redact-confirm-B6j36TPw.js +83 -0
- package/dist/redact-confirm-B6j36TPw.js.map +1 -0
- package/dist/redact-confirm-CX_SY2ZN.cjs +83 -0
- package/dist/redact-confirm-CX_SY2ZN.cjs.map +1 -0
- package/dist/redaction-panel-BmoIl-Ll.cjs +96 -0
- package/dist/redaction-panel-BmoIl-Ll.cjs.map +1 -0
- package/dist/redaction-panel-IJDshSJI.js +96 -0
- package/dist/redaction-panel-IJDshSJI.js.map +1 -0
- package/dist/search-panel-HfIhJm1M.js +249 -0
- package/dist/search-panel-HfIhJm1M.js.map +1 -0
- package/dist/search-panel-s9IoiOtv.cjs +249 -0
- package/dist/search-panel-s9IoiOtv.cjs.map +1 -0
- package/dist/sheet-BDsa_Sup.cjs +442 -0
- package/dist/sheet-BDsa_Sup.cjs.map +1 -0
- package/dist/sheet-R6XDgyEf.js +365 -0
- package/dist/sheet-R6XDgyEf.js.map +1 -0
- package/dist/sign-dialog-CgnR7akj.cjs +155 -0
- package/dist/sign-dialog-CgnR7akj.cjs.map +1 -0
- package/dist/sign-dialog-GDqCoQhA.js +155 -0
- package/dist/sign-dialog-GDqCoQhA.js.map +1 -0
- package/dist/signature-inspector-DyO313Qf.cjs +166 -0
- package/dist/signature-inspector-DyO313Qf.cjs.map +1 -0
- package/dist/signature-inspector-okFz8do6.js +166 -0
- package/dist/signature-inspector-okFz8do6.js.map +1 -0
- package/dist/signature-maker-BhCmYyHD.js +393 -0
- package/dist/signature-maker-BhCmYyHD.js.map +1 -0
- package/dist/signature-maker-BoYa8vgA.cjs +393 -0
- package/dist/signature-maker-BoYa8vgA.cjs.map +1 -0
- package/dist/signatures-panel-B56PC6d9.cjs +300 -0
- package/dist/signatures-panel-B56PC6d9.cjs.map +1 -0
- package/dist/signatures-panel-CgnFfhFm.js +300 -0
- package/dist/signatures-panel-CgnFfhFm.js.map +1 -0
- package/dist/stamp-store-BHvRs-Do.cjs +147 -0
- package/dist/stamp-store-BHvRs-Do.cjs.map +1 -0
- package/dist/stamp-store-WuF-L3Jj.js +112 -0
- package/dist/stamp-store-WuF-L3Jj.js.map +1 -0
- package/dist/stamps-panel-B9NYGayn.cjs +231 -0
- package/dist/stamps-panel-B9NYGayn.cjs.map +1 -0
- package/dist/stamps-panel-CDO0FPyK.js +231 -0
- package/dist/stamps-panel-CDO0FPyK.js.map +1 -0
- package/dist/toolbar-CJu2wKTU.cjs +1659 -0
- package/dist/toolbar-CJu2wKTU.cjs.map +1 -0
- package/dist/toolbar-z9od3tKl.js +1564 -0
- package/dist/toolbar-z9od3tKl.js.map +1 -0
- package/dist/zh-cn-3tmMeHqv.cjs +566 -0
- package/dist/zh-cn-3tmMeHqv.cjs.map +1 -0
- package/dist/zh-cn-CxkUUBct.js +566 -0
- package/dist/zh-cn-CxkUUBct.js.map +1 -0
- package/package.json +6 -4
- package/src/styles.css +153 -89
- package/dist/commands.cjs.map +0 -1
- package/dist/commands.js.map +0 -1
- package/dist/es-DYwzfxyt.cjs +0 -293
- package/dist/es-DYwzfxyt.cjs.map +0 -1
- package/dist/es-sN6uqBgD.js +0 -293
- package/dist/es-sN6uqBgD.js.map +0 -1
|
@@ -0,0 +1,1659 @@
|
|
|
1
|
+
const require_icons = require("./icons-x0Tc05bs.cjs");
|
|
2
|
+
const require_sheet = require("./sheet-BDsa_Sup.cjs");
|
|
3
|
+
const require_stamp_store = require("./stamp-store-BHvRs-Do.cjs");
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let _embedpdf_react_runtime = require("@embedpdf/react/runtime");
|
|
6
|
+
let _embedpdf_react_stage = require("@embedpdf/react/stage");
|
|
7
|
+
let _embedpdf_react_page_edit = require("@embedpdf/react/page-edit");
|
|
8
|
+
let _embedpdf_react_interaction = require("@embedpdf/react/interaction");
|
|
9
|
+
let _embedpdf_react_annotation = require("@embedpdf/react/annotation");
|
|
10
|
+
let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
|
|
11
|
+
let _embedpdf_react_measurement = require("@embedpdf/react/measurement");
|
|
12
|
+
let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
|
|
13
|
+
let _embedpdf_react_form = require("@embedpdf/react/form");
|
|
14
|
+
let _embedpdf_react_signature = require("@embedpdf/react/signature");
|
|
15
|
+
let _embedpdf_react_i18n = require("@embedpdf/react/i18n");
|
|
16
|
+
let _embedpdf_react_commands = require("@embedpdf/react/commands");
|
|
17
|
+
let _embedpdf_react_shell = require("@embedpdf/react/shell");
|
|
18
|
+
let _embedpdf_core_ui = require("@embedpdf/core-ui");
|
|
19
|
+
let _embedpdf_web = require("@embedpdf/web");
|
|
20
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
21
|
+
let _embedpdf_react_toolbar = require("@embedpdf/react/toolbar");
|
|
22
|
+
//#region src/features.ts
|
|
23
|
+
/** Every built-in feature and its parts: the names `disable` takes, as All features lists them. */
|
|
24
|
+
const FEATURE_PARTS = {
|
|
25
|
+
annotations: [
|
|
26
|
+
"highlight",
|
|
27
|
+
"markup",
|
|
28
|
+
"drawing",
|
|
29
|
+
"text",
|
|
30
|
+
"notes",
|
|
31
|
+
"links"
|
|
32
|
+
],
|
|
33
|
+
shapes: [],
|
|
34
|
+
stamps: [],
|
|
35
|
+
comments: ["writing"],
|
|
36
|
+
forms: ["filling", "building"],
|
|
37
|
+
signatures: ["signing", "checking"],
|
|
38
|
+
redaction: [],
|
|
39
|
+
measurement: [],
|
|
40
|
+
pages: [],
|
|
41
|
+
attachments: ["adding"],
|
|
42
|
+
text: ["selection", "copy"],
|
|
43
|
+
search: [],
|
|
44
|
+
thumbnails: [],
|
|
45
|
+
outline: [],
|
|
46
|
+
documents: [
|
|
47
|
+
"open",
|
|
48
|
+
"download",
|
|
49
|
+
"print",
|
|
50
|
+
"tabs"
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
/** The features that change the document: what `readOnly` turns off. */
|
|
54
|
+
const READ_ONLY_FEATURES = [
|
|
55
|
+
"annotations",
|
|
56
|
+
"shapes",
|
|
57
|
+
"stamps",
|
|
58
|
+
"comments.writing",
|
|
59
|
+
"forms",
|
|
60
|
+
"signatures.signing",
|
|
61
|
+
"redaction",
|
|
62
|
+
"measurement",
|
|
63
|
+
"pages",
|
|
64
|
+
"attachments.adding"
|
|
65
|
+
];
|
|
66
|
+
const isInside = (feature, outer) => feature === outer || feature.startsWith(`${outer}.`);
|
|
67
|
+
/**
|
|
68
|
+
* Whether nothing that changes the document is left, `isOff` saying which features are off (the
|
|
69
|
+
* commands plugin's `isCategoryDisabled`): what the View only label shows for.
|
|
70
|
+
*/
|
|
71
|
+
const leavesNothingToChange = (isOff) => READ_ONLY_FEATURES.every(isOff);
|
|
72
|
+
/**
|
|
73
|
+
* The annotations each feature makes, by the names the annotation plugin's `editable` setting
|
|
74
|
+
* takes: a subtype, or a measurement's intent. Every subtype people can change is here; a popup
|
|
75
|
+
* or an unsupported annotation is never changed by hand. A signature placed off a field is a
|
|
76
|
+
* stamp; one drawn into a field is the field's value.
|
|
77
|
+
*/
|
|
78
|
+
const FEATURE_ANNOTATIONS = {
|
|
79
|
+
"annotations.highlight": ["highlight"],
|
|
80
|
+
"annotations.markup": [
|
|
81
|
+
"underline",
|
|
82
|
+
"strikeout",
|
|
83
|
+
"squiggly"
|
|
84
|
+
],
|
|
85
|
+
"annotations.drawing": ["ink"],
|
|
86
|
+
"annotations.text": ["free-text", "caret"],
|
|
87
|
+
"annotations.notes": ["text"],
|
|
88
|
+
"annotations.links": ["link"],
|
|
89
|
+
shapes: [
|
|
90
|
+
"square",
|
|
91
|
+
"circle",
|
|
92
|
+
"line",
|
|
93
|
+
"polygon",
|
|
94
|
+
"polyline"
|
|
95
|
+
],
|
|
96
|
+
stamps: ["stamp"],
|
|
97
|
+
redaction: ["redact"],
|
|
98
|
+
measurement: [
|
|
99
|
+
"line-dimension",
|
|
100
|
+
"polyline-dimension",
|
|
101
|
+
"polygon-dimension"
|
|
102
|
+
],
|
|
103
|
+
"attachments.adding": ["file-attachment"],
|
|
104
|
+
"forms.building": ["widget"]
|
|
105
|
+
};
|
|
106
|
+
/** The fields each feature fills in, by the families the form plugin's `fillable` setting takes. */
|
|
107
|
+
const FEATURE_FIELDS = {
|
|
108
|
+
"forms.filling": [
|
|
109
|
+
"text",
|
|
110
|
+
"checkbox",
|
|
111
|
+
"radio",
|
|
112
|
+
"combobox",
|
|
113
|
+
"listbox",
|
|
114
|
+
"pushbutton"
|
|
115
|
+
],
|
|
116
|
+
"signatures.signing": ["signature"]
|
|
117
|
+
};
|
|
118
|
+
/** `true` while no feature of `table` is off, `false` once every one is, else what the ones on hold. */
|
|
119
|
+
function stillOn(table, off) {
|
|
120
|
+
const features = Object.keys(table);
|
|
121
|
+
const on = features.filter((feature) => !off.some((name) => isInside(feature, name)));
|
|
122
|
+
if (on.length === features.length) return true;
|
|
123
|
+
if (on.length === 0) return false;
|
|
124
|
+
return on.flatMap((feature) => table[feature]);
|
|
125
|
+
}
|
|
126
|
+
const partsOf = (feature) => FEATURE_PARTS[feature] ?? [];
|
|
127
|
+
/**
|
|
128
|
+
* The features `readOnly` turns off, minus `except`. An exception inside a feature splits it
|
|
129
|
+
* into its parts, so `except: ['forms.filling']` leaves `forms.building` off.
|
|
130
|
+
*/
|
|
131
|
+
function readOnlyFeaturesOf(readOnly) {
|
|
132
|
+
if (!readOnly) return [];
|
|
133
|
+
const except = readOnly === true ? [] : readOnly.except;
|
|
134
|
+
const off = [];
|
|
135
|
+
const subtract = (feature) => {
|
|
136
|
+
if (except.some((kept) => isInside(feature, kept))) return;
|
|
137
|
+
const parts = partsOf(feature);
|
|
138
|
+
if (parts.length === 0 || !except.some((kept) => isInside(kept, feature))) {
|
|
139
|
+
off.push(feature);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const part of parts) subtract(`${feature}.${part}`);
|
|
143
|
+
};
|
|
144
|
+
for (const feature of READ_ONLY_FEATURES) subtract(feature);
|
|
145
|
+
return off;
|
|
146
|
+
}
|
|
147
|
+
/** The features `disable` and `readOnly` turn off: the names without a colon, and `readOnly`'s. */
|
|
148
|
+
function featuresOffOf(disable, readOnly) {
|
|
149
|
+
const features = (disable ?? []).filter((name) => !name.includes(":"));
|
|
150
|
+
return [.../* @__PURE__ */ new Set([...features, ...readOnlyFeaturesOf(readOnly)])];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The commands plugin's settings for a `disable` list and a `readOnly` value: the names with a
|
|
154
|
+
* colon are command ids, the rest features, and `readOnly` adds its own features.
|
|
155
|
+
*/
|
|
156
|
+
function disabledSettingsOf(disable, readOnly) {
|
|
157
|
+
const commands = new Set((disable ?? []).filter((name) => name.includes(":")));
|
|
158
|
+
return {
|
|
159
|
+
disabledCategories: featuresOffOf(disable, readOnly),
|
|
160
|
+
disabledCommands: [...commands]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* What people may still change by hand for a `disable` list and a `readOnly` value: the
|
|
165
|
+
* annotations and fields of the features left on. A single command turned off leaves them.
|
|
166
|
+
*/
|
|
167
|
+
function byHandSettingsOf(disable, readOnly) {
|
|
168
|
+
const off = featuresOffOf(disable, readOnly);
|
|
169
|
+
return {
|
|
170
|
+
editable: stillOn(FEATURE_ANNOTATIONS, off),
|
|
171
|
+
fillable: stillOn(FEATURE_FIELDS, off)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* What two `editable` or `fillable` values both allow: `true` is everything, `false` nothing,
|
|
176
|
+
* and a list its names, in the order of `left`. The viewer's own value and the app's setting
|
|
177
|
+
* meet here, so each can only take away.
|
|
178
|
+
*/
|
|
179
|
+
function bothAllow(left, right) {
|
|
180
|
+
if (left === true) return right;
|
|
181
|
+
if (right === true) return left;
|
|
182
|
+
if (left === false || right === false) return false;
|
|
183
|
+
return left.filter((name) => right.includes(name));
|
|
184
|
+
}
|
|
185
|
+
/** Whether two `editable` or `fillable` values say the same. */
|
|
186
|
+
const sameByHand = (left, right) => typeof left === "boolean" || typeof right === "boolean" ? left === right : left.length === right.length && left.every((name, index) => name === right[index]);
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/ui/bar-edge.tsx
|
|
189
|
+
/**
|
|
190
|
+
* Where a bar is, for what opens from it. The shell wraps each bar it draws in
|
|
191
|
+
* a `BarEdgeProvider` naming the viewer edge the bar sits on; a menu (and the
|
|
192
|
+
* toolbar's own "More") reads it with `useBarEdge()` and opens away from that
|
|
193
|
+
* edge: a bottom bar's menus open upward, a start bar's to the side, a
|
|
194
|
+
* floating bar's toward the middle of the viewer.
|
|
195
|
+
*
|
|
196
|
+
* A floating bar names the half of the viewer it floats in (`float-top`,
|
|
197
|
+
* `float-bottom`), since its menus open toward the other half; a bar over a
|
|
198
|
+
* selection says `selection`, and its menus open on the side the bar sits,
|
|
199
|
+
* above or below what's selected, away from it. Outside any provider the edge
|
|
200
|
+
* is `'top'`, the main bar's.
|
|
201
|
+
*/
|
|
202
|
+
const BarEdgeContext = (0, react.createContext)("top");
|
|
203
|
+
/** Names the edge of the bar drawn inside it. */
|
|
204
|
+
const BarEdgeProvider = BarEdgeContext.Provider;
|
|
205
|
+
/** The edge of the bar this component is drawn in (`'top'` outside any bar). */
|
|
206
|
+
function useBarEdge() {
|
|
207
|
+
return (0, react.useContext)(BarEdgeContext);
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/ui/measurement-scale.tsx
|
|
211
|
+
/**
|
|
212
|
+
* The Measure bar's scale button: the current page's drawing scale, which opens the measurement
|
|
213
|
+
* panel. Apart from the panel and the calibration dialog (measurement.tsx), so the bar draws it
|
|
214
|
+
* without loading them.
|
|
215
|
+
*/
|
|
216
|
+
const button = "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-50";
|
|
217
|
+
function MeasurementScaleButton() {
|
|
218
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
219
|
+
const scale = (0, _embedpdf_react_measurement.usePageScale)((0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_stage.StageToken, (stage) => stage.getCurrentPage()));
|
|
220
|
+
const surface = (0, _embedpdf_react_shell.useSurface)("measurement");
|
|
221
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
222
|
+
part: "toolbar-button",
|
|
223
|
+
className: `${button} inline-flex items-center gap-1.5 whitespace-nowrap`,
|
|
224
|
+
onClick: () => surface.open(),
|
|
225
|
+
title: t("measurement.title"),
|
|
226
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
227
|
+
name: "updateScale",
|
|
228
|
+
size: 20,
|
|
229
|
+
className: "shrink-0"
|
|
230
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
231
|
+
t("measurement.scale"),
|
|
232
|
+
":",
|
|
233
|
+
" ",
|
|
234
|
+
scale?.measure?.subtype === "rectilinear" ? scale.measure.ratio ?? t("measurement.custom") : t("measurement.unavailable")
|
|
235
|
+
] })]
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function PageNumber() {
|
|
239
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
240
|
+
const stage = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_stage.StageToken);
|
|
241
|
+
const current = (0, _embedpdf_react_stage.useStageState)((state) => state.currentPageIndex + 1);
|
|
242
|
+
const total = (0, _embedpdf_react_stage.useStageState)((state) => state.pageCount);
|
|
243
|
+
/** What the field holds while it is open, or `null` while the number shows. */
|
|
244
|
+
const [draft, setDraft] = (0, react.useState)(null);
|
|
245
|
+
const field = (0, react.useRef)(null);
|
|
246
|
+
const button = (0, react.useRef)(null);
|
|
247
|
+
/** Whether the field closed from the keyboard, so focus goes back to the number. */
|
|
248
|
+
const refocus = (0, react.useRef)(false);
|
|
249
|
+
const editing = draft !== null;
|
|
250
|
+
(0, react.useEffect)(() => {
|
|
251
|
+
if (editing) {
|
|
252
|
+
field.current?.focus();
|
|
253
|
+
field.current?.select();
|
|
254
|
+
} else if (refocus.current) {
|
|
255
|
+
refocus.current = false;
|
|
256
|
+
button.current?.focus();
|
|
257
|
+
}
|
|
258
|
+
}, [editing]);
|
|
259
|
+
if (!total) return null;
|
|
260
|
+
/** Closes the field from the keyboard: focus goes back to the number. */
|
|
261
|
+
const close = () => {
|
|
262
|
+
refocus.current = true;
|
|
263
|
+
setDraft(null);
|
|
264
|
+
};
|
|
265
|
+
const go = () => {
|
|
266
|
+
const page = Number.parseInt(draft ?? "", 10);
|
|
267
|
+
if (Number.isFinite(page)) stage?.goToPage(Math.min(Math.max(page, 1), total) - 1);
|
|
268
|
+
close();
|
|
269
|
+
};
|
|
270
|
+
const [before, after = ""] = t("demo.page", { params: {
|
|
271
|
+
current: "",
|
|
272
|
+
total
|
|
273
|
+
} }).split("");
|
|
274
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
275
|
+
className: "text-fg-secondary block whitespace-nowrap px-3.5 text-sm leading-7",
|
|
276
|
+
children: [
|
|
277
|
+
before,
|
|
278
|
+
editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
279
|
+
ref: field,
|
|
280
|
+
type: "text",
|
|
281
|
+
inputMode: "numeric",
|
|
282
|
+
"aria-label": t("demo.goToPage"),
|
|
283
|
+
value: draft,
|
|
284
|
+
onChange: (event) => setDraft(event.target.value.replace(/[^0-9]/g, "")),
|
|
285
|
+
onKeyDown: (event) => {
|
|
286
|
+
if (event.key === "Enter") {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
go();
|
|
289
|
+
} else if (event.key === "Escape") {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
close();
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
onBlur: () => setDraft(null),
|
|
295
|
+
style: { width: `${String(total).length + 1}ch` },
|
|
296
|
+
className: "bg-hover ring-accent text-fg rounded text-center tabular-nums outline-none ring-1"
|
|
297
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
298
|
+
ref: button,
|
|
299
|
+
type: "button",
|
|
300
|
+
title: t("demo.goToPage"),
|
|
301
|
+
onClick: () => setDraft(String(current)),
|
|
302
|
+
className: "hover:bg-hover hover:text-fg -mx-0.5 cursor-pointer rounded px-0.5 tabular-nums",
|
|
303
|
+
children: current
|
|
304
|
+
}),
|
|
305
|
+
after
|
|
306
|
+
]
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/ui/quick-stamps.tsx
|
|
311
|
+
/**
|
|
312
|
+
* Quick marks in the toolbar: a row of stamp thumbnails that arm on click —
|
|
313
|
+
* the checkmark, the cross, whatever the embedder configured
|
|
314
|
+
* (`plugins.stamp.quick`: `library:name`, `standard` for the standard stamps). The hover ghost and
|
|
315
|
+
* the click placement are the annotation plugin's, exactly as from the
|
|
316
|
+
* sidebar; only the picker moved. Renders nothing until the assets exist.
|
|
317
|
+
* Lives in the workspace toolbar, so it reads the active document's tool.
|
|
318
|
+
*/
|
|
319
|
+
function QuickStamps() {
|
|
320
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
321
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
322
|
+
const { standard, quick } = require_icons.useStampSettings();
|
|
323
|
+
const { locale } = (0, _embedpdf_react_i18n.useI18nState)();
|
|
324
|
+
const assets = (0, _embedpdf_react_stamp.useStampAssets)();
|
|
325
|
+
const documentId = (0, _embedpdf_react_runtime.useDocumentId)();
|
|
326
|
+
const activeToolId = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_interaction.InteractionToken, (interaction) => interaction.getActiveToolId(), null);
|
|
327
|
+
const armedId = (0, _embedpdf_react_stamp.useStampState)((state) => state.armedAsset?.id ?? null);
|
|
328
|
+
const ids = (quick ?? []).map(require_stamp_store.quickStampAssetIdOf);
|
|
329
|
+
const wantsDefault = ids.some((id) => id.startsWith(`${require_stamp_store.DEFAULT_LIBRARY_ID}:`));
|
|
330
|
+
(0, react.useEffect)(() => {
|
|
331
|
+
if (!wantsDefault) return;
|
|
332
|
+
require_stamp_store.restoreStampLibrariesOnce(stamp).then(() => require_stamp_store.ensureDefaultLibrary(stamp, require_stamp_store.resolveStampsLocale(locale), standard)).catch((error) => console.error("[embedpdf] quick stamps failed:", error));
|
|
333
|
+
}, [
|
|
334
|
+
stamp,
|
|
335
|
+
locale,
|
|
336
|
+
wantsDefault
|
|
337
|
+
]);
|
|
338
|
+
const shown = ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset);
|
|
339
|
+
if (shown.length === 0 || !documentId) return null;
|
|
340
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
341
|
+
className: "flex items-center gap-1",
|
|
342
|
+
role: "group",
|
|
343
|
+
"aria-label": t("demo.quickStampsLabel"),
|
|
344
|
+
children: shown.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStampButton, {
|
|
345
|
+
asset,
|
|
346
|
+
active: activeToolId === "stamp" && armedId === asset.id,
|
|
347
|
+
onArm: () => void stamp.armAsset(asset.id).catch((error) => console.error("[embedpdf] arm failed", error))
|
|
348
|
+
}, asset.id))
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function QuickStampButton({ asset, active, onArm }) {
|
|
352
|
+
const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
|
|
353
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
354
|
+
type: "button",
|
|
355
|
+
onClick: onArm,
|
|
356
|
+
title: asset.label,
|
|
357
|
+
"aria-label": asset.label,
|
|
358
|
+
"aria-pressed": active || void 0,
|
|
359
|
+
part: buttonPart(active),
|
|
360
|
+
className: buttonClass(active, true),
|
|
361
|
+
children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
362
|
+
src: url,
|
|
363
|
+
alt: "",
|
|
364
|
+
className: "h-5 w-5 object-contain",
|
|
365
|
+
draggable: false
|
|
366
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
367
|
+
className: "text-xs",
|
|
368
|
+
children: asset.label
|
|
369
|
+
})
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/ui/view-only.tsx
|
|
374
|
+
/**
|
|
375
|
+
* The View only label: the `{ custom: 'view-only' }` item, in the main bar by default. It says why
|
|
376
|
+
* the tools are gone, while nothing that changes the document is on (`readOnly`, or `disable`
|
|
377
|
+
* naming every such feature), or while the scope lets this user change nothing in the open
|
|
378
|
+
* document. Otherwise it draws nothing.
|
|
379
|
+
*/
|
|
380
|
+
/** Whether the scope lets this user change an annotation: make one, or change or delete one there. */
|
|
381
|
+
function mayChangeAnnotations(annotation) {
|
|
382
|
+
if (annotation.canCreate()) return true;
|
|
383
|
+
return annotation.list().some(({ ref }) => annotation.canUpdate(ref) || annotation.canDelete(ref));
|
|
384
|
+
}
|
|
385
|
+
/** Whether the scope lets this user change the form: its fields, a value, or a signature. */
|
|
386
|
+
function mayChangeForm(form, signature) {
|
|
387
|
+
if (form.canDesign()) return true;
|
|
388
|
+
return form.list().some((field) => field.family === "signature" ? signature?.canSign(field.ref) ?? false : form.canFill(field.ref));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Whether the scope lets this user change nothing in the open document: no annotation to make,
|
|
392
|
+
* change or delete (comments included), no field to fill in, sign or design, no page to edit and
|
|
393
|
+
* no redaction to apply. Asked of the plugins' `can*` twins; `false` while no document is open.
|
|
394
|
+
*/
|
|
395
|
+
function useNothingAllowed() {
|
|
396
|
+
const open = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_annotation.AnnotationToken) !== null;
|
|
397
|
+
const signature = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_signature.SignatureToken);
|
|
398
|
+
const annotates = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_annotation.AnnotationToken, mayChangeAnnotations, false);
|
|
399
|
+
const fills = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_form.FormToken, (form) => mayChangeForm(form, signature), false);
|
|
400
|
+
const editsPages = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_page_edit.PageEditToken, (pageEdit) => pageEdit.canEdit(), false);
|
|
401
|
+
const redacts = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_redaction.RedactionToken, (redaction) => redaction.canApply(), false);
|
|
402
|
+
return open && !annotates && !fills && !editsPages && !redacts;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The label; `compact` draws its icon alone (a narrow bar, or a vertical one), its words still
|
|
406
|
+
* there for a screen reader. Plain text, not a live region: it says what the viewer is, and the
|
|
407
|
+
* refusal notice stays the viewer's one status message. Its tooltip says why.
|
|
408
|
+
*/
|
|
409
|
+
function ViewOnlyLabel({ compact = false }) {
|
|
410
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
411
|
+
const commands = (0, _embedpdf_react_commands.useCommands)();
|
|
412
|
+
(0, _embedpdf_react_commands.useCommandsSettings)((settings) => settings.disabledCategories);
|
|
413
|
+
const turnedOff = leavesNothingToChange((feature) => commands.isCategoryDisabled(feature));
|
|
414
|
+
const nothingAllowed = useNothingAllowed();
|
|
415
|
+
if (!turnedOff && !nothingAllowed) return null;
|
|
416
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
417
|
+
title: t(turnedOff ? "viewOnly.turnedOff" : "viewOnly.notAllowed"),
|
|
418
|
+
className: "bg-hover text-fg-secondary flex h-7 shrink-0 select-none items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-xs font-medium",
|
|
419
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
420
|
+
name: "eye",
|
|
421
|
+
size: 16,
|
|
422
|
+
className: "shrink-0"
|
|
423
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
424
|
+
className: compact ? "sr-only" : void 0,
|
|
425
|
+
children: t("viewOnly.label")
|
|
426
|
+
})]
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/ui/item-context.ts
|
|
431
|
+
/** The attribute each field of a {@link ToolbarItemContext} is written to. */
|
|
432
|
+
const TOOLBAR_ITEM_CONTEXT_ATTRIBUTES = {
|
|
433
|
+
orientation: "data-orientation",
|
|
434
|
+
edge: "data-edge",
|
|
435
|
+
variant: "data-variant",
|
|
436
|
+
size: "data-size"
|
|
437
|
+
};
|
|
438
|
+
/** The public place of a bar on `edge`: a floating bar is `float`, whichever half it floats in. */
|
|
439
|
+
const toolbarItemEdgeOf = (edge) => edge === "float-top" || edge === "float-bottom" ? "float" : edge;
|
|
440
|
+
/** Write `context` on `element`, changing only the attributes that differ. */
|
|
441
|
+
function writeToolbarItemContext(element, context) {
|
|
442
|
+
for (const [field, attribute] of Object.entries(TOOLBAR_ITEM_CONTEXT_ATTRIBUTES)) {
|
|
443
|
+
const value = context[field];
|
|
444
|
+
if (value === null) element.removeAttribute(attribute);
|
|
445
|
+
else if (element.getAttribute(attribute) !== value) element.setAttribute(attribute, value);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/ui/slotted-edges.ts
|
|
450
|
+
/**
|
|
451
|
+
* The edge of the bar each slotted toolbar item is in, by the element slotted
|
|
452
|
+
* there, for the viewer's own components inside it: they're drawn apart from
|
|
453
|
+
* the bar (`placed-components.tsx`), and a menu among them opens away from
|
|
454
|
+
* the bar's edge like the bar's own. The item sockets write it
|
|
455
|
+
* (`item-socket.tsx`); outside any item, a component opens its menus like the
|
|
456
|
+
* main bar's.
|
|
457
|
+
*/
|
|
458
|
+
/** The element `node` is a child of: its parent, or the host of the shadow root it is in. */
|
|
459
|
+
const parentOf = (node) => node.parentElement ?? node.getRootNode().host ?? null;
|
|
460
|
+
function createSlottedEdges() {
|
|
461
|
+
const edges = /* @__PURE__ */ new WeakMap();
|
|
462
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
463
|
+
let version = 0;
|
|
464
|
+
return {
|
|
465
|
+
set(element, edge) {
|
|
466
|
+
if (edges.get(element) === edge) return;
|
|
467
|
+
edges.set(element, edge);
|
|
468
|
+
version += 1;
|
|
469
|
+
for (const listener of [...listeners]) listener();
|
|
470
|
+
},
|
|
471
|
+
edgeOf(element) {
|
|
472
|
+
for (let node = element; node; node = parentOf(node)) {
|
|
473
|
+
const edge = edges.get(node);
|
|
474
|
+
if (edge) return edge;
|
|
475
|
+
}
|
|
476
|
+
return "top";
|
|
477
|
+
},
|
|
478
|
+
subscribe(listener) {
|
|
479
|
+
listeners.add(listener);
|
|
480
|
+
return () => {
|
|
481
|
+
listeners.delete(listener);
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
version: () => version
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const SlottedEdgesContext = (0, react.createContext)(createSlottedEdges());
|
|
488
|
+
/** Gives the item sockets and the viewer's own components inside it one {@link SlottedEdges}. */
|
|
489
|
+
const SlottedEdgesProvider = SlottedEdgesContext.Provider;
|
|
490
|
+
function useSlottedEdges() {
|
|
491
|
+
return (0, react.useContext)(SlottedEdgesContext);
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/ui/item-socket.tsx
|
|
495
|
+
/**
|
|
496
|
+
* Your own toolbar item, in its bar: a `<slot name="item:<id>">` the browser
|
|
497
|
+
* shows your element in, with what the item stands for when nothing is
|
|
498
|
+
* slotted (its command's button). What's slotted lives in the page, so the
|
|
499
|
+
* socket is measured where it renders; the toolbar's measurement layer can't
|
|
500
|
+
* hold it. It tells what's slotted where it sits (`item-context.ts`), and the
|
|
501
|
+
* viewer's own components inside it learn the bar's edge from it, so their
|
|
502
|
+
* menus open away from the bar.
|
|
503
|
+
*/
|
|
504
|
+
/** The socket's box: a row of what's slotted, at its own size along the bar. */
|
|
505
|
+
const SOCKET_STYLE = {
|
|
506
|
+
display: "inline-flex",
|
|
507
|
+
alignItems: "center",
|
|
508
|
+
flexShrink: 0
|
|
509
|
+
};
|
|
510
|
+
function ItemSocket({ id, variant, orientation, measure, children }) {
|
|
511
|
+
const slot = (0, react.useRef)(null);
|
|
512
|
+
const edge = useBarEdge();
|
|
513
|
+
const { size } = require_icons.useLayoutEnv();
|
|
514
|
+
const edges = useSlottedEdges();
|
|
515
|
+
const report = (0, react.useRef)(measure);
|
|
516
|
+
report.current = measure;
|
|
517
|
+
const resizes = (0, react.useRef)(null);
|
|
518
|
+
const hasVariants = variant !== null;
|
|
519
|
+
(0, react.useLayoutEffect)(() => {
|
|
520
|
+
const element = slot.current;
|
|
521
|
+
if (!element) return;
|
|
522
|
+
const side = orientation === "vertical" ? "height" : "width";
|
|
523
|
+
const measureNow = () => report.current(element.getBoundingClientRect()[side]);
|
|
524
|
+
if (!hasVariants) measureNow();
|
|
525
|
+
const observer = new ResizeObserver(measureNow);
|
|
526
|
+
observer.observe(element);
|
|
527
|
+
resizes.current = observer;
|
|
528
|
+
return () => {
|
|
529
|
+
observer.disconnect();
|
|
530
|
+
resizes.current = null;
|
|
531
|
+
};
|
|
532
|
+
}, [orientation, hasVariants]);
|
|
533
|
+
(0, react.useLayoutEffect)(() => {
|
|
534
|
+
const element = slot.current;
|
|
535
|
+
const observer = resizes.current;
|
|
536
|
+
if (!element || !observer) return;
|
|
537
|
+
observer.unobserve(element);
|
|
538
|
+
observer.observe(element);
|
|
539
|
+
}, [variant]);
|
|
540
|
+
(0, react.useLayoutEffect)(() => {
|
|
541
|
+
const element = slot.current;
|
|
542
|
+
if (!element) return;
|
|
543
|
+
const context = {
|
|
544
|
+
orientation,
|
|
545
|
+
edge: toolbarItemEdgeOf(edge),
|
|
546
|
+
variant,
|
|
547
|
+
size
|
|
548
|
+
};
|
|
549
|
+
const tell = () => {
|
|
550
|
+
for (const slotted of element.assignedElements()) {
|
|
551
|
+
writeToolbarItemContext(slotted, context);
|
|
552
|
+
edges.set(slotted, edge);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
tell();
|
|
556
|
+
element.addEventListener("slotchange", tell);
|
|
557
|
+
return () => element.removeEventListener("slotchange", tell);
|
|
558
|
+
}, [
|
|
559
|
+
orientation,
|
|
560
|
+
edge,
|
|
561
|
+
variant,
|
|
562
|
+
size,
|
|
563
|
+
edges
|
|
564
|
+
]);
|
|
565
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", {
|
|
566
|
+
ref: slot,
|
|
567
|
+
name: `item:${id}`,
|
|
568
|
+
style: SOCKET_STYLE,
|
|
569
|
+
children
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/ui/menu-placement.ts
|
|
574
|
+
/**
|
|
575
|
+
* The placement of a menu opened from a button in a bar on `edge`: below a
|
|
576
|
+
* top bar's button, above a bottom bar's, beside a start or end bar's (on the
|
|
577
|
+
* side of the pages), toward the middle from a floating bar, and from a bar
|
|
578
|
+
* over a selection on the side the bar sits (`selectionBar`), away from
|
|
579
|
+
* what's selected.
|
|
580
|
+
*/
|
|
581
|
+
function menuPlacementOf(edge, direction, align = "start", selectionBar = "above") {
|
|
582
|
+
if (edge === "start" || edge === "end") return `${edge === "start" === (direction === "ltr") ? "right" : "left"}-${align}`;
|
|
583
|
+
return `${edge === "bottom" || edge === "float-bottom" || edge === "selection" && selectionBar === "above" ? "top" : "bottom"}-${align === "start" === (direction === "ltr") ? "start" : "end"}`;
|
|
584
|
+
}
|
|
585
|
+
/** The placement of a submenu: beside its row, on the side the text runs toward, tops lined up. */
|
|
586
|
+
function submenuPlacementOf(direction) {
|
|
587
|
+
return direction === "ltr" ? "right-start" : "left-start";
|
|
588
|
+
}
|
|
589
|
+
/** The key that opens a submenu from its row, and the one that closes it again. */
|
|
590
|
+
function submenuKeysOf(direction) {
|
|
591
|
+
return direction === "ltr" ? {
|
|
592
|
+
open: "ArrowRight",
|
|
593
|
+
close: "ArrowLeft"
|
|
594
|
+
} : {
|
|
595
|
+
open: "ArrowLeft",
|
|
596
|
+
close: "ArrowRight"
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/ui/menu.tsx
|
|
601
|
+
/**
|
|
602
|
+
* The chrome's menus: a layout menu (sections of command ids) and the menus
|
|
603
|
+
* the toolbar derives (the "More" menu, a folded group's), drawn as rows in a
|
|
604
|
+
* popover that opens away from its bar's edge (`menu-placement.ts`). The
|
|
605
|
+
* popover is in the browser's top layer, so no bar, panel or the viewer's own
|
|
606
|
+
* box clips it, and it takes the menu keys from `@embedpdf/web`. Rows resolve
|
|
607
|
+
* live through the command registry: label, icon, shortcut, whether it's the
|
|
608
|
+
* current choice, enabled state; a row whose command isn't visible is gone, and a section
|
|
609
|
+
* with none left goes with its line. A row whose command opens a menu opens
|
|
610
|
+
* it beside it. On a phone (a compact viewer) every menu is a sheet from the
|
|
611
|
+
* bottom of the viewer instead, and a submenu slides in over the menu it
|
|
612
|
+
* opened from, with a row that goes back.
|
|
613
|
+
*/
|
|
614
|
+
/** Px between a menu and its button, and between a menu and the window's edges. */
|
|
615
|
+
const MENU_GAP_PX = 4;
|
|
616
|
+
/** The `popover` attribute, which React's types don't list yet. */
|
|
617
|
+
const POPOVER = { popover: "manual" };
|
|
618
|
+
/** The rows of a menu, the elements the menu keys move between. */
|
|
619
|
+
const MENU_ITEM_SELECTOR = "[role=\"menuitem\"], [role=\"menuitemradio\"], [role=\"menuitemcheckbox\"]";
|
|
620
|
+
/** A copy no one can use: the toolbar's hidden measurement layer draws every button twice. */
|
|
621
|
+
const HIDDEN_COPY_SELECTOR = "[inert], [aria-hidden=\"true\"]";
|
|
622
|
+
/** Inside a menu, a menu is a submenu: it opens beside its row. */
|
|
623
|
+
const InMenuContext = (0, react.createContext)(false);
|
|
624
|
+
/** The rows of `menu` itself, not those of a submenu open inside it. */
|
|
625
|
+
const itemsOf = (menu) => [...menu.querySelectorAll(MENU_ITEM_SELECTOR)].filter((item) => item.closest("[role=\"menu\"]") === menu);
|
|
626
|
+
/** The button that opens the menu: the one beside it that says it has a popup. */
|
|
627
|
+
const triggerOf = (holder, menu) => [...holder.querySelectorAll("[aria-haspopup]")].find((candidate) => !menu.contains(candidate)) ?? null;
|
|
628
|
+
/**
|
|
629
|
+
* Where the bar over a selection that `element` is in sits now: its anchored wrapper says so
|
|
630
|
+
* in `data-placement` (`Anchored` in `@embedpdf/react`), `bottom…` once it went below.
|
|
631
|
+
*/
|
|
632
|
+
const selectionBarSideOf = (element) => element.closest("[data-placement]")?.getAttribute("data-placement")?.startsWith("bottom") ? "below" : "above";
|
|
633
|
+
const boxOf = (element) => {
|
|
634
|
+
const rect = element.getBoundingClientRect();
|
|
635
|
+
return {
|
|
636
|
+
x: rect.left,
|
|
637
|
+
y: rect.top,
|
|
638
|
+
width: rect.width,
|
|
639
|
+
height: rect.height
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* What a submenu opens beside: its row, across the whole width of the menu
|
|
644
|
+
* the row is in, and raised by the submenu's own padding so its first row
|
|
645
|
+
* lines up with the row that opened it.
|
|
646
|
+
*/
|
|
647
|
+
function submenuBoxOf(holder, submenu) {
|
|
648
|
+
const row = boxOf(holder);
|
|
649
|
+
const parent = holder.closest("[role=\"menu\"]");
|
|
650
|
+
const across = parent ? boxOf(parent) : row;
|
|
651
|
+
const inset = submenu.clientTop + parseFloat(getComputedStyle(submenu).paddingTop || "0");
|
|
652
|
+
return {
|
|
653
|
+
x: across.x,
|
|
654
|
+
y: row.y - inset,
|
|
655
|
+
width: across.width,
|
|
656
|
+
height: row.height
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Place `menu` around `box` in the window: on the side `placement` asks for,
|
|
661
|
+
* or the other side when that has no room, moved to stay on screen, and never
|
|
662
|
+
* taller than the window.
|
|
663
|
+
*/
|
|
664
|
+
function placeMenu(menu, box, placement, gap) {
|
|
665
|
+
const view = (0, _embedpdf_web.anchoredViewOf)(menu, "client");
|
|
666
|
+
if (!view) return;
|
|
667
|
+
menu.style.maxHeight = `${Math.max(0, view.height - 2 * MENU_GAP_PX)}px`;
|
|
668
|
+
const { left, top } = (0, _embedpdf_web.fitAnchoredRect)(box, placement, gap, {
|
|
669
|
+
size: {
|
|
670
|
+
width: menu.offsetWidth,
|
|
671
|
+
height: menu.offsetHeight
|
|
672
|
+
},
|
|
673
|
+
view
|
|
674
|
+
});
|
|
675
|
+
menu.style.left = `${left}px`;
|
|
676
|
+
menu.style.top = `${top}px`;
|
|
677
|
+
const landed = menu.getBoundingClientRect();
|
|
678
|
+
if (Math.abs(landed.left - left) > .5 || Math.abs(landed.top - top) > .5) {
|
|
679
|
+
menu.style.left = `${2 * left - landed.left}px`;
|
|
680
|
+
menu.style.top = `${2 * top - landed.top}px`;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Open `menu` from the button beside it in `holder`: show it in the top layer,
|
|
685
|
+
* place it and keep it placed, close it on a press outside it and its button,
|
|
686
|
+
* and drive it from the keyboard (a submenu's row also opens it with the arrow
|
|
687
|
+
* toward it, and the arrow back closes it). Returns the close.
|
|
688
|
+
*/
|
|
689
|
+
function openMenu(menu, holder, options) {
|
|
690
|
+
const { nested, close } = options;
|
|
691
|
+
const trigger = triggerOf(holder, menu);
|
|
692
|
+
const direction = (0, _embedpdf_web.directionOf)(holder);
|
|
693
|
+
const placement = options.placement(direction);
|
|
694
|
+
const name = trigger?.getAttribute("aria-label") || trigger?.getAttribute("title") || trigger?.textContent?.trim();
|
|
695
|
+
if (name) menu.setAttribute("aria-label", name);
|
|
696
|
+
if (typeof menu.showPopover === "function") menu.showPopover();
|
|
697
|
+
const place = () => placeMenu(menu, nested ? submenuBoxOf(holder, menu) : boxOf(holder), placement, nested ? 0 : MENU_GAP_PX);
|
|
698
|
+
place();
|
|
699
|
+
const resizes = new ResizeObserver(place);
|
|
700
|
+
resizes.observe(menu);
|
|
701
|
+
resizes.observe(holder);
|
|
702
|
+
const stopGeometry = (0, _embedpdf_web.observeClientGeometry)(place);
|
|
703
|
+
const stopOutside = (0, _embedpdf_web.observeOutsidePress)(menu, (event) => {
|
|
704
|
+
if (!trigger || !event.composedPath().includes(trigger)) close();
|
|
705
|
+
});
|
|
706
|
+
const detachKeys = (0, _embedpdf_web.attachMenuKeys)(menu, {
|
|
707
|
+
trigger,
|
|
708
|
+
close,
|
|
709
|
+
items: () => itemsOf(menu)
|
|
710
|
+
});
|
|
711
|
+
const keys = submenuKeysOf(direction);
|
|
712
|
+
const onKeyDown = (event) => {
|
|
713
|
+
const item = event.target;
|
|
714
|
+
const free = !event.defaultPrevented;
|
|
715
|
+
const opensMenu = item.getAttribute("aria-haspopup") === "menu";
|
|
716
|
+
if (free && event.key === keys.open && opensMenu && itemsOf(menu).includes(item)) {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
if (item.getAttribute("aria-expanded") !== "true") item.click();
|
|
719
|
+
else {
|
|
720
|
+
const submenu = item.parentElement?.querySelector("[role=\"menu\"]");
|
|
721
|
+
if (submenu) itemsOf(submenu)[0]?.focus();
|
|
722
|
+
}
|
|
723
|
+
} else if (free && event.key === keys.close && nested) {
|
|
724
|
+
event.preventDefault();
|
|
725
|
+
close();
|
|
726
|
+
trigger?.focus();
|
|
727
|
+
}
|
|
728
|
+
if (nested && event.key !== "Tab") event.stopPropagation();
|
|
729
|
+
};
|
|
730
|
+
menu.addEventListener("keydown", onKeyDown);
|
|
731
|
+
return () => {
|
|
732
|
+
menu.removeEventListener("keydown", onKeyDown);
|
|
733
|
+
detachKeys();
|
|
734
|
+
stopOutside();
|
|
735
|
+
stopGeometry();
|
|
736
|
+
resizes.disconnect();
|
|
737
|
+
if (typeof menu.hidePopover === "function" && menu.matches(":popover-open")) menu.hidePopover();
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* A menu's surface, opened by the button beside it in the same parent (the
|
|
742
|
+
* one with `aria-haspopup`). It opens away from its bar's edge, or beside its
|
|
743
|
+
* row in another menu, and closes on Escape, Tab or a press anywhere outside
|
|
744
|
+
* it and its button. In a compact viewer it opens as a sheet.
|
|
745
|
+
*/
|
|
746
|
+
function MenuPopover(props) {
|
|
747
|
+
const probe = (0, react.useRef)(null);
|
|
748
|
+
const [usable, setUsable] = (0, react.useState)(false);
|
|
749
|
+
const compact = require_icons.useLayoutEnv().size === "compact";
|
|
750
|
+
(0, react.useLayoutEffect)(() => {
|
|
751
|
+
const holder = probe.current?.parentElement;
|
|
752
|
+
if (holder && !holder.closest(HIDDEN_COPY_SELECTOR)) setUsable(true);
|
|
753
|
+
}, []);
|
|
754
|
+
if (!usable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
755
|
+
ref: probe,
|
|
756
|
+
hidden: true
|
|
757
|
+
});
|
|
758
|
+
return compact ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSheet, { ...props }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenMenu, { ...props });
|
|
759
|
+
}
|
|
760
|
+
function OpenMenu({ children, onClose, align = "start" }) {
|
|
761
|
+
const surface = (0, react.useRef)(null);
|
|
762
|
+
const edge = useBarEdge();
|
|
763
|
+
const nested = (0, react.useContext)(InMenuContext);
|
|
764
|
+
const close = (0, react.useRef)(onClose);
|
|
765
|
+
close.current = onClose;
|
|
766
|
+
(0, react.useLayoutEffect)(() => {
|
|
767
|
+
const menu = surface.current;
|
|
768
|
+
const holder = menu?.parentElement;
|
|
769
|
+
if (!menu || !holder) return;
|
|
770
|
+
return openMenu(menu, holder, {
|
|
771
|
+
placement: (direction) => nested ? submenuPlacementOf(direction) : menuPlacementOf(edge, direction, align, edge === "selection" ? selectionBarSideOf(holder) : void 0),
|
|
772
|
+
nested,
|
|
773
|
+
close: () => close.current()
|
|
774
|
+
});
|
|
775
|
+
}, []);
|
|
776
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
777
|
+
ref: surface,
|
|
778
|
+
...POPOVER,
|
|
779
|
+
role: "menu",
|
|
780
|
+
part: "menu",
|
|
781
|
+
className: "border-border-subtle bg-elevated text-fg z-50 overflow-y-auto rounded-lg border shadow-xl",
|
|
782
|
+
style: {
|
|
783
|
+
position: "fixed",
|
|
784
|
+
right: "auto",
|
|
785
|
+
bottom: "auto",
|
|
786
|
+
margin: 0
|
|
787
|
+
},
|
|
788
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InMenuContext.Provider, {
|
|
789
|
+
value: true,
|
|
790
|
+
children
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
/** In a menu's sheet: how a row opens its submenu, over the menu. Null outside a sheet. */
|
|
795
|
+
const MenuSheetContext = (0, react.createContext)(null);
|
|
796
|
+
/** The button a menu's sheet opened from: the one beside the sheet that says it has a popup. */
|
|
797
|
+
function sheetTriggerOf(menu) {
|
|
798
|
+
const layer = menu.closest("[data-epdf-layer]");
|
|
799
|
+
const holder = layer?.parentElement;
|
|
800
|
+
return layer && holder ? triggerOf(holder, layer) : null;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* A menu as a sheet: its rows in a sheet from the bottom of the viewer, which closes on a tap
|
|
804
|
+
* above it, Escape or a pick. A row that opens a menu slides that menu in over this one, with a
|
|
805
|
+
* row (or the arrow key back) that goes back to where it was.
|
|
806
|
+
*/
|
|
807
|
+
function MenuSheet({ children, onClose }) {
|
|
808
|
+
const [submenus, setSubmenus] = (0, react.useState)([]);
|
|
809
|
+
const current = (0, react.useRef)(submenus);
|
|
810
|
+
current.current = submenus;
|
|
811
|
+
/** The row to give focus back to once the submenu over it has gone. */
|
|
812
|
+
const refocus = (0, react.useRef)(null);
|
|
813
|
+
const close = (0, react.useRef)(onClose);
|
|
814
|
+
close.current = onClose;
|
|
815
|
+
const open = (submenu) => setSubmenus([...current.current, submenu]);
|
|
816
|
+
const back = () => {
|
|
817
|
+
const shown = current.current;
|
|
818
|
+
if (shown.length === 0) return;
|
|
819
|
+
refocus.current = shown[shown.length - 1].opener;
|
|
820
|
+
setSubmenus(shown.slice(0, -1));
|
|
821
|
+
};
|
|
822
|
+
(0, react.useLayoutEffect)(() => {
|
|
823
|
+
refocus.current?.focus();
|
|
824
|
+
refocus.current = null;
|
|
825
|
+
}, [submenus]);
|
|
826
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_sheet.Sheet, {
|
|
827
|
+
label: "",
|
|
828
|
+
onClose,
|
|
829
|
+
trapFocus: false,
|
|
830
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSheetContext.Provider, {
|
|
831
|
+
value: open,
|
|
832
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
833
|
+
className: "grid",
|
|
834
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SheetMenu, {
|
|
835
|
+
covered: submenus.length > 0,
|
|
836
|
+
close: () => close.current(),
|
|
837
|
+
children
|
|
838
|
+
}), submenus.map((submenu, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SheetMenu, {
|
|
839
|
+
covered: i < submenus.length - 1,
|
|
840
|
+
close: back,
|
|
841
|
+
opener: submenu.opener,
|
|
842
|
+
label: submenu.label,
|
|
843
|
+
onBack: back,
|
|
844
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuBody, {
|
|
845
|
+
menuId: submenu.menuId,
|
|
846
|
+
onRun: submenu.onRun
|
|
847
|
+
})
|
|
848
|
+
}, i))]
|
|
849
|
+
})
|
|
850
|
+
})
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* One menu in a menu's sheet: the first one, or a submenu over it with a row that goes back.
|
|
855
|
+
* It takes the menu keys while it's on top; covered, it can't take focus or presses.
|
|
856
|
+
*/
|
|
857
|
+
function SheetMenu({ covered, close, opener, label, onBack, children }) {
|
|
858
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
859
|
+
const menu = (0, react.useRef)(null);
|
|
860
|
+
const actions = (0, react.useRef)({
|
|
861
|
+
close,
|
|
862
|
+
onBack
|
|
863
|
+
});
|
|
864
|
+
actions.current = {
|
|
865
|
+
close,
|
|
866
|
+
onBack
|
|
867
|
+
};
|
|
868
|
+
(0, react.useLayoutEffect)(() => {
|
|
869
|
+
const element = menu.current;
|
|
870
|
+
if (!element) return;
|
|
871
|
+
const trigger = opener ?? sheetTriggerOf(element);
|
|
872
|
+
const name = label ?? (trigger?.getAttribute("aria-label") || trigger?.getAttribute("title"));
|
|
873
|
+
if (name) element.setAttribute("aria-label", name);
|
|
874
|
+
const detachKeys = (0, _embedpdf_web.attachMenuKeys)(element, {
|
|
875
|
+
trigger,
|
|
876
|
+
close: () => actions.current.close(),
|
|
877
|
+
items: () => itemsOf(element),
|
|
878
|
+
initialFocus: "none"
|
|
879
|
+
});
|
|
880
|
+
queueMicrotask(() => {
|
|
881
|
+
if (element.isConnected && !element.inert) itemsOf(element).find((item) => !(0, _embedpdf_web.isUnavailable)(item))?.focus();
|
|
882
|
+
});
|
|
883
|
+
const keys = submenuKeysOf((0, _embedpdf_web.directionOf)(element));
|
|
884
|
+
const onKeyDown = (event) => {
|
|
885
|
+
if (event.defaultPrevented) return;
|
|
886
|
+
const item = event.target;
|
|
887
|
+
if (event.key === keys.open && item.getAttribute("aria-haspopup") === "menu") {
|
|
888
|
+
event.preventDefault();
|
|
889
|
+
item.click();
|
|
890
|
+
} else if (event.key === keys.close && actions.current.onBack) {
|
|
891
|
+
event.preventDefault();
|
|
892
|
+
actions.current.onBack();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
element.addEventListener("keydown", onKeyDown);
|
|
896
|
+
return () => {
|
|
897
|
+
element.removeEventListener("keydown", onKeyDown);
|
|
898
|
+
detachKeys();
|
|
899
|
+
};
|
|
900
|
+
}, []);
|
|
901
|
+
(0, react.useLayoutEffect)(() => {
|
|
902
|
+
if (menu.current) menu.current.inert = covered;
|
|
903
|
+
}, [covered]);
|
|
904
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
905
|
+
ref: menu,
|
|
906
|
+
role: "menu",
|
|
907
|
+
part: "menu",
|
|
908
|
+
className: `bg-elevated col-start-1 row-start-1 min-w-0 ${onBack ? "starting:translate-x-full rtl:starting:-translate-x-full transition-[translate] duration-200 ease-out motion-reduce:transition-none" : ""}`,
|
|
909
|
+
children: [onBack && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
910
|
+
className: "p-1 pb-0",
|
|
911
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
912
|
+
type: "button",
|
|
913
|
+
tabIndex: -1,
|
|
914
|
+
role: "menuitem",
|
|
915
|
+
part: "menu-item",
|
|
916
|
+
"data-epdf-menu-back": "",
|
|
917
|
+
onClick: onBack,
|
|
918
|
+
className: ROW_CLASS,
|
|
919
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
920
|
+
name: "chevronLeft",
|
|
921
|
+
size: 16,
|
|
922
|
+
className: "text-fg-muted rtl:-scale-x-100"
|
|
923
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
924
|
+
className: "flex-1 truncate",
|
|
925
|
+
children: t("menu.back")
|
|
926
|
+
})]
|
|
927
|
+
}), label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
928
|
+
className: "text-fg px-2.5 pb-1 pt-2 text-sm font-semibold",
|
|
929
|
+
children: label
|
|
930
|
+
})]
|
|
931
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InMenuContext.Provider, {
|
|
932
|
+
value: true,
|
|
933
|
+
children
|
|
934
|
+
})]
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Which button each open menu opened from, per viewer (its shell). The shell
|
|
939
|
+
* keeps a menu open by its id, and several buttons can name one (the zoom menu
|
|
940
|
+
* in the main bar and in a floating bar): it opens at the button pressed, or
|
|
941
|
+
* at the first one drawn when code opened it.
|
|
942
|
+
*/
|
|
943
|
+
const openers = /* @__PURE__ */ new WeakMap();
|
|
944
|
+
function openersOf(shell) {
|
|
945
|
+
let byMenu = openers.get(shell);
|
|
946
|
+
if (!byMenu) {
|
|
947
|
+
byMenu = /* @__PURE__ */ new Map();
|
|
948
|
+
openers.set(shell, byMenu);
|
|
949
|
+
}
|
|
950
|
+
return byMenu;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* A button that opens the layout menu `menuId`, and the menu when it's open
|
|
954
|
+
* there. `children` draws the button, told whether its menu is open at it; the
|
|
955
|
+
* menu lines up with this wrapper (`className`), so it can hold more than the
|
|
956
|
+
* button, like the zoom field around its chevron.
|
|
957
|
+
*/
|
|
958
|
+
function MenuAnchor({ menuId, className = "relative inline-flex", children }) {
|
|
959
|
+
const shell = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_shell.ShellToken);
|
|
960
|
+
const open = (0, _embedpdf_react_shell.useShellState)((state) => state.openMenus.includes(menuId));
|
|
961
|
+
const anchor = (0, react.useRef)(null);
|
|
962
|
+
const [, claim] = (0, react.useReducer)((count) => count + 1, 0);
|
|
963
|
+
const opener = shell ? openersOf(shell).get(menuId) : void 0;
|
|
964
|
+
const openHere = open && opener !== void 0 && opener === anchor.current;
|
|
965
|
+
(0, react.useLayoutEffect)(() => {
|
|
966
|
+
const element = anchor.current;
|
|
967
|
+
if (!shell || !element) return;
|
|
968
|
+
const byMenu = openersOf(shell);
|
|
969
|
+
const current = byMenu.get(menuId);
|
|
970
|
+
if (!open) {
|
|
971
|
+
if (current === element) byMenu.delete(menuId);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (!current?.isConnected && !element.closest(HIDDEN_COPY_SELECTOR)) {
|
|
975
|
+
byMenu.set(menuId, element);
|
|
976
|
+
claim();
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
980
|
+
ref: anchor,
|
|
981
|
+
className,
|
|
982
|
+
onClickCapture: (event) => {
|
|
983
|
+
const target = event.target;
|
|
984
|
+
const pressedButton = target.closest("[aria-haspopup]") && !target.closest("[role=\"menu\"]");
|
|
985
|
+
if (shell && anchor.current && pressedButton) openersOf(shell).set(menuId, anchor.current);
|
|
986
|
+
},
|
|
987
|
+
children: [children(openHere), openHere && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuPopover, {
|
|
988
|
+
onClose: () => shell?.closeMenu(menuId),
|
|
989
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuBody, {
|
|
990
|
+
menuId,
|
|
991
|
+
onRun: () => shell?.closeMenu(menuId)
|
|
992
|
+
})
|
|
993
|
+
})]
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
const choiceOf = (rows) => {
|
|
997
|
+
const choosable = rows.filter((row) => !row.menu);
|
|
998
|
+
const active = choosable.filter((row) => row.active).length;
|
|
999
|
+
if (active === 0) return "none";
|
|
1000
|
+
return active === 1 && choosable.length > 1 ? "radio" : "checkbox";
|
|
1001
|
+
};
|
|
1002
|
+
const ROLE_OF = {
|
|
1003
|
+
none: "menuitem",
|
|
1004
|
+
radio: "menuitemradio",
|
|
1005
|
+
checkbox: "menuitemcheckbox"
|
|
1006
|
+
};
|
|
1007
|
+
const ROW_BASE = "flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-start text-sm outline-none transition-colors aria-disabled:cursor-not-allowed aria-disabled:opacity-40";
|
|
1008
|
+
/** A row, the pointer over it and the keyboard on it: the neutral hover look. */
|
|
1009
|
+
const ROW_CLASS = `${ROW_BASE} text-fg-secondary hover:bg-hover focus-visible:bg-hover aria-disabled:hover:bg-transparent`;
|
|
1010
|
+
/**
|
|
1011
|
+
* The current choice: the accent look of an active toolbar button, so "on" reads the same
|
|
1012
|
+
* everywhere and never looks like the row the keyboard is on. The keyboard on it adds an inset
|
|
1013
|
+
* ring; with forced colors, where backgrounds go, an outline marks it.
|
|
1014
|
+
*/
|
|
1015
|
+
const CHOSEN_ROW_CLASS = `${ROW_BASE} bg-selected text-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent forced-colors:outline-2 forced-colors:outline-solid forced-colors:-outline-offset-2`;
|
|
1016
|
+
/**
|
|
1017
|
+
* One row: the command's icon, label and shortcut, in the accent look when it
|
|
1018
|
+
* is the current choice. A row that can't run says why in its tooltip, and
|
|
1019
|
+
* stays where it is, greyed out.
|
|
1020
|
+
*/
|
|
1021
|
+
function MenuItem({ command, choice, onSelect }) {
|
|
1022
|
+
const checked = choice !== "none" && command.active;
|
|
1023
|
+
const [shortcut] = command.shortcuts;
|
|
1024
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1025
|
+
type: "button",
|
|
1026
|
+
tabIndex: -1,
|
|
1027
|
+
role: ROLE_OF[choice],
|
|
1028
|
+
"aria-checked": choice === "none" ? void 0 : command.active,
|
|
1029
|
+
"aria-disabled": command.enabled ? void 0 : true,
|
|
1030
|
+
title: command.enabled ? void 0 : command.disabledReason,
|
|
1031
|
+
part: checked ? "menu-item menu-item-active" : "menu-item",
|
|
1032
|
+
onClick: () => {
|
|
1033
|
+
if (command.enabled) onSelect();
|
|
1034
|
+
},
|
|
1035
|
+
className: checked ? CHOSEN_ROW_CLASS : ROW_CLASS,
|
|
1036
|
+
children: [
|
|
1037
|
+
command.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1038
|
+
name: command.icon,
|
|
1039
|
+
size: 16,
|
|
1040
|
+
accent: command.iconAccent,
|
|
1041
|
+
className: checked ? "text-accent" : "text-fg-muted"
|
|
1042
|
+
}),
|
|
1043
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1044
|
+
className: "flex-1 truncate",
|
|
1045
|
+
children: command.label
|
|
1046
|
+
}),
|
|
1047
|
+
shortcut && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1048
|
+
className: "text-fg-muted ms-4 text-xs",
|
|
1049
|
+
children: (0, _embedpdf_react_commands.formatShortcut)(shortcut)
|
|
1050
|
+
})
|
|
1051
|
+
]
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* A row whose command opens a menu: it opens that menu beside it, with a
|
|
1056
|
+
* click or the arrow key toward it. Picking a row in there runs `onRun` too,
|
|
1057
|
+
* so the whole menu closes.
|
|
1058
|
+
*/
|
|
1059
|
+
function SubmenuItem({ command, menuId, onRun }) {
|
|
1060
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1061
|
+
const openInSheet = (0, react.useContext)(MenuSheetContext);
|
|
1062
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1063
|
+
role: "none",
|
|
1064
|
+
className: "relative",
|
|
1065
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1066
|
+
type: "button",
|
|
1067
|
+
tabIndex: -1,
|
|
1068
|
+
role: "menuitem",
|
|
1069
|
+
"aria-haspopup": "menu",
|
|
1070
|
+
"aria-expanded": open,
|
|
1071
|
+
"aria-disabled": command.enabled ? void 0 : true,
|
|
1072
|
+
title: command.enabled ? void 0 : command.disabledReason,
|
|
1073
|
+
part: "menu-item",
|
|
1074
|
+
onClick: (event) => {
|
|
1075
|
+
if (!command.enabled) return;
|
|
1076
|
+
if (openInSheet) openInSheet({
|
|
1077
|
+
menuId,
|
|
1078
|
+
label: command.label,
|
|
1079
|
+
opener: event.currentTarget,
|
|
1080
|
+
onRun
|
|
1081
|
+
});
|
|
1082
|
+
else setOpen((previous) => !previous);
|
|
1083
|
+
},
|
|
1084
|
+
className: `${ROW_CLASS} ${open ? "bg-hover" : ""}`,
|
|
1085
|
+
children: [
|
|
1086
|
+
command.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1087
|
+
name: command.icon,
|
|
1088
|
+
size: 16,
|
|
1089
|
+
accent: command.iconAccent,
|
|
1090
|
+
className: "text-fg-muted"
|
|
1091
|
+
}),
|
|
1092
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1093
|
+
className: "flex-1 truncate",
|
|
1094
|
+
children: command.label
|
|
1095
|
+
}),
|
|
1096
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1097
|
+
name: "chevronRight",
|
|
1098
|
+
size: 16,
|
|
1099
|
+
className: "text-fg-muted rtl:-scale-x-100"
|
|
1100
|
+
})
|
|
1101
|
+
]
|
|
1102
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuPopover, {
|
|
1103
|
+
onClose: () => setOpen(false),
|
|
1104
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuBody, {
|
|
1105
|
+
menuId,
|
|
1106
|
+
onRun
|
|
1107
|
+
})
|
|
1108
|
+
})]
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/** A layout menu's row, by command id. */
|
|
1112
|
+
function MenuRow({ commandId, choice, onRun }) {
|
|
1113
|
+
const command = (0, _embedpdf_react_commands.useCommand)(commandId);
|
|
1114
|
+
if (!command.visible) return null;
|
|
1115
|
+
if (command.menu) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubmenuItem, {
|
|
1116
|
+
command,
|
|
1117
|
+
menuId: command.menu,
|
|
1118
|
+
onRun
|
|
1119
|
+
});
|
|
1120
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
|
|
1121
|
+
command,
|
|
1122
|
+
choice,
|
|
1123
|
+
onSelect: () => {
|
|
1124
|
+
command.run();
|
|
1125
|
+
onRun?.();
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
/** A section: its rows as a group named by its title, after a line when it isn't the first. */
|
|
1130
|
+
function MenuGroup({ title, first, children }) {
|
|
1131
|
+
const titleId = (0, react.useId)();
|
|
1132
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!first && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1133
|
+
role: "separator",
|
|
1134
|
+
className: "bg-border-subtle my-1 h-px"
|
|
1135
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1136
|
+
role: "group",
|
|
1137
|
+
"aria-labelledby": title ? titleId : void 0,
|
|
1138
|
+
children: [title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1139
|
+
id: titleId,
|
|
1140
|
+
className: "text-fg-muted px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide",
|
|
1141
|
+
children: title
|
|
1142
|
+
}), children]
|
|
1143
|
+
})] });
|
|
1144
|
+
}
|
|
1145
|
+
const sameChoices = (left, right) => left.length === right.length && left.every((choice, i) => choice === right[i]);
|
|
1146
|
+
/** One section-separated menu, by its id in the layout. */
|
|
1147
|
+
function MenuBody({ menuId, onRun }) {
|
|
1148
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
1149
|
+
const menu = require_icons.useMenu(menuId);
|
|
1150
|
+
const commands = (0, _embedpdf_react_commands.useCommands)();
|
|
1151
|
+
const documentId = (0, _embedpdf_react_runtime.useDocumentId)() ?? void 0;
|
|
1152
|
+
const choices = (0, _embedpdf_react_runtime.useKernelValue)(() => (menu ?? []).map((section) => {
|
|
1153
|
+
const rows = section.items.map((id) => commands.resolveCommand(id, documentId)).filter((command) => command?.visible === true);
|
|
1154
|
+
return rows.length === 0 ? null : choiceOf(rows);
|
|
1155
|
+
}), sameChoices);
|
|
1156
|
+
if (!menu) return null;
|
|
1157
|
+
const shown = menu.flatMap((section, i) => {
|
|
1158
|
+
const choice = choices[i];
|
|
1159
|
+
return choice ? [{
|
|
1160
|
+
section,
|
|
1161
|
+
choice
|
|
1162
|
+
}] : [];
|
|
1163
|
+
});
|
|
1164
|
+
if (shown.length === 0) return null;
|
|
1165
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1166
|
+
className: "min-w-52 p-1",
|
|
1167
|
+
children: shown.map(({ section, choice }, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuGroup, {
|
|
1168
|
+
first: i === 0,
|
|
1169
|
+
title: section.title ?? (section.titleKey ? t(section.titleKey) : void 0),
|
|
1170
|
+
children: section.items.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuRow, {
|
|
1171
|
+
commandId: id,
|
|
1172
|
+
choice,
|
|
1173
|
+
onRun
|
|
1174
|
+
}, id))
|
|
1175
|
+
}, i))
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* A menu the toolbar derives (the "More" menu, a folded group's): its
|
|
1180
|
+
* sections in bar order, the bar's groups as titled sections, and a row for
|
|
1181
|
+
* each command; a command that opens a menu opens it beside its row.
|
|
1182
|
+
*/
|
|
1183
|
+
function ToolbarMenu({ view, align }) {
|
|
1184
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
1185
|
+
if (!view.isOpen) return null;
|
|
1186
|
+
const sections = view.sections.map((section) => {
|
|
1187
|
+
const rows = section.rows.flatMap((row) => {
|
|
1188
|
+
const command = view.resolve(row.command);
|
|
1189
|
+
return command ? [{
|
|
1190
|
+
row,
|
|
1191
|
+
command
|
|
1192
|
+
}] : [];
|
|
1193
|
+
});
|
|
1194
|
+
return {
|
|
1195
|
+
section,
|
|
1196
|
+
rows,
|
|
1197
|
+
choice: section.role === "radio" ? "radio" : choiceOf(rows.map(({ command }) => command))
|
|
1198
|
+
};
|
|
1199
|
+
});
|
|
1200
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuPopover, {
|
|
1201
|
+
onClose: view.close,
|
|
1202
|
+
align,
|
|
1203
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1204
|
+
className: "min-w-56 p-1",
|
|
1205
|
+
children: sections.map(({ section, rows, choice }, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuGroup, {
|
|
1206
|
+
first: i === 0,
|
|
1207
|
+
title: section.labelKey ? t(section.labelKey) : void 0,
|
|
1208
|
+
children: rows.map(({ row, command }) => row.type === "submenu" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubmenuItem, {
|
|
1209
|
+
command,
|
|
1210
|
+
menuId: row.menu,
|
|
1211
|
+
onRun: view.close
|
|
1212
|
+
}, row.command) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
|
|
1213
|
+
command,
|
|
1214
|
+
choice,
|
|
1215
|
+
onSelect: () => {
|
|
1216
|
+
view.execute(row.command);
|
|
1217
|
+
view.close();
|
|
1218
|
+
}
|
|
1219
|
+
}, row.command))
|
|
1220
|
+
}, i))
|
|
1221
|
+
})
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
//#endregion
|
|
1225
|
+
//#region src/ui/toolbar.tsx
|
|
1226
|
+
/**
|
|
1227
|
+
* The app's toolbar look — Tailwind render props over the headless <Toolbar>.
|
|
1228
|
+
* The app owns the pixels; ui-core owns the physics (measure → solve →
|
|
1229
|
+
* overflow). Nothing here knows about breakpoints or widths.
|
|
1230
|
+
*/
|
|
1231
|
+
const BTN_BASE = "flex h-8 w-auto min-w-8 cursor-pointer items-center justify-center rounded-md p-[5px] transition-colors";
|
|
1232
|
+
const BTN_HOVER = "hover:bg-hover hover:ring hover:ring-accent";
|
|
1233
|
+
const BTN_ACTIVE = "border-none bg-selected text-accent shadow ring ring-accent";
|
|
1234
|
+
const BTN_DISABLED = "cursor-not-allowed opacity-50 hover:bg-transparent hover:ring-0";
|
|
1235
|
+
const buttonClass = (active, enabled = true) => `${BTN_BASE} ${active ? BTN_ACTIVE : BTN_HOVER} ${enabled ? "" : BTN_DISABLED}`;
|
|
1236
|
+
/**
|
|
1237
|
+
* The part name of a button in a bar, for page CSS through `::part()`: `toolbar-button`, and
|
|
1238
|
+
* `toolbar-button-active` too while it's pressed or its menu is open. Part names are public, as
|
|
1239
|
+
* the Theming page lists them.
|
|
1240
|
+
*/
|
|
1241
|
+
const buttonPart = (active) => active ? "toolbar-button toolbar-button-active" : "toolbar-button";
|
|
1242
|
+
/** The part name of a mode tab: `mode-tab`, and `mode-tab-active` too for the open mode. */
|
|
1243
|
+
const modeTabPart = (active) => active ? "mode-tab mode-tab-active" : "mode-tab";
|
|
1244
|
+
const TAB_BASE = "flex h-8 w-auto min-w-8 cursor-pointer items-center justify-center rounded-none border-b-2 px-2 py-1 text-sm transition-colors hover:bg-transparent hover:border-b-fg-muted";
|
|
1245
|
+
const TAB_ACTIVE = "border-b-accent text-accent hover:border-b-accent";
|
|
1246
|
+
const TAB_INACTIVE = "border-b-transparent";
|
|
1247
|
+
const tabClass = (active, enabled = true) => `${TAB_BASE} ${active ? TAB_ACTIVE : TAB_INACTIVE} ${enabled ? "" : "cursor-not-allowed opacity-50"}`;
|
|
1248
|
+
function CommandButton({ cmd, variant, run }) {
|
|
1249
|
+
const isTab = variant === "label";
|
|
1250
|
+
const draw = (active) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1251
|
+
type: "button",
|
|
1252
|
+
onClick: run,
|
|
1253
|
+
disabled: !cmd.enabled,
|
|
1254
|
+
"aria-pressed": cmd.menu ? void 0 : active || void 0,
|
|
1255
|
+
"aria-haspopup": cmd.menu ? "menu" : void 0,
|
|
1256
|
+
"aria-expanded": cmd.menu ? active : void 0,
|
|
1257
|
+
"aria-label": cmd.label,
|
|
1258
|
+
title: cmd.disabledReason ?? cmd.label,
|
|
1259
|
+
part: isTab ? modeTabPart(active) : buttonPart(active),
|
|
1260
|
+
className: isTab ? tabClass(active, cmd.enabled) : buttonClass(active, cmd.enabled),
|
|
1261
|
+
children: isTab ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1262
|
+
className: "whitespace-nowrap px-1",
|
|
1263
|
+
children: cmd.label
|
|
1264
|
+
}) : variant === "icon+label" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1265
|
+
className: "flex items-center whitespace-nowrap text-sm",
|
|
1266
|
+
children: [cmd.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1267
|
+
name: cmd.icon,
|
|
1268
|
+
size: 20,
|
|
1269
|
+
accent: cmd.iconAccent,
|
|
1270
|
+
className: "me-1.5 shrink-0"
|
|
1271
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: cmd.label })]
|
|
1272
|
+
}) : cmd.icon ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1273
|
+
name: cmd.icon,
|
|
1274
|
+
size: 20,
|
|
1275
|
+
accent: cmd.iconAccent
|
|
1276
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1277
|
+
className: "text-sm",
|
|
1278
|
+
children: cmd.label
|
|
1279
|
+
})
|
|
1280
|
+
});
|
|
1281
|
+
if (!cmd.menu) return draw(cmd.active);
|
|
1282
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuAnchor, {
|
|
1283
|
+
menuId: cmd.menu,
|
|
1284
|
+
children: draw
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Makes a control inside an item one more stop of the toolbar's arrow keys: the zoom field's
|
|
1289
|
+
* buttons are reached with the arrows, like the bar's own, and the bar stays one Tab stop.
|
|
1290
|
+
*/
|
|
1291
|
+
const ARROW_STOP = {
|
|
1292
|
+
[_embedpdf_web.TOOLBAR_ITEM_ATTRIBUTE]: "",
|
|
1293
|
+
className: "contents"
|
|
1294
|
+
};
|
|
1295
|
+
function ZoomControls() {
|
|
1296
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
1297
|
+
const commands = (0, _embedpdf_react_commands.useCommands)();
|
|
1298
|
+
const zoomIn = (0, _embedpdf_react_commands.useCommand)("zoom:in");
|
|
1299
|
+
const zoomOut = (0, _embedpdf_react_commands.useCommand)("zoom:out");
|
|
1300
|
+
const zoomMenu = (0, _embedpdf_react_commands.useCommand)("menu:zoom");
|
|
1301
|
+
const shell = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_shell.ShellToken);
|
|
1302
|
+
const stage = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_stage.StageToken);
|
|
1303
|
+
const level = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_stage.StageToken, (stage) => stage.getZoomLevel(), 1);
|
|
1304
|
+
const pct = Math.round((level ?? 1) * 100);
|
|
1305
|
+
const [inputValue, setInputValue] = (0, react.useState)(String(pct));
|
|
1306
|
+
(0, react.useEffect)(() => setInputValue(String(pct)), [pct]);
|
|
1307
|
+
const submit = (event) => {
|
|
1308
|
+
event.preventDefault();
|
|
1309
|
+
const value = parseFloat(inputValue);
|
|
1310
|
+
if (!isNaN(value) && value > 0) stage?.zoomTo({ level: value / 100 });
|
|
1311
|
+
};
|
|
1312
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuAnchor, {
|
|
1313
|
+
menuId: "zoom",
|
|
1314
|
+
className: "relative block",
|
|
1315
|
+
children: (zoomOpen) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1316
|
+
className: "bg-hover flex items-center rounded",
|
|
1317
|
+
children: [
|
|
1318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1319
|
+
onSubmit: submit,
|
|
1320
|
+
className: "flex min-w-0 flex-nowrap items-center overflow-hidden whitespace-nowrap",
|
|
1321
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1322
|
+
name: "zoom",
|
|
1323
|
+
type: "text",
|
|
1324
|
+
inputMode: "numeric",
|
|
1325
|
+
pattern: "\\d*",
|
|
1326
|
+
"aria-label": t("commands.zoom.level"),
|
|
1327
|
+
className: "h-6 w-8 min-w-0 shrink border-0 bg-transparent p-0 text-end text-sm outline-none focus:outline-none",
|
|
1328
|
+
value: inputValue,
|
|
1329
|
+
onChange: (event) => setInputValue(event.target.value.replace(/[^0-9]/g, "")),
|
|
1330
|
+
onBlur: () => {
|
|
1331
|
+
if (!inputValue || parseFloat(inputValue) <= 0) setInputValue(String(pct));
|
|
1332
|
+
}
|
|
1333
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1334
|
+
className: "shrink-0 text-sm",
|
|
1335
|
+
children: "%"
|
|
1336
|
+
})]
|
|
1337
|
+
}),
|
|
1338
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1339
|
+
...ARROW_STOP,
|
|
1340
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1341
|
+
type: "button",
|
|
1342
|
+
"aria-haspopup": "menu",
|
|
1343
|
+
"aria-expanded": zoomOpen,
|
|
1344
|
+
title: zoomMenu.label,
|
|
1345
|
+
part: buttonPart(false),
|
|
1346
|
+
className: buttonClass(false),
|
|
1347
|
+
onClick: () => shell?.toggleMenu("zoom"),
|
|
1348
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1349
|
+
name: "chevronDown",
|
|
1350
|
+
size: 20
|
|
1351
|
+
})
|
|
1352
|
+
})
|
|
1353
|
+
}),
|
|
1354
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1355
|
+
...ARROW_STOP,
|
|
1356
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1357
|
+
type: "button",
|
|
1358
|
+
title: zoomOut.label,
|
|
1359
|
+
part: buttonPart(false),
|
|
1360
|
+
className: buttonClass(false),
|
|
1361
|
+
onClick: () => commands.execute("zoom:out"),
|
|
1362
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1363
|
+
name: "zoomOut",
|
|
1364
|
+
size: 20
|
|
1365
|
+
})
|
|
1366
|
+
})
|
|
1367
|
+
}),
|
|
1368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1369
|
+
...ARROW_STOP,
|
|
1370
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1371
|
+
type: "button",
|
|
1372
|
+
title: zoomIn.label,
|
|
1373
|
+
part: buttonPart(false),
|
|
1374
|
+
className: buttonClass(false),
|
|
1375
|
+
onClick: () => commands.execute("zoom:in"),
|
|
1376
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1377
|
+
name: "zoomIn",
|
|
1378
|
+
size: 20
|
|
1379
|
+
})
|
|
1380
|
+
})
|
|
1381
|
+
})
|
|
1382
|
+
]
|
|
1383
|
+
})
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
/** The 'button' variant of the zoom slot — a single icon that opens the menu. */
|
|
1387
|
+
function ZoomButton() {
|
|
1388
|
+
const shell = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_shell.ShellToken);
|
|
1389
|
+
const zoomMenu = (0, _embedpdf_react_commands.useCommand)("menu:zoom");
|
|
1390
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuAnchor, {
|
|
1391
|
+
menuId: "zoom",
|
|
1392
|
+
children: (zoomOpen) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1393
|
+
type: "button",
|
|
1394
|
+
title: zoomMenu.label,
|
|
1395
|
+
"aria-haspopup": "menu",
|
|
1396
|
+
"aria-expanded": zoomOpen,
|
|
1397
|
+
part: buttonPart(zoomOpen),
|
|
1398
|
+
className: buttonClass(zoomOpen),
|
|
1399
|
+
onClick: () => shell?.toggleMenu("zoom"),
|
|
1400
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1401
|
+
name: "zoomIn",
|
|
1402
|
+
size: 20
|
|
1403
|
+
})
|
|
1404
|
+
})
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function GroupTrigger({ view }) {
|
|
1408
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1409
|
+
const activeHidden = view.commands.some((command) => command.active);
|
|
1410
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1411
|
+
className: "relative inline-flex",
|
|
1412
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1413
|
+
type: "button",
|
|
1414
|
+
onClick: () => setOpen((previous) => !previous),
|
|
1415
|
+
"aria-haspopup": "menu",
|
|
1416
|
+
"aria-expanded": open,
|
|
1417
|
+
part: view.id === "modes" ? modeTabPart(activeHidden) : buttonPart(open),
|
|
1418
|
+
className: tabClass(activeHidden),
|
|
1419
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1420
|
+
name: "chevronDown",
|
|
1421
|
+
size: 18
|
|
1422
|
+
})
|
|
1423
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, { view: (0, _embedpdf_core_ui.groupMenuView)(view, open, () => setOpen(false)) })]
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
function CollapsedModes({ view }) {
|
|
1427
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1428
|
+
const active = view.commands.find((command) => command.active) ?? view.commands[0];
|
|
1429
|
+
const isActive = Boolean(active?.active && active.id !== _embedpdf_core_ui.VIEW_MODE_COMMAND);
|
|
1430
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1431
|
+
style: { width: 100 },
|
|
1432
|
+
className: "relative",
|
|
1433
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1434
|
+
type: "button",
|
|
1435
|
+
onClick: () => setOpen((previous) => !previous),
|
|
1436
|
+
"aria-haspopup": "menu",
|
|
1437
|
+
"aria-expanded": open,
|
|
1438
|
+
part: buttonPart(open),
|
|
1439
|
+
className: "bg-surface hover:bg-hover flex w-full cursor-pointer appearance-none items-center rounded-md py-1.5 pe-2 ps-3 text-[13px] transition-colors",
|
|
1440
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1441
|
+
className: `min-w-0 flex-1 truncate text-start ${isActive ? "text-accent" : "text-fg"}`,
|
|
1442
|
+
children: active?.label
|
|
1443
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1444
|
+
name: "chevronDown",
|
|
1445
|
+
size: 16,
|
|
1446
|
+
className: isActive ? "text-accent" : "text-fg-secondary"
|
|
1447
|
+
})]
|
|
1448
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, { view: (0, _embedpdf_core_ui.groupMenuView)(view, open, () => setOpen(false)) })]
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
function CollapsedMenu({ view }) {
|
|
1452
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
1453
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1454
|
+
const shown = view.commands.find((command) => command.active) ?? view.commands[0];
|
|
1455
|
+
const label = view.labelKey ? t(view.labelKey) : shown?.label;
|
|
1456
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1457
|
+
className: "relative inline-flex",
|
|
1458
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1459
|
+
type: "button",
|
|
1460
|
+
onClick: () => setOpen((previous) => !previous),
|
|
1461
|
+
"aria-haspopup": "menu",
|
|
1462
|
+
"aria-expanded": open,
|
|
1463
|
+
"aria-label": label,
|
|
1464
|
+
title: label,
|
|
1465
|
+
part: buttonPart(open),
|
|
1466
|
+
className: buttonClass(open),
|
|
1467
|
+
children: [shown?.icon ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1468
|
+
name: shown.icon,
|
|
1469
|
+
size: 20,
|
|
1470
|
+
accent: shown.iconAccent
|
|
1471
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1472
|
+
className: "whitespace-nowrap px-1 text-sm",
|
|
1473
|
+
children: label
|
|
1474
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1475
|
+
name: "chevronDown",
|
|
1476
|
+
size: 14,
|
|
1477
|
+
className: "text-fg-secondary"
|
|
1478
|
+
})]
|
|
1479
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, { view: (0, _embedpdf_core_ui.groupMenuView)(view, open, () => setOpen(false)) })]
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
function OverflowMenu({ view }) {
|
|
1483
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, {
|
|
1484
|
+
view,
|
|
1485
|
+
align: "end"
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
function OverflowTrigger({ isOpen, toggle }) {
|
|
1489
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1490
|
+
type: "button",
|
|
1491
|
+
onClick: toggle,
|
|
1492
|
+
"aria-haspopup": "menu",
|
|
1493
|
+
"aria-expanded": isOpen,
|
|
1494
|
+
title: (0, _embedpdf_react_i18n.useT)()("demo.more"),
|
|
1495
|
+
part: buttonPart(isOpen),
|
|
1496
|
+
className: buttonClass(isOpen),
|
|
1497
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
|
|
1498
|
+
name: "dots",
|
|
1499
|
+
size: 20
|
|
1500
|
+
})
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
/** The custom items in `bar`, in order. */
|
|
1504
|
+
function customItemsOf(bar) {
|
|
1505
|
+
return [
|
|
1506
|
+
...bar.sections.start ?? [],
|
|
1507
|
+
...bar.sections.center ?? [],
|
|
1508
|
+
...bar.sections.end ?? []
|
|
1509
|
+
].flatMap((group) => group.items.filter((child) => typeof child === "object" && "slot" in child));
|
|
1510
|
+
}
|
|
1511
|
+
/** What your item stands for while nothing is slotted into it: its command's button. */
|
|
1512
|
+
function StandIn({ commandId }) {
|
|
1513
|
+
const command = (0, _embedpdf_react_commands.useCommand)(commandId);
|
|
1514
|
+
if (!command.visible) return null;
|
|
1515
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandButton, {
|
|
1516
|
+
cmd: command,
|
|
1517
|
+
variant: "icon",
|
|
1518
|
+
run: () => void command.run()
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
function AppToolbar({ bar, orientation = "horizontal", className }) {
|
|
1522
|
+
const vertical = orientation === "vertical";
|
|
1523
|
+
const builtIn = {
|
|
1524
|
+
"measurement-scale": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MeasurementScaleButton, {}),
|
|
1525
|
+
zoom: (variant) => variant === "button" || vertical ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}),
|
|
1526
|
+
"quick-stamps": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStamps, {}),
|
|
1527
|
+
"page-number": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageNumber, {}),
|
|
1528
|
+
"view-only": (variant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewOnlyLabel, { compact: variant === "icon" || vertical })
|
|
1529
|
+
};
|
|
1530
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_toolbar.Toolbar, {
|
|
1531
|
+
bar,
|
|
1532
|
+
orientation,
|
|
1533
|
+
gap: 8,
|
|
1534
|
+
separatorWidth: 1,
|
|
1535
|
+
className,
|
|
1536
|
+
renderCommand: (cmd, variant, run) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandButton, {
|
|
1537
|
+
cmd,
|
|
1538
|
+
variant,
|
|
1539
|
+
run
|
|
1540
|
+
}),
|
|
1541
|
+
renderCustom: {
|
|
1542
|
+
...Object.fromEntries(customItemsOf(bar).filter((item) => !Object.prototype.hasOwnProperty.call(builtIn, item.slot)).map((item) => [item.slot, (variant, ctx) => ctx.layer === "measure" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ItemSocket, {
|
|
1543
|
+
id: item.slot,
|
|
1544
|
+
variant: item.variants ? variant : null,
|
|
1545
|
+
orientation,
|
|
1546
|
+
measure: ctx.measure,
|
|
1547
|
+
children: item.terminal ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StandIn, { commandId: item.terminal }) : null
|
|
1548
|
+
})])),
|
|
1549
|
+
...builtIn
|
|
1550
|
+
},
|
|
1551
|
+
renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedModes, { view }) : view.collapse === "menu" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedMenu, { view }) : void 0,
|
|
1552
|
+
renderGroupTrigger: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupTrigger, { view }),
|
|
1553
|
+
renderSeparator: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `bg-border self-center ${vertical ? "h-px w-6" : "h-6 w-px"}` }),
|
|
1554
|
+
renderOverflowTrigger: (isOpen, toggle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverflowTrigger, {
|
|
1555
|
+
isOpen,
|
|
1556
|
+
toggle
|
|
1557
|
+
}),
|
|
1558
|
+
renderOverflowMenu: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverflowMenu, { view })
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
//#endregion
|
|
1562
|
+
Object.defineProperty(exports, "AppToolbar", {
|
|
1563
|
+
enumerable: true,
|
|
1564
|
+
get: function() {
|
|
1565
|
+
return AppToolbar;
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
Object.defineProperty(exports, "BarEdgeProvider", {
|
|
1569
|
+
enumerable: true,
|
|
1570
|
+
get: function() {
|
|
1571
|
+
return BarEdgeProvider;
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
Object.defineProperty(exports, "CommandButton", {
|
|
1575
|
+
enumerable: true,
|
|
1576
|
+
get: function() {
|
|
1577
|
+
return CommandButton;
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
Object.defineProperty(exports, "MenuAnchor", {
|
|
1581
|
+
enumerable: true,
|
|
1582
|
+
get: function() {
|
|
1583
|
+
return MenuAnchor;
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
Object.defineProperty(exports, "MenuItem", {
|
|
1587
|
+
enumerable: true,
|
|
1588
|
+
get: function() {
|
|
1589
|
+
return MenuItem;
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
Object.defineProperty(exports, "MenuPopover", {
|
|
1593
|
+
enumerable: true,
|
|
1594
|
+
get: function() {
|
|
1595
|
+
return MenuPopover;
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
Object.defineProperty(exports, "SlottedEdgesProvider", {
|
|
1599
|
+
enumerable: true,
|
|
1600
|
+
get: function() {
|
|
1601
|
+
return SlottedEdgesProvider;
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
Object.defineProperty(exports, "TOOLBAR_ITEM_CONTEXT_ATTRIBUTES", {
|
|
1605
|
+
enumerable: true,
|
|
1606
|
+
get: function() {
|
|
1607
|
+
return TOOLBAR_ITEM_CONTEXT_ATTRIBUTES;
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
Object.defineProperty(exports, "bothAllow", {
|
|
1611
|
+
enumerable: true,
|
|
1612
|
+
get: function() {
|
|
1613
|
+
return bothAllow;
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
Object.defineProperty(exports, "buttonClass", {
|
|
1617
|
+
enumerable: true,
|
|
1618
|
+
get: function() {
|
|
1619
|
+
return buttonClass;
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
Object.defineProperty(exports, "buttonPart", {
|
|
1623
|
+
enumerable: true,
|
|
1624
|
+
get: function() {
|
|
1625
|
+
return buttonPart;
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
Object.defineProperty(exports, "byHandSettingsOf", {
|
|
1629
|
+
enumerable: true,
|
|
1630
|
+
get: function() {
|
|
1631
|
+
return byHandSettingsOf;
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
Object.defineProperty(exports, "createSlottedEdges", {
|
|
1635
|
+
enumerable: true,
|
|
1636
|
+
get: function() {
|
|
1637
|
+
return createSlottedEdges;
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
Object.defineProperty(exports, "disabledSettingsOf", {
|
|
1641
|
+
enumerable: true,
|
|
1642
|
+
get: function() {
|
|
1643
|
+
return disabledSettingsOf;
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
Object.defineProperty(exports, "sameByHand", {
|
|
1647
|
+
enumerable: true,
|
|
1648
|
+
get: function() {
|
|
1649
|
+
return sameByHand;
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
Object.defineProperty(exports, "useSlottedEdges", {
|
|
1653
|
+
enumerable: true,
|
|
1654
|
+
get: function() {
|
|
1655
|
+
return useSlottedEdges;
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
//# sourceMappingURL=toolbar-CJu2wKTU.cjs.map
|