forked from remix-run/react-router
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dom.ts
249 lines (216 loc) · 6.86 KB
/
dom.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import type { FormEncType, FormMethod } from "@remix-run/router";
import type { RelativeRoutingType } from "react-router";
export const defaultMethod = "get";
const defaultEncType = "application/x-www-form-urlencoded";
export function isHtmlElement(object: any): object is HTMLElement {
return object != null && typeof object.tagName === "string";
}
export function isButtonElement(object: any): object is HTMLButtonElement {
return isHtmlElement(object) && object.tagName.toLowerCase() === "button";
}
export function isFormElement(object: any): object is HTMLFormElement {
return isHtmlElement(object) && object.tagName.toLowerCase() === "form";
}
export function isInputElement(object: any): object is HTMLInputElement {
return isHtmlElement(object) && object.tagName.toLowerCase() === "input";
}
type LimitedMouseEvent = Pick<
MouseEvent,
"button" | "metaKey" | "altKey" | "ctrlKey" | "shiftKey"
>;
function isModifiedEvent(event: LimitedMouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
export function shouldProcessLinkClick(
event: LimitedMouseEvent,
target?: string
) {
return (
event.button === 0 && // Ignore everything but left clicks
(!target || target === "_self") && // Let browser handle "target=_blank" etc.
!isModifiedEvent(event) // Ignore clicks with modifier keys
);
}
export type ParamKeyValuePair = [string, string];
export type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams;
/**
* Creates a URLSearchParams object using the given initializer.
*
* This is identical to `new URLSearchParams(init)` except it also
* supports arrays as values in the object form of the initializer
* instead of just strings. This is convenient when you need multiple
* values for a given key, but don't want to use an array initializer.
*
* For example, instead of:
*
* let searchParams = new URLSearchParams([
* ['sort', 'name'],
* ['sort', 'price']
* ]);
*
* you can do:
*
* let searchParams = createSearchParams({
* sort: ['name', 'price']
* });
*/
export function createSearchParams(
init: URLSearchParamsInit = ""
): URLSearchParams {
return new URLSearchParams(
typeof init === "string" ||
Array.isArray(init) ||
init instanceof URLSearchParams
? init
: Object.keys(init).reduce((memo, key) => {
let value = init[key];
return memo.concat(
Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
);
}, [] as ParamKeyValuePair[])
);
}
export function getSearchParamsForLocation(
locationSearch: string,
defaultSearchParams: URLSearchParams
) {
let searchParams = createSearchParams(locationSearch);
for (let key of defaultSearchParams.keys()) {
if (!searchParams.has(key)) {
defaultSearchParams.getAll(key).forEach((value) => {
searchParams.append(key, value);
});
}
}
return searchParams;
}
export interface SubmitOptions {
/**
* The HTTP method used to submit the form. Overrides `<form method>`.
* Defaults to "GET".
*/
method?: FormMethod;
/**
* The action URL path used to submit the form. Overrides `<form action>`.
* Defaults to the path of the current route.
*
* Note: It is assumed the path is already resolved. If you need to resolve a
* relative path, use `useFormAction`.
*/
action?: string;
/**
* The action URL used to submit the form. Overrides `<form encType>`.
* Defaults to "application/x-www-form-urlencoded".
*/
encType?: FormEncType;
/**
* Set `true` to replace the current entry in the browser's history stack
* instead of creating a new one (i.e. stay on "the same page"). Defaults
* to `false`.
*/
replace?: boolean;
/**
* Determines whether the form action is relative to the route hierarchy or
* the pathname. Use this if you want to opt out of navigating the route
* hierarchy and want to instead route based on /-delimited URL segments
*/
relative?: RelativeRoutingType;
}
export function getFormSubmissionInfo(
target:
| HTMLFormElement
| HTMLButtonElement
| HTMLInputElement
| FormData
| URLSearchParams
| { [name: string]: string }
| null,
defaultAction: string,
options: SubmitOptions
): {
url: URL;
method: string;
encType: string;
formData: FormData;
} {
let method: string;
let action: string;
let encType: string;
let formData: FormData;
if (isFormElement(target)) {
let submissionTrigger: HTMLButtonElement | HTMLInputElement = (
options as any
).submissionTrigger;
method = options.method || target.getAttribute("method") || defaultMethod;
action = options.action || target.getAttribute("action") || defaultAction;
encType =
options.encType || target.getAttribute("enctype") || defaultEncType;
formData = new FormData(target);
if (submissionTrigger && submissionTrigger.name) {
formData.append(submissionTrigger.name, submissionTrigger.value);
}
} else if (
isButtonElement(target) ||
(isInputElement(target) &&
(target.type === "submit" || target.type === "image"))
) {
let form = target.form;
if (form == null) {
throw new Error(
`Cannot submit a <button> or <input type="submit"> without a <form>`
);
}
// <button>/<input type="submit"> may override attributes of <form>
method =
options.method ||
target.getAttribute("formmethod") ||
form.getAttribute("method") ||
defaultMethod;
action =
options.action ||
target.getAttribute("formaction") ||
form.getAttribute("action") ||
defaultAction;
encType =
options.encType ||
target.getAttribute("formenctype") ||
form.getAttribute("enctype") ||
defaultEncType;
formData = new FormData(form);
// Include name + value from a <button>, appending in case the button name
// matches an existing input name
if (target.name) {
formData.append(target.name, target.value);
}
} else if (isHtmlElement(target)) {
throw new Error(
`Cannot submit element that is not <form>, <button>, or ` +
`<input type="submit|image">`
);
} else {
method = options.method || defaultMethod;
action = options.action || defaultAction;
encType = options.encType || defaultEncType;
if (target instanceof FormData) {
formData = target;
} else {
formData = new FormData();
if (target instanceof URLSearchParams) {
for (let [name, value] of target) {
formData.append(name, value);
}
} else if (target != null) {
for (let name of Object.keys(target)) {
formData.append(name, target[name]);
}
}
}
}
let { protocol, host } = window.location;
let url = new URL(action, `${protocol}//${host}`);
return { url, method, encType, formData };
}