-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPinturaEditor.js
194 lines (167 loc) · 6.73 KB
/
PinturaEditor.js
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
import { WebView } from 'react-native-webview';
import { View, Platform } from 'react-native';
import React, { forwardRef, useRef, useState, useEffect } from 'react';
import PinturaProxy from './bin/pintura.html';
const upperCaseFirstLetter = (str) => str.charAt(0).toUpperCase() + str.slice(1);
// @deprecated
// Converts a local file to a DataURL so there's no rights issue when loading the image
export const localFileToDataURL = (url) =>
new Promise((resolve, reject) => {
// url to blob
fetch(url)
.then((res) => res.blob())
.then((blob) => {
// if mimetype missing fix
if (!blob.type) {
const [path, _] = url.split('?');
const ext = path.split('.').pop();
blob = new Blob([blob], {
type: 'image/' + ext,
lastModified: Date.now(),
});
}
// convert to dataURL
const fr = new FileReader();
fr.onload = () => resolve(fr.result);
fr.onerror = () => reject(fr.error);
fr.readAsDataURL(blob);
})
.catch(reject);
});
// This allows passing functions to webview
const getFunctionParts = (fn) => {
const fnStr = fn.toString();
const params = fnStr.match(/^(?:function [a-z]*)?\(([ ,a-z_0-9]*)/i);
const body = fnStr.match(/^(?:.*?=>|function.*?{)(.+)/is);
return {
body: body ? body[1].replace(/}$/, '').trim() : '',
params: params ? params[1] : '',
};
};
// This replaces undefined values and in outgoing messages so they're not lost
const stringifyMessage = (obj) =>
JSON.stringify(obj, (k, v) => {
if (v === undefined) return '__UNDEFINED__';
if (typeof v === 'function') {
const { params, body } = getFunctionParts(v);
if (!body && !params) return undefined;
return '__FUNCTION__' + params + '____' + body;
}
return v;
});
// This restores undefined values in incoming messages
const deepReplaceValues = (obj, replacer) => {
let out = Array.isArray(obj) ? [...obj] : { ...obj };
Object.entries(out).forEach(([key, value]) => {
if (Array.isArray(value) || typeof value === 'object') {
out[key] = deepReplaceValues(value, replacer);
} else {
out[key] = replacer(key, value);
}
});
return out;
};
const replaceValues = (key, value) => (value === '__UNDEFINED__' ? undefined : value);
const parseMessage = (str) => deepReplaceValues(JSON.parse(str), replaceValues);
/* eslint-disable @typescript-eslint/no-var-requires */
const Editor = forwardRef((props, ref) => {
const { style, styleRules, ...options } = props;
const [source, setSource] = useState({});
const webViewRef = useRef(null);
// options map used to filter out options that are set multiple times
const [propMap] = useState(new Map());
// this sets up proxy so we can call functions on the editor instance
useEffect(() => {
const handler = {
get: (target, prop) => {
if (prop === 'history') return new Proxy({ group: 'history' }, handler); // eslint-disable-line no-undef
return (...args) => {
const name = [target.group, prop].filter(Boolean).join('.');
webViewRef.current.postMessage(
stringifyMessage({
editorFunction: [name, ...args],
})
);
};
},
};
ref.current.editor = new Proxy({}, handler); // eslint-disable-line no-undef
}, [webViewRef, ref]);
// this passes options to the editor
useEffect(() => {
const editorOptions = {};
for (key of Object.keys(options)) {
const currentValue = propMap.get(key);
const newValue = options[key];
// skip new value!
if (newValue === currentValue) {
continue;
}
// set new value
editorOptions[key] = newValue;
// remember this value so we can compare in next prop update
propMap.set(key, newValue);
}
webViewRef.current.postMessage(stringifyMessage({ editorOptions }));
}, [webViewRef, options]);
// clear propmap on unmount
useEffect(() => {
return () => {
propMap.clear();
};
}, []);
// this passes style rules to the editor
useEffect(() => {
webViewRef.current.postMessage(stringifyMessage({ editorStyleRules: styleRules }));
}, [webViewRef, styleRules]);
// load editor template
useEffect(() => {
if (Platform.OS === 'android') {
setSource({ uri: 'file:///android_asset/pintura.html' });
} else {
setSource(PinturaProxy);
}
}, []);
return (
<View ref={ref} style={{ ...style, backgroundColor: 'transparent' }}>
<WebView
ref={webViewRef}
style={{ width: '100%', height: '100%', backgroundColor: 'transparent' }}
javaScriptEnabled={true}
scrollEnabled={false}
domStorageEnabled={true}
allowFileAccess={true}
allowFileAccessFromFileURLs={true}
allowUniversalAccessFromFileURLs={true}
allowsLinkPreview={false}
allowsInlineMediaPlayback={true}
automaticallyAdjustContentInsets={false}
originWhitelist={['*']}
textZoom={100}
source={source}
onMessage={async (event) => {
// message from WebView
const { type, detail } = parseMessage(event.nativeEvent.data);
// webview ready, lets send over first batch of options
if (type === 'webviewloaded') {
return webViewRef.current.postMessage(
stringifyMessage({
editorStyleRules: styleRules,
editorOptions: options,
})
);
}
// if is log
if (type === 'log') {
return console.log(detail.map((d) => JSON.stringify(d)).join(', ')); // eslint-disable-line no-undef
}
// get handler
const handler = options[`on${upperCaseFirstLetter(type)}`];
// call handler
handler && handler(detail);
}}
/>
</View>
);
});
export default Editor;