@saltcorn/mobile-app 0.9.5-beta.8 → 0.9.5
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/config.xml +5 -0
- package/package.json +3 -3
- package/www/index.html +9 -7
- package/www/js/mocks/request.js +1 -4
- package/www/js/routes/auth.js +34 -5
- package/www/js/routes/common.js +9 -2
- package/www/js/routes/page.js +11 -5
- package/www/js/routes/view.js +12 -6
- package/www/js/utils/global_utils.js +21 -6
- package/www/js/utils/iframe_view_utils.js +53 -14
package/config.xml
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
</config-file>
|
|
13
13
|
<preference name="Scheme" value="http"/>
|
|
14
14
|
<preference name="MixedContentMode" value="2"/>
|
|
15
|
+
<preference name="minSdkVersion" value="33"/>
|
|
16
|
+
<preference name="targetSdkVersion" value="33"/>
|
|
17
|
+
</platform>
|
|
18
|
+
<platform name="ios">
|
|
19
|
+
<preference name="deployment-target" value="12.0"/>
|
|
15
20
|
</platform>
|
|
16
21
|
<content src="index.html"/>
|
|
17
22
|
<access origin="*"/>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/mobile-app",
|
|
3
3
|
"displayName": "Saltcorn mobile app",
|
|
4
|
-
"version": "0.9.5
|
|
4
|
+
"version": "0.9.5",
|
|
5
5
|
"description": "Apache Cordova application with @saltcorn/markup",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"ansi-regex": "4.1.1"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"cordova": "^
|
|
23
|
+
"cordova": "^12.0.0",
|
|
24
24
|
"cordova-sqlite-ext": "^6.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"cordova-android": "^
|
|
27
|
+
"cordova-android": "^12.0.1"
|
|
28
28
|
}
|
|
29
29
|
}
|
package/www/index.html
CHANGED
|
@@ -141,12 +141,6 @@
|
|
|
141
141
|
}
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
-
const setVersionTtag = () => {
|
|
145
|
-
const config = saltcorn.data.state.getState().mobileConfig;
|
|
146
|
-
let iframe = document.getElementById("content-iframe");
|
|
147
|
-
iframe.contentWindow._sc_version_tag = config.version_tag;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
144
|
const initJwt = async () => {
|
|
151
145
|
if (!(await saltcorn.data.db.tableExists("jwt_table"))) {
|
|
152
146
|
await createJwtTable();
|
|
@@ -228,12 +222,15 @@
|
|
|
228
222
|
if (mobileConfig.user_id) await gotoEntryView();
|
|
229
223
|
else {
|
|
230
224
|
const decodedJwt = jwt_decode(mobileConfig.jwt);
|
|
225
|
+
mobileConfig.user = decodedJwt.user;
|
|
226
|
+
// TODO remove these, use 'user' everywhere
|
|
231
227
|
mobileConfig.role_id = decodedJwt.user.role_id
|
|
232
228
|
? decodedJwt.user.role_id
|
|
233
229
|
: 100;
|
|
234
230
|
mobileConfig.user_id = decodedJwt.user.id;
|
|
235
231
|
mobileConfig.user_name = decodedJwt.user.email;
|
|
236
232
|
mobileConfig.language = decodedJwt.user.language;
|
|
233
|
+
|
|
237
234
|
mobileConfig.isPublicUser = false;
|
|
238
235
|
}
|
|
239
236
|
addRoute({ route: entryPoint, query: undefined });
|
|
@@ -342,7 +339,6 @@
|
|
|
342
339
|
);
|
|
343
340
|
await state.setConfig("base_url", config.server_path);
|
|
344
341
|
await initRoutes();
|
|
345
|
-
setVersionTtag();
|
|
346
342
|
const entryPoint = config.entry_point;
|
|
347
343
|
await initI18Next();
|
|
348
344
|
await readSiteLogo(state);
|
|
@@ -364,12 +360,15 @@
|
|
|
364
360
|
if ((networkDisabled && jwt) || (await checkJWT(jwt))) {
|
|
365
361
|
const mobileConfig = state.mobileConfig;
|
|
366
362
|
const decodedJwt = jwt_decode(mobileConfig.jwt);
|
|
363
|
+
mobileConfig.user = decodedJwt.user;
|
|
364
|
+
// TODO remove these, use 'user' everywhere
|
|
367
365
|
mobileConfig.role_id = decodedJwt.user.role_id
|
|
368
366
|
? decodedJwt.user.role_id
|
|
369
367
|
: 100;
|
|
370
368
|
mobileConfig.user_id = decodedJwt.user.id;
|
|
371
369
|
mobileConfig.user_name = decodedJwt.user.email;
|
|
372
370
|
mobileConfig.language = decodedJwt.user.language;
|
|
371
|
+
|
|
373
372
|
mobileConfig.isPublicUser = false;
|
|
374
373
|
await i18next.changeLanguage(mobileConfig.language);
|
|
375
374
|
if (mobileConfig.allowOfflineMode) {
|
|
@@ -419,9 +418,12 @@
|
|
|
419
418
|
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
420
419
|
} else if (isPublicJwt(jwt)) {
|
|
421
420
|
const config = state.mobileConfig;
|
|
421
|
+
config.user = { role_id: 100, user_name: "public", language: "en" };
|
|
422
|
+
// TODO remove these, use 'user' everywhere
|
|
422
423
|
config.role_id = 100;
|
|
423
424
|
config.user_name = "public";
|
|
424
425
|
config.language = "en";
|
|
426
|
+
|
|
425
427
|
config.isPublicUser = true;
|
|
426
428
|
i18next.changeLanguage(config.language);
|
|
427
429
|
addRoute({ route: entryPoint, query: undefined });
|
package/www/js/mocks/request.js
CHANGED
|
@@ -35,10 +35,7 @@ function MobileRequest({
|
|
|
35
35
|
const mobileCfg = saltcorn.data.state.getState().mobileConfig;
|
|
36
36
|
return mobileCfg?.language ? mobileCfg.language : "en";
|
|
37
37
|
},
|
|
38
|
-
user:
|
|
39
|
-
id: userId,
|
|
40
|
-
role_id: roleId,
|
|
41
|
-
},
|
|
38
|
+
user: cfg.user,
|
|
42
39
|
flash: (type, msg) => {
|
|
43
40
|
flashMessages.push({ type, msg });
|
|
44
41
|
},
|
package/www/js/routes/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*global sbAdmin2Layout, apiCall, removeJwt, saltcorn, clearHistory*/
|
|
1
|
+
/*global sbAdmin2Layout, apiCall, removeJwt, saltcorn, clearHistory, MobileRequest, MobileResponse, getHeaders*/
|
|
2
2
|
|
|
3
3
|
const prepareAuthForm = () => {
|
|
4
4
|
return new saltcorn.data.models.Form({
|
|
@@ -35,11 +35,40 @@ const getAuthLinks = (current, entryPoint) => {
|
|
|
35
35
|
return links;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
const renderLoginView = (entryPoint, versionTag, alerts = []) => {
|
|
38
|
+
const renderLoginView = async (entryPoint, versionTag, alerts = []) => {
|
|
39
|
+
const state = saltcorn.data.state.getState();
|
|
39
40
|
const form = prepareAuthForm(entryPoint);
|
|
40
41
|
form.onSubmit = `javascript:loginFormSubmit(this, '${entryPoint}')`;
|
|
41
42
|
form.submitLabel = "Login";
|
|
42
|
-
|
|
43
|
+
const layout = sbAdmin2Layout();
|
|
44
|
+
const login_form_name = state.getConfig("login_form", "");
|
|
45
|
+
if (login_form_name) {
|
|
46
|
+
const login_form = saltcorn.data.models.View.findOne({
|
|
47
|
+
name: login_form_name,
|
|
48
|
+
});
|
|
49
|
+
if (login_form) {
|
|
50
|
+
const req = new MobileRequest();
|
|
51
|
+
const res = new MobileResponse();
|
|
52
|
+
const resp = await login_form.run_possibly_on_page({}, req, res);
|
|
53
|
+
if (login_form.default_render_page) {
|
|
54
|
+
return layout.wrap({
|
|
55
|
+
title: "Login",
|
|
56
|
+
no_menu: true,
|
|
57
|
+
body: resp,
|
|
58
|
+
alerts: [],
|
|
59
|
+
role: req.user ? req.user.role_id : 100,
|
|
60
|
+
req,
|
|
61
|
+
headers: getHeaders(),
|
|
62
|
+
brand: {
|
|
63
|
+
name: state.getConfig("site_name") || "Saltcorn",
|
|
64
|
+
logo: state.mobileConfig.encodedSiteLogo,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return layout.authWrap({
|
|
43
72
|
title: "login",
|
|
44
73
|
form: form,
|
|
45
74
|
authLinks: getAuthLinks("login", entryPoint),
|
|
@@ -72,7 +101,7 @@ const renderSignupView = (entryPoint, versionTag) => {
|
|
|
72
101
|
const getLoginView = async (context) => {
|
|
73
102
|
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
74
103
|
return {
|
|
75
|
-
content: renderLoginView(
|
|
104
|
+
content: await renderLoginView(
|
|
76
105
|
mobileConfig.entry_point,
|
|
77
106
|
mobileConfig.version_tag,
|
|
78
107
|
context.alerts ? context.alerts : []
|
|
@@ -97,7 +126,7 @@ const logoutAction = async () => {
|
|
|
97
126
|
clearHistory();
|
|
98
127
|
config.jwt = undefined;
|
|
99
128
|
return {
|
|
100
|
-
content: renderLoginView(config.entry_point, config.version_tag),
|
|
129
|
+
content: await renderLoginView(config.entry_point, config.version_tag),
|
|
101
130
|
};
|
|
102
131
|
} else {
|
|
103
132
|
console.log("unable to logout");
|
package/www/js/routes/common.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*global saltcorn, offlineHelper*/
|
|
2
2
|
|
|
3
3
|
const getHeaders = () => {
|
|
4
|
-
const
|
|
4
|
+
const state = saltcorn.data.state.getState();
|
|
5
|
+
const config = state.mobileConfig;
|
|
5
6
|
const versionTag = config.version_tag;
|
|
6
7
|
const stdHeaders = [
|
|
7
8
|
{ css: `static_assets/${versionTag}/saltcorn.css` },
|
|
@@ -9,7 +10,13 @@ const getHeaders = () => {
|
|
|
9
10
|
{ script: `static_assets/${versionTag}/dayjs.min.js` },
|
|
10
11
|
{ script: "js/utils/iframe_view_utils.js" },
|
|
11
12
|
];
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
let from_cfg = [];
|
|
15
|
+
if (state.getConfig("page_custom_css", ""))
|
|
16
|
+
from_cfg.push({ style: state.getConfig("page_custom_css", "") });
|
|
17
|
+
if (state.getConfig("page_custom_html", ""))
|
|
18
|
+
from_cfg.push({ headerTag: state.getConfig("page_custom_html", "") });
|
|
19
|
+
return [...stdHeaders, ...config.pluginHeaders, ...from_cfg];
|
|
13
20
|
};
|
|
14
21
|
|
|
15
22
|
const parseQuery = (queryStr) => {
|
package/www/js/routes/page.js
CHANGED
|
@@ -81,15 +81,21 @@ const runPageGroup = async (pageGroup, state, context, { req, res }) => {
|
|
|
81
81
|
// get/page/pagename
|
|
82
82
|
const getPage = async (context) => {
|
|
83
83
|
const state = saltcorn.data.state.getState();
|
|
84
|
-
const
|
|
84
|
+
const query = context.query ? parseQuery(context.query) : {};
|
|
85
|
+
const req = new MobileRequest({ xhr: context.xhr, query: query });
|
|
85
86
|
const res = new MobileResponse();
|
|
86
87
|
const { page_name } = context.params;
|
|
87
88
|
const { page, pageGroup } = findPageOrGroup(page_name);
|
|
88
89
|
let contents = null;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
contents = await
|
|
92
|
-
|
|
90
|
+
state.queriesCache = {};
|
|
91
|
+
try {
|
|
92
|
+
if (page) contents = await runPage(page, state, context, { req, res });
|
|
93
|
+
else if (pageGroup)
|
|
94
|
+
contents = await runPageGroup(pageGroup, state, context, { req, res });
|
|
95
|
+
else throw new Error(req.__("Page %s not found", page_name));
|
|
96
|
+
} finally {
|
|
97
|
+
state.queriesCache = null;
|
|
98
|
+
}
|
|
93
99
|
if (contents.html_file) {
|
|
94
100
|
if (state.mobileConfig?.isOfflineMode)
|
|
95
101
|
throw new Error(req.__("Offline mode: cannot load file"));
|
package/www/js/routes/view.js
CHANGED
|
@@ -137,12 +137,18 @@ const getView = async (context) => {
|
|
|
137
137
|
req.__("Not authorized") + additionalInfos
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
state.queriesCache = {};
|
|
141
|
+
let contents = null;
|
|
142
|
+
try {
|
|
143
|
+
contents = await view.run_possibly_on_page(
|
|
144
|
+
query,
|
|
145
|
+
req,
|
|
146
|
+
res,
|
|
147
|
+
view.isRemoteTable()
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
state.queriesCache = null;
|
|
151
|
+
}
|
|
146
152
|
const wrapped = res.getWrapHtml();
|
|
147
153
|
if (wrapped)
|
|
148
154
|
return wrapContents(
|
|
@@ -11,9 +11,25 @@ function currentLocation() {
|
|
|
11
11
|
return routingHistory[index].route;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function currentQuery() {
|
|
14
|
+
function currentQuery(skipPosts = false) {
|
|
15
15
|
if (routingHistory.length == 0) return undefined;
|
|
16
|
-
|
|
16
|
+
let index = routingHistory.length - 1;
|
|
17
|
+
if (skipPosts)
|
|
18
|
+
while (index > 0 && routingHistory[index].route.startsWith("post/")) {
|
|
19
|
+
index--;
|
|
20
|
+
}
|
|
21
|
+
return routingHistory[index].query;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function addQueryParam(key, value) {
|
|
25
|
+
let query = currentQuery();
|
|
26
|
+
if (!query) {
|
|
27
|
+
routingHistory[routingHistory.length - 1].query = `${key}=${value}`;
|
|
28
|
+
} else {
|
|
29
|
+
const parsed = new URLSearchParams(query);
|
|
30
|
+
parsed.set(key, value);
|
|
31
|
+
routingHistory[routingHistory.length - 1].query = parsed.toString();
|
|
32
|
+
}
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
function addRoute(routeEntry) {
|
|
@@ -176,9 +192,7 @@ function splitPathQuery(url) {
|
|
|
176
192
|
|
|
177
193
|
async function replaceIframe(content, isFile = false) {
|
|
178
194
|
const iframe = document.getElementById("content-iframe");
|
|
179
|
-
|
|
180
|
-
const url = await getDirEntry(`${cordova.file.dataDirectory}content.html`);
|
|
181
|
-
iframe.src = url.toURL();
|
|
195
|
+
iframe.srcdoc = content;
|
|
182
196
|
if (isFile) {
|
|
183
197
|
iframe.setAttribute("is-html-file", true);
|
|
184
198
|
await new Promise((resolve, reject) => {
|
|
@@ -242,6 +256,7 @@ async function replaceIframeInnerContent(content) {
|
|
|
242
256
|
if (scmodal) {
|
|
243
257
|
scmodal.modal("hide");
|
|
244
258
|
}
|
|
259
|
+
iframe.contentWindow.scrollTo(0, 0);
|
|
245
260
|
iframe.contentWindow.initialize_page();
|
|
246
261
|
}
|
|
247
262
|
|
|
@@ -366,7 +381,7 @@ async function handleRoute(route, query, files, data) {
|
|
|
366
381
|
async function reload() {
|
|
367
382
|
const currentRoute = currentLocation();
|
|
368
383
|
if (!currentRoute) await gotoEntryView();
|
|
369
|
-
await handleRoute(currentRoute, currentQuery());
|
|
384
|
+
await handleRoute(currentRoute, currentQuery(true));
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
async function goBack(steps = 1, exitOnFirstPage = false) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*eslint-env browser*/
|
|
2
|
-
/*global $, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success*/
|
|
2
|
+
/*global $, KTDrawer, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success*/
|
|
3
3
|
|
|
4
4
|
function combineFormAndQuery(form, query) {
|
|
5
5
|
let paramsList = [];
|
|
@@ -27,15 +27,34 @@ async function execLink(url, linkSrc) {
|
|
|
27
27
|
} else
|
|
28
28
|
try {
|
|
29
29
|
showLoadSpinner();
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (url.startsWith("javascript:")) eval(url.substring(11));
|
|
31
|
+
else {
|
|
32
|
+
const { path, query } = parent.splitPathQuery(url);
|
|
33
|
+
await parent.handleRoute(`get${path}`, query);
|
|
34
|
+
}
|
|
32
35
|
} finally {
|
|
33
36
|
removeLoadSpinner();
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
async function runUrl(url, method = "get") {
|
|
41
|
+
const { path, query } = parent.splitPathQuery(url);
|
|
42
|
+
const page = await parent.router.resolve({
|
|
43
|
+
pathname: `${method}${path}`,
|
|
44
|
+
query: query,
|
|
45
|
+
});
|
|
46
|
+
return page.content;
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
async function execNavbarLink(url) {
|
|
38
50
|
$(".navbar-toggler").click();
|
|
51
|
+
if (typeof KTDrawer === "function") {
|
|
52
|
+
const aside = $("#kt_aside")[0];
|
|
53
|
+
if (aside) {
|
|
54
|
+
const kAside = KTDrawer.getInstance(aside);
|
|
55
|
+
kAside.hide();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
execLink(url);
|
|
40
59
|
}
|
|
41
60
|
|
|
@@ -186,14 +205,10 @@ async function login(e, entryPoint, isSignup) {
|
|
|
186
205
|
config.user_name = decodedJwt.user.email;
|
|
187
206
|
config.user_id = decodedJwt.user.id;
|
|
188
207
|
config.language = decodedJwt.user.language;
|
|
208
|
+
config.user = decodedJwt.user;
|
|
189
209
|
config.isPublicUser = false;
|
|
190
210
|
config.isOfflineMode = false;
|
|
191
|
-
await parent.insertUser(
|
|
192
|
-
id: config.user_id,
|
|
193
|
-
email: config.user_name,
|
|
194
|
-
role_id: config.role_id,
|
|
195
|
-
language: config.language,
|
|
196
|
-
});
|
|
211
|
+
await parent.insertUser(config.user);
|
|
197
212
|
await parent.setJwt(loginResult);
|
|
198
213
|
config.jwt = loginResult;
|
|
199
214
|
await parent.i18next.changeLanguage(config.language);
|
|
@@ -245,9 +260,16 @@ async function publicLogin(entryPoint) {
|
|
|
245
260
|
const loginResult = await loginRequest({ isPublic: true });
|
|
246
261
|
if (typeof loginResult === "string") {
|
|
247
262
|
const config = parent.saltcorn.data.state.getState().mobileConfig;
|
|
263
|
+
config.user = {
|
|
264
|
+
role_id: 100,
|
|
265
|
+
user_name: "public",
|
|
266
|
+
language: "en",
|
|
267
|
+
};
|
|
268
|
+
// TODO remove these, use 'user' everywhere
|
|
248
269
|
config.role_id = 100;
|
|
249
270
|
config.user_name = "public";
|
|
250
271
|
config.language = "en";
|
|
272
|
+
|
|
251
273
|
config.isPublicUser = true;
|
|
252
274
|
await parent.setJwt(loginResult);
|
|
253
275
|
config.jwt = loginResult;
|
|
@@ -321,7 +343,13 @@ async function signupFormSubmit(e, entryView) {
|
|
|
321
343
|
|
|
322
344
|
async function loginFormSubmit(e, entryView) {
|
|
323
345
|
try {
|
|
324
|
-
|
|
346
|
+
let safeEntryView = entryView;
|
|
347
|
+
if (!safeEntryView) {
|
|
348
|
+
const config = parent.saltcorn.data.state.getState().mobileConfig;
|
|
349
|
+
if (!config.entry_point) throw new Error("Unable to find an entry-point");
|
|
350
|
+
safeEntryView = config.entry_point;
|
|
351
|
+
}
|
|
352
|
+
await login(e, safeEntryView, false);
|
|
325
353
|
} catch (error) {
|
|
326
354
|
parent.errorAlert(error);
|
|
327
355
|
}
|
|
@@ -464,6 +492,10 @@ async function gopage(n, pagesize, viewIdentifier, extra) {
|
|
|
464
492
|
);
|
|
465
493
|
}
|
|
466
494
|
|
|
495
|
+
function ajax_modal(url, opts = {}) {
|
|
496
|
+
mobile_modal(url, opts);
|
|
497
|
+
}
|
|
498
|
+
|
|
467
499
|
async function mobile_modal(url, opts = {}) {
|
|
468
500
|
if ($("#scmodal").length === 0) {
|
|
469
501
|
$("body").append(`<div id="scmodal" class="modal">
|
|
@@ -473,6 +505,13 @@ async function mobile_modal(url, opts = {}) {
|
|
|
473
505
|
<h5 class="modal-title">Modal title</h5>
|
|
474
506
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
|
475
507
|
</button>
|
|
508
|
+
<div
|
|
509
|
+
id="modal-toasts-area"
|
|
510
|
+
class="toast-container position-fixed top-0 start-50 p-0"
|
|
511
|
+
style: "z-index: 7000;"
|
|
512
|
+
aria-live="polite"
|
|
513
|
+
aria-atomic="true">
|
|
514
|
+
</div>
|
|
476
515
|
</div>
|
|
477
516
|
<div class="modal-body">
|
|
478
517
|
<p>Modal body text goes here.</p>
|
|
@@ -857,10 +896,10 @@ function showLoadSpinner() {
|
|
|
857
896
|
$("body").append(`
|
|
858
897
|
<div
|
|
859
898
|
id="scspinner"
|
|
860
|
-
style="position:
|
|
861
|
-
top:
|
|
862
|
-
|
|
863
|
-
|
|
899
|
+
style="position: fixed;
|
|
900
|
+
top: 50%;
|
|
901
|
+
left: 50%;
|
|
902
|
+
transform: translate(-50%, -50%);
|
|
864
903
|
z-index: 9999;"
|
|
865
904
|
>
|
|
866
905
|
<div
|