@webflow/webflow-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +5 -0
- package/.turbo/turbo-build.log +13 -0
- package/.turbo/turbo-lint.log +5 -0
- package/CHANGELOG.md +7 -0
- package/dist/devlink-engine/_Builtin/BackgroundVideo.d.ts +25 -0
- package/dist/devlink-engine/_Builtin/BackgroundVideo.js +38 -0
- package/dist/devlink-engine/_Builtin/Basic.d.ts +740 -0
- package/dist/devlink-engine/_Builtin/Basic.js +100 -0
- package/dist/devlink-engine/_Builtin/Dropdown.d.ts +38 -0
- package/dist/devlink-engine/_Builtin/Dropdown.js +47 -0
- package/dist/devlink-engine/_Builtin/Facebook.d.ts +11 -0
- package/dist/devlink-engine/_Builtin/Facebook.js +22 -0
- package/dist/devlink-engine/_Builtin/Form.d.ts +8401 -0
- package/dist/devlink-engine/_Builtin/Form.js +316 -0
- package/dist/devlink-engine/_Builtin/Map.d.ts +14 -0
- package/dist/devlink-engine/_Builtin/Map.js +71 -0
- package/dist/devlink-engine/_Builtin/Navbar.d.ts +53 -0
- package/dist/devlink-engine/_Builtin/Navbar.js +240 -0
- package/dist/devlink-engine/_Builtin/Slider.d.ts +876 -0
- package/dist/devlink-engine/_Builtin/Slider.js +304 -0
- package/dist/devlink-engine/_Builtin/Tabs.d.ts +34 -0
- package/dist/devlink-engine/_Builtin/Tabs.js +84 -0
- package/dist/devlink-engine/_Builtin/Twitter.d.ts +17 -0
- package/dist/devlink-engine/_Builtin/Twitter.js +41 -0
- package/dist/devlink-engine/_Builtin/Typography.d.ts +291 -0
- package/dist/devlink-engine/_Builtin/Typography.js +38 -0
- package/dist/devlink-engine/_Builtin/Video.d.ts +8 -0
- package/dist/devlink-engine/_Builtin/Video.js +8 -0
- package/dist/devlink-engine/_Builtin/YouTubeVideo.d.ts +15 -0
- package/dist/devlink-engine/_Builtin/YouTubeVideo.js +27 -0
- package/dist/devlink-engine/_Builtin/index.d.ts +13 -0
- package/dist/devlink-engine/_Builtin/index.js +13 -0
- package/dist/devlink-engine/interactions.d.ts +13 -0
- package/dist/devlink-engine/interactions.js +86 -0
- package/dist/devlink-engine/types.d.ts +32 -0
- package/dist/devlink-engine/types.js +1 -0
- package/dist/devlink-engine/utils.d.ts +66 -0
- package/dist/devlink-engine/utils.js +133 -0
- package/dist/index.js +197591 -0
- package/package.json +30 -0
- package/src/index.ts +33 -0
- package/tsconfig.json +5 -0
- package/tsup.config.ts +6 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { loadScript } from "../utils";
|
|
3
|
+
export function FormWrapper({ className = "", state: initialState = "normal", onSubmit, children, ...props }) {
|
|
4
|
+
const [state, setState] = React.useState(initialState);
|
|
5
|
+
return React.createElement("div", {
|
|
6
|
+
className: className + " w-form",
|
|
7
|
+
...props,
|
|
8
|
+
}, React.Children.map(children, (child) => {
|
|
9
|
+
if (child.type === FormForm) {
|
|
10
|
+
return React.cloneElement(child, {
|
|
11
|
+
...child.props,
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
onSubmit: (e) => {
|
|
14
|
+
try {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
// If grecaptcha is defined, it means there is a ReCaptcha in the form
|
|
17
|
+
if (window.grecaptcha) {
|
|
18
|
+
// If the response is empty, it means the user didn't check the box
|
|
19
|
+
if (!window.grecaptcha?.getResponse()) {
|
|
20
|
+
alert(`Please confirm you’re not a robot.`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (onSubmit) {
|
|
25
|
+
onSubmit(e);
|
|
26
|
+
setState("success");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
/**
|
|
31
|
+
* Capture the error to correctly set the state, but rethrow it
|
|
32
|
+
* in case another error handling method is used above up in
|
|
33
|
+
* the tree (e.g. Error Boundaries)
|
|
34
|
+
*/
|
|
35
|
+
setState("error");
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
style: {
|
|
40
|
+
display: state == "normal" || state == "error" ? "block" : "none",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (child.type === FormSuccessMessage) {
|
|
45
|
+
return React.cloneElement(child, {
|
|
46
|
+
...child.props,
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
style: { display: state == "success" ? "block" : "none" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (child.type === FormErrorMessage) {
|
|
52
|
+
return React.cloneElement(child, {
|
|
53
|
+
...child.props,
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
style: { display: state == "error" ? "block" : "none" },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return child;
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
export function FormForm(props) {
|
|
62
|
+
return React.createElement("form", props);
|
|
63
|
+
}
|
|
64
|
+
export function FormBlockLabel(props) {
|
|
65
|
+
return React.createElement("label", props);
|
|
66
|
+
}
|
|
67
|
+
export function FormTextInput({ className = "", ...props }) {
|
|
68
|
+
return React.createElement("input", {
|
|
69
|
+
type: "text",
|
|
70
|
+
className: className + " w-input",
|
|
71
|
+
...props,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function FormTextarea({ className = "", ...props }) {
|
|
75
|
+
return React.createElement("input", {
|
|
76
|
+
type: "textarea",
|
|
77
|
+
className: className + " w-input",
|
|
78
|
+
...props,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function FormInlineLabel({ className = "", ...props }) {
|
|
82
|
+
return React.createElement("span", {
|
|
83
|
+
className: className + " w-form-label",
|
|
84
|
+
...props,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function FormCheckboxWrapper({ className = "", ...props }) {
|
|
88
|
+
return React.createElement("label", {
|
|
89
|
+
className: className + " w-checkbox",
|
|
90
|
+
...props,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function FormRadioWrapper({ className = "", ...props }) {
|
|
94
|
+
return React.createElement("label", {
|
|
95
|
+
className: className + " w-radio",
|
|
96
|
+
...props,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function FormCheckboxInput({ className = "", checked = false, ...props }) {
|
|
100
|
+
const checkedProps = {};
|
|
101
|
+
if (typeof checked === "boolean") {
|
|
102
|
+
checkedProps["defaultChecked"] = checked;
|
|
103
|
+
}
|
|
104
|
+
return React.createElement("input", {
|
|
105
|
+
type: "checkbox",
|
|
106
|
+
className: className + " w-checkbox",
|
|
107
|
+
...checkedProps,
|
|
108
|
+
...props,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export function FormRadioInput({ className = "", inputType, ...props }) {
|
|
112
|
+
if (inputType === "custom") {
|
|
113
|
+
// TODO: support FormCustomRadioInput
|
|
114
|
+
}
|
|
115
|
+
return React.createElement("input", {
|
|
116
|
+
className: className + " w-radio-input",
|
|
117
|
+
...props,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const MAX_FILE_SIZE_DEFAULT = 10485760;
|
|
121
|
+
const FileUploadContext = React.createContext({
|
|
122
|
+
files: null,
|
|
123
|
+
error: null,
|
|
124
|
+
maxSize: MAX_FILE_SIZE_DEFAULT,
|
|
125
|
+
setFiles: () => { },
|
|
126
|
+
setError: () => { },
|
|
127
|
+
});
|
|
128
|
+
export function FormFileUploadWrapper({ maxSize = MAX_FILE_SIZE_DEFAULT, ...props }) {
|
|
129
|
+
const [files, setFiles] = React.useState(null);
|
|
130
|
+
const [error, setError] = React.useState(null);
|
|
131
|
+
return React.createElement(FileUploadContext.Provider, {
|
|
132
|
+
value: { files, setFiles, error, setError, maxSize },
|
|
133
|
+
}, React.createElement(_FormFileUploadWrapper, { ...props }));
|
|
134
|
+
}
|
|
135
|
+
export function _FormFileUploadWrapper({ className = "", ...props }) {
|
|
136
|
+
return React.createElement("div", {
|
|
137
|
+
className: className + " w-file-upload",
|
|
138
|
+
...props,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export function FormFileUploadDefault({ className = "", ...props }) {
|
|
142
|
+
const { files, error } = React.useContext(FileUploadContext);
|
|
143
|
+
return React.createElement("div", {
|
|
144
|
+
className: className + " w-file-upload-default",
|
|
145
|
+
...props,
|
|
146
|
+
style: {
|
|
147
|
+
...props.style,
|
|
148
|
+
display: !files || error ? "block" : "none",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function FormFileUploadInput({ className = "", ...props }) {
|
|
153
|
+
const { setFiles, setError, maxSize } = React.useContext(FileUploadContext);
|
|
154
|
+
return React.createElement("input", {
|
|
155
|
+
type: "file",
|
|
156
|
+
className: className + " w-file-upload-input",
|
|
157
|
+
...props,
|
|
158
|
+
onChange: (e) => {
|
|
159
|
+
if (e.target.files) {
|
|
160
|
+
if (e.target.files[0].size <= maxSize) {
|
|
161
|
+
setError(null);
|
|
162
|
+
setFiles(e.target.files);
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
setError("SIZE_ERROR");
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export function FormFileUploadLabel({ className = "", ...props }) {
|
|
171
|
+
return React.createElement("label", {
|
|
172
|
+
className: className + " w-file-upload-label",
|
|
173
|
+
...props,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export function FormFileUploadText({ className = "", ...props }) {
|
|
177
|
+
return React.createElement("div", {
|
|
178
|
+
className: className + " w-inline-block",
|
|
179
|
+
...props,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export function FormFileUploadInfo({ className = "", ...props }) {
|
|
183
|
+
return React.createElement("div", {
|
|
184
|
+
className: className + " w-file-upload-info",
|
|
185
|
+
...props,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export function FormFileUploadUploading({ className = "", ...props }) {
|
|
189
|
+
return React.createElement("div", {
|
|
190
|
+
className: className + " w-file-upload-uploading",
|
|
191
|
+
style: { ...props.style, display: "none" },
|
|
192
|
+
...props,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export function FormFileUploadUploadingBtn({ className = "", ...props }) {
|
|
196
|
+
return React.createElement("div", {
|
|
197
|
+
className: className + " w-file-upload-uploading-btn",
|
|
198
|
+
...props,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function FormFileUploadUploadingIcon({ className = "", ...props }) {
|
|
202
|
+
return React.createElement("svg", {
|
|
203
|
+
className: className + " icon w-icon-file-upload-uploading",
|
|
204
|
+
...props,
|
|
205
|
+
}, React.createElement(React.Fragment, null,
|
|
206
|
+
React.createElement("path", { fill: "currentColor", opacity: ".2", d: "M15 30a15 15 0 1 1 0-30 15 15 0 0 1 0 30zm0-3a12 12 0 1 0 0-24 12 12 0 0 0 0 24z" }),
|
|
207
|
+
React.createElement("path", { fill: "currentColor", opacity: ".75", d: "M0 15A15 15 0 0 1 15 0v3A12 12 0 0 0 3 15H0z" },
|
|
208
|
+
React.createElement("animateTransform", { attributeName: "transform", attributeType: "XML", dur: "0.6s", from: "0 15 15", repeatCount: "indefinite", to: "360 15 15", type: "rotate" }))));
|
|
209
|
+
}
|
|
210
|
+
export function FormFileUploadSuccess({ className = "", ...props }) {
|
|
211
|
+
const { files, error } = React.useContext(FileUploadContext);
|
|
212
|
+
return React.createElement("div", {
|
|
213
|
+
className: className + " w-file-upload-success",
|
|
214
|
+
...props,
|
|
215
|
+
style: {
|
|
216
|
+
...props.style,
|
|
217
|
+
display: !!files && !error ? "block" : "none",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function FormFileUploadFile({ className = "", ...props }) {
|
|
222
|
+
return React.createElement("div", {
|
|
223
|
+
className: className + " w-file-upload-file",
|
|
224
|
+
...props,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export function FormFileUploadFileName({ className = "", ...props }) {
|
|
228
|
+
const { files } = React.useContext(FileUploadContext);
|
|
229
|
+
return React.createElement("div", {
|
|
230
|
+
className: className + " w-file-upload-file-name",
|
|
231
|
+
...props,
|
|
232
|
+
}, files && files[0].name);
|
|
233
|
+
}
|
|
234
|
+
export function FormFileUploadRemoveLink({ className = "", ...props }) {
|
|
235
|
+
const { setFiles } = React.useContext(FileUploadContext);
|
|
236
|
+
return React.createElement("div", {
|
|
237
|
+
className: className + " w-file-remove-link",
|
|
238
|
+
...props,
|
|
239
|
+
onClick: () => {
|
|
240
|
+
setFiles(null);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
export function FormFileUploadError({ className = "", ...props }) {
|
|
245
|
+
const { error } = React.useContext(FileUploadContext);
|
|
246
|
+
return React.createElement("div", {
|
|
247
|
+
className: className + " w-file-upload-error",
|
|
248
|
+
...props,
|
|
249
|
+
style: {
|
|
250
|
+
...props.style,
|
|
251
|
+
display: !!error ? "block" : "none",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export function FormFileUploadErrorMsg({ errors, className = "", ...props }) {
|
|
256
|
+
const { error } = React.useContext(FileUploadContext);
|
|
257
|
+
return React.createElement("div", {
|
|
258
|
+
className: className + " w-file-upload-error-msg",
|
|
259
|
+
...props,
|
|
260
|
+
}, errors[error ?? "GENERIC_ERROR"]);
|
|
261
|
+
}
|
|
262
|
+
export function FormButton({ className = "", value, ...props }) {
|
|
263
|
+
return React.createElement("input", {
|
|
264
|
+
type: "submit",
|
|
265
|
+
value: value ?? "",
|
|
266
|
+
className: className + " w-button",
|
|
267
|
+
...props,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
export function SearchForm(props) {
|
|
271
|
+
return React.createElement("form", props);
|
|
272
|
+
}
|
|
273
|
+
export function SearchInput({ className = "", ...props }) {
|
|
274
|
+
return React.createElement("input", {
|
|
275
|
+
type: "text",
|
|
276
|
+
className: className + " w-input",
|
|
277
|
+
...props,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export function SearchButton({ value = "", className = "", ...props }) {
|
|
281
|
+
return React.createElement("input", {
|
|
282
|
+
type: "submit",
|
|
283
|
+
value,
|
|
284
|
+
className: className + " w-button",
|
|
285
|
+
...props,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
export function FormSuccessMessage({ className = "", ...props }) {
|
|
289
|
+
return React.createElement("div", {
|
|
290
|
+
className: className + " w-form-done",
|
|
291
|
+
...props,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
export function FormErrorMessage({ className = "", ...props }) {
|
|
295
|
+
return React.createElement("div", {
|
|
296
|
+
className: className + " w-form-fail",
|
|
297
|
+
...props,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function hasValue(str) {
|
|
301
|
+
if (typeof str !== "string")
|
|
302
|
+
return false;
|
|
303
|
+
// is  
|
|
304
|
+
return str.replace(/^[s ]+|[s ]+$/g, "").length > 0;
|
|
305
|
+
}
|
|
306
|
+
export function FormSelect({ options, className = "", ...props }) {
|
|
307
|
+
return React.createElement("select", { className: className + " w-select", ...props }, options.map(({ v, t }, index) => React.createElement("option", { key: index, value: hasValue(v) ? v : "" }, hasValue(t) ? t : "")));
|
|
308
|
+
}
|
|
309
|
+
export function FormReCaptcha({ siteKey = "", theme = "light", size = "normal", }) {
|
|
310
|
+
React.useEffect(() => {
|
|
311
|
+
loadScript("https://www.google.com/recaptcha/api.js", {
|
|
312
|
+
cacheRegex: /(http|https):\/\/(www)?.+\/recaptcha/,
|
|
313
|
+
});
|
|
314
|
+
}, []);
|
|
315
|
+
return (React.createElement("div", { className: "g-recaptcha", "data-sitekey": siteKey, "data-theme": theme, "data-size": size }));
|
|
316
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
type MapWidgetProps = {
|
|
3
|
+
className?: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
zoom?: number;
|
|
6
|
+
latlng?: string;
|
|
7
|
+
mapStyle?: "roadmap" | "satellite" | "hybrid" | "terrain";
|
|
8
|
+
tooltip?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
enableScroll?: boolean;
|
|
11
|
+
enableTouch?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function MapWidget({ apiKey, mapStyle, zoom, latlng, tooltip, title, enableScroll, enableTouch, className, ...props }: MapWidgetProps): JSX.Element;
|
|
14
|
+
export default Map;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { cj, loadScript } from "../utils";
|
|
3
|
+
function buildTitle(title, tooltip) {
|
|
4
|
+
let markerTitle = "Map pin";
|
|
5
|
+
if (title && tooltip) {
|
|
6
|
+
markerTitle = `Map pin on ${title} showing location of ${tooltip}`;
|
|
7
|
+
}
|
|
8
|
+
else if (title && !tooltip) {
|
|
9
|
+
markerTitle = `Map pin on ${title}`;
|
|
10
|
+
}
|
|
11
|
+
else if (!title && tooltip) {
|
|
12
|
+
markerTitle = `Map pin showing location of ${tooltip}`;
|
|
13
|
+
}
|
|
14
|
+
return markerTitle;
|
|
15
|
+
}
|
|
16
|
+
export function MapWidget({ apiKey = "", mapStyle = "roadmap", zoom = 12, latlng = "51.511214,-0.119824", tooltip = "", title = "", enableScroll = true, enableTouch = true, className = "", ...props }) {
|
|
17
|
+
const mapRef = useRef(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const loadMap = () => {
|
|
20
|
+
if (!mapRef.current)
|
|
21
|
+
return;
|
|
22
|
+
if (!window?.google?.maps)
|
|
23
|
+
return;
|
|
24
|
+
const { Map, Marker, InfoWindow } = window.google.maps;
|
|
25
|
+
const coords = latlng.split(",");
|
|
26
|
+
const center = { lat: parseFloat(coords[0]), lng: parseFloat(coords[1]) };
|
|
27
|
+
const map = new Map(mapRef.current, {
|
|
28
|
+
zoom,
|
|
29
|
+
center,
|
|
30
|
+
mapTypeId: mapStyle,
|
|
31
|
+
mapTypeControl: false,
|
|
32
|
+
panControl: false,
|
|
33
|
+
streetViewControl: false,
|
|
34
|
+
draggable: enableTouch,
|
|
35
|
+
scrollwheel: enableScroll,
|
|
36
|
+
zoomControl: true,
|
|
37
|
+
});
|
|
38
|
+
const marker = new Marker({
|
|
39
|
+
draggable: false,
|
|
40
|
+
position: center,
|
|
41
|
+
title: buildTitle(title, tooltip),
|
|
42
|
+
map,
|
|
43
|
+
});
|
|
44
|
+
if (tooltip) {
|
|
45
|
+
new InfoWindow({
|
|
46
|
+
disableAutoPan: true,
|
|
47
|
+
content: tooltip,
|
|
48
|
+
position: center,
|
|
49
|
+
}).open({ anchor: marker, map });
|
|
50
|
+
}
|
|
51
|
+
google.maps.event.addListener(marker, "click", function () {
|
|
52
|
+
window.open(`https://maps.google.com/?z=${zoom}&daddr=${latlng}`);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
loadScript(`https://maps.googleapis.com/maps/api/js?v=3.52.5&key=${apiKey}`, {
|
|
56
|
+
cacheRegex: /maps\.googleapis\.com\/maps\/api\/js\?v=3\.52\.5\&key=/gi,
|
|
57
|
+
}).then(loadMap);
|
|
58
|
+
}, [
|
|
59
|
+
apiKey,
|
|
60
|
+
mapStyle,
|
|
61
|
+
zoom,
|
|
62
|
+
latlng,
|
|
63
|
+
tooltip,
|
|
64
|
+
title,
|
|
65
|
+
enableScroll,
|
|
66
|
+
enableTouch,
|
|
67
|
+
mapRef,
|
|
68
|
+
]);
|
|
69
|
+
return (React.createElement("div", { ...props, className: cj(className, "w-widget w-widget-map"), role: "region", ref: mapRef }));
|
|
70
|
+
}
|
|
71
|
+
export default Map;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { EASING_FUNCTIONS } from "../utils";
|
|
3
|
+
import { LinkProps, ContainerProps } from "./Basic";
|
|
4
|
+
type NavbarConfig = {
|
|
5
|
+
animation: string;
|
|
6
|
+
collapse: string;
|
|
7
|
+
docHeight: boolean;
|
|
8
|
+
duration: number;
|
|
9
|
+
easing: keyof typeof EASING_FUNCTIONS;
|
|
10
|
+
easing2: keyof typeof EASING_FUNCTIONS;
|
|
11
|
+
noScroll: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare const NavbarContext: React.Context<NavbarConfig & {
|
|
14
|
+
animDirect: -1 | 1;
|
|
15
|
+
animOver: boolean;
|
|
16
|
+
getBodyHeight: () => number;
|
|
17
|
+
getOverlayHeight: () => number;
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
menu?: React.MutableRefObject<HTMLElement | null> | undefined;
|
|
20
|
+
root?: React.MutableRefObject<HTMLElement | null> | undefined;
|
|
21
|
+
toggleOpen: () => void;
|
|
22
|
+
navbarMounted: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
type NavbarChildrenType = NavbarContainerProps | NavbarBrandProps | NavbarMenuProps | NavbarButtonProps;
|
|
25
|
+
type NavbarProps = {
|
|
26
|
+
tag: React.ElementType;
|
|
27
|
+
config: NavbarConfig;
|
|
28
|
+
className?: string;
|
|
29
|
+
children?: React.ReactElement<NavbarChildrenType>[] | React.ReactElement<NavbarChildrenType>;
|
|
30
|
+
};
|
|
31
|
+
export declare function NavbarWrapper(props: NavbarProps): JSX.Element;
|
|
32
|
+
type NavbarContainerProps = ContainerProps & {
|
|
33
|
+
toggleOpen: () => void;
|
|
34
|
+
isOpen: boolean;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
};
|
|
37
|
+
export declare function NavbarContainer({ children, ...props }: NavbarContainerProps): JSX.Element;
|
|
38
|
+
type NavbarBrandProps = LinkProps;
|
|
39
|
+
export declare function NavbarBrand({ className, ...props }: NavbarBrandProps): JSX.Element;
|
|
40
|
+
type NavbarMenuProps = React.PropsWithChildren<{
|
|
41
|
+
tag?: React.ElementType;
|
|
42
|
+
className?: string;
|
|
43
|
+
isOpen?: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function NavbarMenu({ tag, className, ...props }: NavbarMenuProps): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|
|
46
|
+
type NavbarLinkProps = LinkProps;
|
|
47
|
+
export declare function NavbarLink({ className, ...props }: NavbarLinkProps): JSX.Element;
|
|
48
|
+
type NavbarButtonProps = React.PropsWithChildren<{
|
|
49
|
+
tag?: React.ElementType;
|
|
50
|
+
className?: string;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function NavbarButton({ tag, className, ...props }: NavbarButtonProps): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { EASING_FUNCTIONS, cj, debounce, isServer, useLayoutEffect, useResizeObserver, } from "../utils";
|
|
3
|
+
import { Link, Container } from "./Basic";
|
|
4
|
+
export const NavbarContext = React.createContext({
|
|
5
|
+
animDirect: 1,
|
|
6
|
+
animOver: false,
|
|
7
|
+
animation: "animation",
|
|
8
|
+
collapse: "medium",
|
|
9
|
+
docHeight: false,
|
|
10
|
+
duration: 400,
|
|
11
|
+
easing2: "ease",
|
|
12
|
+
easing: "ease",
|
|
13
|
+
getBodyHeight: () => 0,
|
|
14
|
+
getOverlayHeight: () => 0,
|
|
15
|
+
isOpen: false,
|
|
16
|
+
noScroll: false,
|
|
17
|
+
toggleOpen: () => { },
|
|
18
|
+
navbarMounted: false,
|
|
19
|
+
});
|
|
20
|
+
function getAnimationKeyframes({ axis = "Y", start, end, }) {
|
|
21
|
+
const t = `translate${axis}`;
|
|
22
|
+
return [{ transform: `${t}(${start}px)` }, { transform: `${t}(${end}px)` }];
|
|
23
|
+
}
|
|
24
|
+
export function NavbarWrapper(props) {
|
|
25
|
+
const { animation, docHeight, easing, easing2, duration, noScroll } = props.config;
|
|
26
|
+
const root = React.useRef(null);
|
|
27
|
+
const menu = React.useRef(null);
|
|
28
|
+
const animOver = /^over/.test(animation);
|
|
29
|
+
const animDirect = /left$/.test(animation) ? -1 : 1;
|
|
30
|
+
const getBodyHeight = React.useCallback(() => {
|
|
31
|
+
if (isServer)
|
|
32
|
+
return 0;
|
|
33
|
+
return docHeight
|
|
34
|
+
? document.documentElement.scrollHeight
|
|
35
|
+
: document.body.scrollHeight;
|
|
36
|
+
}, [docHeight]);
|
|
37
|
+
const getOverlayHeight = React.useCallback(() => {
|
|
38
|
+
if (isServer || !root.current)
|
|
39
|
+
return 0;
|
|
40
|
+
let h = getBodyHeight();
|
|
41
|
+
const style = getComputedStyle(root.current);
|
|
42
|
+
if (!animOver && style.position !== "fixed") {
|
|
43
|
+
h -= root.current.offsetHeight;
|
|
44
|
+
}
|
|
45
|
+
return h;
|
|
46
|
+
}, [animOver, getBodyHeight]);
|
|
47
|
+
const getOffsetHeight = React.useCallback(() => {
|
|
48
|
+
if (!root.current || !menu.current)
|
|
49
|
+
return 0;
|
|
50
|
+
return root.current.offsetHeight + menu.current.offsetHeight;
|
|
51
|
+
}, []);
|
|
52
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
53
|
+
const toggleOpen = debounce(() => {
|
|
54
|
+
if (!menu.current)
|
|
55
|
+
return;
|
|
56
|
+
// menu is open and should be closed
|
|
57
|
+
if (isOpen) {
|
|
58
|
+
const keyframes = animOver
|
|
59
|
+
? getAnimationKeyframes({
|
|
60
|
+
axis: "X",
|
|
61
|
+
start: 0,
|
|
62
|
+
end: animDirect * menu.current.offsetWidth,
|
|
63
|
+
})
|
|
64
|
+
: getAnimationKeyframes({ start: 0, end: -getOffsetHeight() });
|
|
65
|
+
const anim = menu.current.animate(keyframes, {
|
|
66
|
+
easing: EASING_FUNCTIONS[easing2] ?? "ease",
|
|
67
|
+
duration,
|
|
68
|
+
fill: "forwards",
|
|
69
|
+
});
|
|
70
|
+
anim.onfinish = () => {
|
|
71
|
+
setIsOpen(!isOpen);
|
|
72
|
+
};
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setIsOpen(!isOpen);
|
|
76
|
+
});
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
if (!menu.current)
|
|
79
|
+
return;
|
|
80
|
+
// menu is closed and will open, but the animation only runs when isOpen is true
|
|
81
|
+
if (isOpen) {
|
|
82
|
+
const keyframes = animOver
|
|
83
|
+
? getAnimationKeyframes({
|
|
84
|
+
axis: "X",
|
|
85
|
+
start: animDirect * menu.current.offsetWidth,
|
|
86
|
+
end: 0,
|
|
87
|
+
})
|
|
88
|
+
: getAnimationKeyframes({ start: -getOffsetHeight(), end: 0 });
|
|
89
|
+
menu.current.animate(keyframes, {
|
|
90
|
+
easing: EASING_FUNCTIONS[easing] ?? "ease",
|
|
91
|
+
duration,
|
|
92
|
+
fill: "forwards",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}, [
|
|
96
|
+
animDirect,
|
|
97
|
+
animOver,
|
|
98
|
+
duration,
|
|
99
|
+
easing,
|
|
100
|
+
getBodyHeight,
|
|
101
|
+
getOffsetHeight,
|
|
102
|
+
isOpen,
|
|
103
|
+
]);
|
|
104
|
+
// if the menu is opened and noScroll === false prevent scrolling
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
if (isOpen && noScroll) {
|
|
107
|
+
document.body.style.overflowY = "hidden";
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
document.body.style.overflowY = "";
|
|
111
|
+
}
|
|
112
|
+
return () => {
|
|
113
|
+
document.body.style.overflowY = "";
|
|
114
|
+
};
|
|
115
|
+
}, [isOpen, noScroll]);
|
|
116
|
+
// Closes menu when the window is resized
|
|
117
|
+
const closeOnResize = React.useCallback(() => setIsOpen(false), [setIsOpen]);
|
|
118
|
+
useResizeObserver(root, closeOnResize);
|
|
119
|
+
return (React.createElement(NavbarContext.Provider, { value: {
|
|
120
|
+
...props.config,
|
|
121
|
+
root,
|
|
122
|
+
menu,
|
|
123
|
+
animOver,
|
|
124
|
+
animDirect,
|
|
125
|
+
getBodyHeight,
|
|
126
|
+
getOverlayHeight,
|
|
127
|
+
isOpen,
|
|
128
|
+
toggleOpen,
|
|
129
|
+
navbarMounted: true,
|
|
130
|
+
} },
|
|
131
|
+
React.createElement(Navbar, { ...props })));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Navbar menu gets appended to the overlay when it's open.
|
|
135
|
+
* This function extracts the child menu when that's the case.
|
|
136
|
+
* */
|
|
137
|
+
const maybeExtractChildMenu = (children, isOpen) => {
|
|
138
|
+
if (!isOpen)
|
|
139
|
+
return { childMenu: null, rest: children };
|
|
140
|
+
const childrenArray = React.Children.toArray(children);
|
|
141
|
+
const { childMenu, rest } = childrenArray.reduce((acc, child) => {
|
|
142
|
+
if (child.type === NavbarMenu) {
|
|
143
|
+
acc.childMenu = child;
|
|
144
|
+
return acc;
|
|
145
|
+
}
|
|
146
|
+
if (child.type === NavbarContainer) {
|
|
147
|
+
const { children: containerChildren, ...containerProps } = child.props;
|
|
148
|
+
const { childMenu, rest } = maybeExtractChildMenu(containerChildren, isOpen);
|
|
149
|
+
acc.childMenu = childMenu;
|
|
150
|
+
acc.rest.push(React.createElement(NavbarContainer, { ...containerProps }, rest));
|
|
151
|
+
return acc;
|
|
152
|
+
}
|
|
153
|
+
acc.rest.push(child);
|
|
154
|
+
return acc;
|
|
155
|
+
}, { childMenu: null, rest: [] });
|
|
156
|
+
return {
|
|
157
|
+
childMenu,
|
|
158
|
+
rest: React.createElement(React.Fragment, null, rest.map((e, i) => React.cloneElement(e, { key: i }))),
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
function Navbar({ tag = "div", className = "", children, config, ...props }) {
|
|
162
|
+
const { root, isOpen } = React.useContext(NavbarContext);
|
|
163
|
+
const { childMenu, rest } = React.useMemo(() => maybeExtractChildMenu(children, isOpen), [children, isOpen]);
|
|
164
|
+
return React.createElement(tag, {
|
|
165
|
+
...props,
|
|
166
|
+
className: cj(className, "w-nav"),
|
|
167
|
+
"data-collapse": config.collapse,
|
|
168
|
+
"data-animation": config.animation,
|
|
169
|
+
ref: root,
|
|
170
|
+
}, React.createElement(React.Fragment, null,
|
|
171
|
+
rest,
|
|
172
|
+
React.createElement(NavbarOverlay, null, childMenu)));
|
|
173
|
+
}
|
|
174
|
+
function NavbarOverlay({ children }) {
|
|
175
|
+
const { isOpen, getOverlayHeight, toggleOpen } = React.useContext(NavbarContext);
|
|
176
|
+
const overlayToggleOpen = React.useCallback((e) => {
|
|
177
|
+
// prevent link clicks to close the overlay
|
|
178
|
+
if (e.target === e.currentTarget) {
|
|
179
|
+
toggleOpen();
|
|
180
|
+
}
|
|
181
|
+
}, [toggleOpen]);
|
|
182
|
+
return (React.createElement("div", { className: "w-nav-overlay", id: "w-nav-overlay", style: {
|
|
183
|
+
display: isOpen ? "block" : "none",
|
|
184
|
+
height: getOverlayHeight(),
|
|
185
|
+
width: isOpen ? "100vw" : 0,
|
|
186
|
+
}, onClick: overlayToggleOpen }, children));
|
|
187
|
+
}
|
|
188
|
+
export function NavbarContainer({ children, ...props }) {
|
|
189
|
+
const ref = React.useRef(null);
|
|
190
|
+
const { isOpen } = React.useContext(NavbarContext);
|
|
191
|
+
const updateLinkStyles = React.useCallback((entry) => {
|
|
192
|
+
const { maxWidth: containerMaxWidth } = getComputedStyle(entry.target);
|
|
193
|
+
document
|
|
194
|
+
.querySelectorAll(".w-nav-menu>.w-dropdown,.w-nav-menu>.w-nav-link")
|
|
195
|
+
.forEach((node) => {
|
|
196
|
+
if (!(node instanceof HTMLElement))
|
|
197
|
+
return;
|
|
198
|
+
if (!isOpen) {
|
|
199
|
+
node.style.maxWidth = "";
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const { maxWidth } = getComputedStyle(node);
|
|
203
|
+
node.style.maxWidth =
|
|
204
|
+
!maxWidth || maxWidth === "none" ? containerMaxWidth : "";
|
|
205
|
+
});
|
|
206
|
+
}, [isOpen]);
|
|
207
|
+
useResizeObserver(ref, updateLinkStyles);
|
|
208
|
+
return (React.createElement(Container, { ...props, ref: ref }, children));
|
|
209
|
+
}
|
|
210
|
+
export function NavbarBrand({ className = "", ...props }) {
|
|
211
|
+
return React.createElement(Link, { ...props, className: cj(className, "w-nav-brand") });
|
|
212
|
+
}
|
|
213
|
+
export function NavbarMenu({ tag = "nav", className = "", ...props }) {
|
|
214
|
+
const { getBodyHeight, animOver, isOpen, menu } = React.useContext(NavbarContext);
|
|
215
|
+
return React.createElement(tag, {
|
|
216
|
+
...props,
|
|
217
|
+
className: cj(className, "w-nav-menu"),
|
|
218
|
+
...(isOpen ? { "data-nav-menu-open": "" } : {}),
|
|
219
|
+
style: animOver ? { height: getBodyHeight() } : {},
|
|
220
|
+
ref: menu,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function NavbarLink({ className = "", ...props }) {
|
|
224
|
+
const { isOpen } = React.useContext(NavbarContext);
|
|
225
|
+
return (React.createElement(Link, { ...props, className: cj(className, "w-nav-link", isOpen && "w--nav-link-open") }));
|
|
226
|
+
}
|
|
227
|
+
export function NavbarButton({ tag = "div", className = "", ...props }) {
|
|
228
|
+
const { isOpen, toggleOpen } = React.useContext(NavbarContext);
|
|
229
|
+
return React.createElement(tag, {
|
|
230
|
+
...props,
|
|
231
|
+
"aria-label": "menu",
|
|
232
|
+
"aria-expanded": isOpen ? "true" : "false",
|
|
233
|
+
"aria-haspopup": "menu",
|
|
234
|
+
"aria-controls": "w-nav-overlay",
|
|
235
|
+
role: "button",
|
|
236
|
+
tabIndex: 0,
|
|
237
|
+
className: cj(className, "w-nav-button", isOpen && "w--open"),
|
|
238
|
+
onClick: toggleOpen,
|
|
239
|
+
});
|
|
240
|
+
}
|