-
Notifications
You must be signed in to change notification settings - Fork 74
/
Copy pathindex.ts
259 lines (233 loc) · 7.02 KB
/
index.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
250
251
252
253
254
255
256
257
258
259
import * as Xml from 'hyperview/src/services/xml';
import { DEFAULT_PRESS_OPACITY, HV_TIMEOUT_ID_ATTR } from './types';
import type {
DOMString,
HvComponentOptions,
StyleSheet,
StyleSheets,
} from 'hyperview/src/types';
import { NODE_TYPE } from 'hyperview/src/types';
import { Platform } from 'react-native';
/**
* This file is currently a dumping place for every functions used accross
* various Hyperview components.
*/
export const createStyleProp = (
element: Element,
stylesheets: StyleSheets,
options: HvComponentOptions,
): Array<StyleSheet> => {
const styleAttr: string = options.styleAttr || 'style';
if (!element.getAttribute(styleAttr)) {
return [];
}
const styleValue: string = element.getAttribute(styleAttr) || '';
const styleIds: Array<string> = Xml.splitAttributeList(styleValue);
let styleRules: Array<StyleSheet> = styleIds.map(
styleId => stylesheets.regular[styleId],
);
if (options.pressed) {
let pressedRules = styleIds
.map(styleId => stylesheets.pressed[styleId])
.filter(Boolean);
if (pressedRules.length === 0) {
pressedRules = [{ opacity: DEFAULT_PRESS_OPACITY }];
}
styleRules = styleRules.concat(pressedRules);
}
if (options.focused) {
const focusedRules = styleIds
.map(s => stylesheets.focused[s])
.filter(Boolean);
styleRules = styleRules.concat(focusedRules);
}
if (options.selected) {
const selectedRules = styleIds
.map(s => stylesheets.selected[s])
.filter(Boolean);
styleRules = styleRules.concat(selectedRules);
}
if (options.pressedSelected) {
const pressedSelectedRules = styleIds
.map(s => stylesheets.pressedSelected[s])
.filter(Boolean);
styleRules = styleRules.concat(pressedSelectedRules);
}
return styleRules;
};
/**
* Sets the element's id attribute as a test id and accessibility label
* (for testing automation purposes).
*/
export const createTestProps = (
element: Element,
): {
testID?: string;
accessibilityLabel?: string;
} => {
const testProps = {};
const id: DOMString | null | undefined = element.getAttribute('id');
if (!id) {
return testProps;
}
if (Platform.OS === 'ios') {
return { testID: id };
}
return { accessibilityLabel: id };
};
export const createProps = (
element: Element,
stylesheets: StyleSheets,
options: HvComponentOptions,
) => {
const numericRules = ['numberOfLines'];
const booleanRules = ['multiline', 'selectable', 'adjustsFontSizeToFit'];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props: Record<string, any> = {};
if (!element.attributes) {
return props;
}
for (let i = 0; i < element.attributes.length; i += 1) {
const attr = element.attributes.item(i);
if (attr) {
if (numericRules.indexOf(attr.name) >= 0) {
const intValue = parseInt(attr.value, 10);
props[attr.name] = intValue || 0;
} else if (booleanRules.indexOf(attr.name) >= 0) {
props[attr.name] = attr.value === 'true';
} else {
props[attr.name] = attr.value;
}
}
}
props.style = createStyleProp(element, stylesheets, options);
const testProps = createTestProps(element);
return { ...props, ...testProps };
};
export const later = (delayMs: number): Promise<void> =>
new Promise((resolve: (result?: Promise<never>) => void) =>
setTimeout(resolve, delayMs),
);
/**
* Clones the element and moves all children from the original element
* to the clone. The returned element will be a new object, but all of the child
* nodes will be existing objects.
*/
export const shallowClone = (element: Element | Document): Element => {
const newElement: Element = element.cloneNode(false) as Element;
let childNode: Node | null | undefined = element.firstChild;
while (childNode) {
const nextChild: Node | null | undefined = childNode.nextSibling;
newElement.appendChild(childNode);
childNode = nextChild;
}
return newElement;
};
/**
* Clones all elements from the given element up to the root of the DOM.
* Returns the new root object. Essentially, this produces a new DOM object
* that re-uses as many existing nodes as possible.
*/
export const shallowCloneToRoot = (element: Element | Document): Document => {
const elementClone: unknown = shallowClone(element);
if (element.nodeType === 9) {
return elementClone as Document;
}
// Need to check parentNode to satisfy Flow
const { parentNode } = element;
if (!parentNode) {
return elementClone as Document;
}
parentNode.replaceChild(elementClone as Document, element);
return shallowCloneToRoot(parentNode as Document);
};
/**
* Taken from internals of xmldom library. Allows us to run a callback on a node tree.
* @param callback return true for continue,false for break
* @return boolean true: break visit;
*/
const visitNode = (node: Node, callback: (n: Node) => boolean): boolean => {
if (callback(node)) {
return true;
}
let childNode: Node | null | undefined = node.firstChild;
while (childNode) {
if (visitNode(childNode, callback)) {
return true;
}
childNode = childNode.nextSibling;
}
return false;
};
/**
* Returns the element with the given timeout id.
* Note this is different from the element's regular id, this is
* used for tracking delayed behaviors.
*/
export const getElementByTimeoutId = (
doc: Document,
id: string,
): Element | null | undefined => {
let foundElement: Element | null | undefined = null;
const callback = (node: Node): boolean => {
if (node.nodeType === NODE_TYPE.ELEMENT_NODE) {
const element = node as Element;
if (element.getAttribute(HV_TIMEOUT_ID_ATTR) === id) {
foundElement = element;
return true;
}
}
return false;
};
visitNode(doc, callback);
return foundElement;
};
/**
* Sets a timeout id on the given element.
*/
export const setTimeoutId = (element: Element, id: string) => {
element.setAttribute(HV_TIMEOUT_ID_ATTR, id);
};
/**
* Removed the timeout id from the given element.
*/
export const removeTimeoutId = (element: Element) => {
element.removeAttribute(HV_TIMEOUT_ID_ATTR);
};
/**
* Searches the parent chain from the given element until it finds an
* element with the given tag name. If no ancestor with the tagName is found,
* returns null.
*/
export const getAncestorByTagName = (
element: Element,
tagName: string,
): Element | null | undefined => {
let { parentNode } = element;
if (!parentNode) {
return null;
}
while ((parentNode as Element).tagName !== tagName) {
({ parentNode } = parentNode);
if (!parentNode) {
return null;
}
}
return parentNode as Element;
};
export const getNameValueFormInputValues = (
element: Element,
): Array<[string, string]> => {
const name = element.getAttribute('name');
if (name) {
return [[name, element.getAttribute('value') || '']];
}
return [];
};
export const encodeXml = (xml: string): string =>
xml
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');