soiz1 commited on
Commit
b263e7a
·
verified ·
1 Parent(s): 979c35d

Update src/addons/addons/save-to-google/userscript.js

Browse files
src/addons/addons/save-to-google/userscript.js CHANGED
@@ -1,544 +1,31 @@
1
- import SB3Downloader from "/src/containers/sb3-downloader.jsx";
2
- import { getProjectFilename } from '/src/containers/sb3-downloader.jsx';
3
-
4
- const getProjectThumbnail = () => new Promise(resolve => {
5
- window.vm.renderer.requestSnapshot(uri => {
6
- resolve(uri);
7
- });
8
- });
9
-
10
  export default async ({ addon, console, msg }) => {
11
- const CLIENT_ID = "1033286471224-n9mv8l869fqikubj2e8q92n8ige3qr6r.apps.googleusercontent.com";
12
- const REDIRECT_URI = "https://soiz1-s4s-upload.hf.space/close2";
13
- const SCOPES = "https://www.googleapis.com/auth/drive.file";
14
- const PROXY_URL = "https://soiz1-drive-proxy.hf.space/?file_id=";
15
- const SHORT_URL = "https://s4.rf.gd/";
16
- const SHARE_URL = "https://scratch-school.ct.ws/upload?id=";
17
-
18
- let accessToken = localStorage.getItem('googleDriveAccessToken') || null;
19
- let currentAccountEmail = localStorage.getItem('googleDriveAccountEmail') || null;
20
- let currentAccountName = localStorage.getItem('googleDriveAccountName') || null;
21
-
22
  while (true) {
23
  const targetElem = await addon.tab.waitForElement(
24
  'div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)',
25
  { markAsSeen: true }
26
  );
27
 
28
- if (!document.querySelector('.sa-custom-modal-button')) {
29
- const button = document.createElement("div");
30
- button.className = "sa-custom-modal-button " + targetElem.className;
31
- button.textContent = "保存";
32
- button.style.cursor = "pointer";
33
-
34
- button.addEventListener("click", () => {
35
- showMainModal(addon);
36
- });
37
-
38
- targetElem.parentElement.appendChild(button);
39
- }
40
- // 「フィードバックを送信」ボタンを作成
41
- const feedbackButton = document.createElement("div");
42
- feedbackButton.className = "sa-feedback-button " + targetElem.className;
43
- feedbackButton.textContent = "フィードバックを送信";
44
- feedbackButton.style.cursor = "pointer";
45
- feedbackButton.style.marginLeft = "10px"; // 少し間隔を空ける
46
-
47
- feedbackButton.addEventListener("click", () => {
48
- window.open("https://forms.gle/mP9U2biYcYUmupiY9", "_blank");
49
- });
50
-
51
- targetElem.parentElement.appendChild(feedbackButton);
52
-
53
- }
54
-
55
- function showMainModal(addon) {
56
- const modal = addon.tab.createModal("Googleドライブに保存", {
57
- isOpen: true,
58
- useEditorClasses: true,
59
- maxWidth: "800px",
60
- maxHeight: "80vh"
61
- });
62
-
63
- modal.content.innerHTML = `
64
- <div style="padding: 1rem; max-height: 70vh; overflow-y: auto;">
65
- <h1 style="font-size: 1.5rem; margin-bottom: 1rem;">Googleドライブに接続</h1>
66
- ${accessToken ? `
67
- <div style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
68
- <div>ログイン中: ${currentAccountName || currentAccountEmail || 'Googleアカウント'}</div>
69
- <button id="change-account-button" class="button" style="padding: 0.25rem 0.5rem; font-size: 0.9rem;">アカウントを変更</button>
70
- </div>
71
- ` : `
72
- <p style="margin-bottom: 1rem;">Googleでログインして、プロジェクトを保存または更新します。</p>
73
- <button id="google-login-button" class="button">Googleでログイン</button>
74
- `}
75
- <div id="file-list-container" style="margin-top: 1rem; ${accessToken ? '' : 'display: none;'}">
76
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
77
- <h2 style="margin: 0; font-size: 1.2rem;">プロジェクト: <span id="project-title" style="cursor: pointer; border-bottom: 1px dashed #000; color: #333;">${window.vm.runtime.projectName || "無題"}</span></h2>
78
- <button id="new-file-button" class="button">新規保存</button>
79
- </div>
80
- <div id="file-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem;"></div>
81
- </div>
82
- </div>
83
- `;
84
-
85
- const loginButton = modal.content.querySelector("#google-login-button");
86
- const changeAccountButton = modal.content.querySelector("#change-account-button");
87
- const fileListContainer = modal.content.querySelector("#file-list-container");
88
- const fileList = modal.content.querySelector("#file-list");
89
- const newFileButton = modal.content.querySelector("#new-file-button");
90
- const projectTitle = modal.content.querySelector("#project-title");
91
-
92
- // プロジェクトタイトルの編集機能
93
- projectTitle?.addEventListener("dblclick", () => {
94
- const currentName = projectTitle.textContent;
95
- const input = document.createElement("input");
96
- input.type = "text";
97
- input.value = currentName;
98
- input.style.width = "200px";
99
-
100
- projectTitle.replaceWith(input);
101
- input.focus();
102
-
103
- const handleBlur = () => {
104
- const newName = input.value.trim() || "無題";
105
- window.vm.runtime.projectName = newName;
106
- projectTitle.textContent = newName;
107
- input.replaceWith(projectTitle);
108
- };
109
-
110
- input.addEventListener("blur", handleBlur);
111
- input.addEventListener("keypress", (e) => {
112
- if (e.key === "Enter") handleBlur();
113
- });
114
- });
115
-
116
- if (loginButton) {
117
- loginButton.addEventListener("click", () => {
118
- startGoogleLogin(modal, addon);
119
- });
120
- }
121
-
122
- if (changeAccountButton) {
123
- changeAccountButton.addEventListener("click", () => {
124
- accessToken = null;
125
- currentAccountEmail = null;
126
- currentAccountName = null;
127
- localStorage.removeItem('googleDriveAccessToken');
128
- localStorage.removeItem('googleDriveAccountEmail');
129
- localStorage.removeItem('googleDriveAccountName');
130
- showMainModal(addon);
131
- modal.remove();
132
- });
133
- }
134
-
135
- newFileButton?.addEventListener("click", async () => {
136
- try {
137
- await saveToGoogleDrive(null, null, modal.remove, addon);
138
- } catch (error) {
139
- console.error("新規保存エラー:", error);
140
- showAlert(addon, "error", "新規保存に失敗しました");
141
- }
142
- });
143
-
144
- if (accessToken) {
145
- fetchDriveFiles(accessToken)
146
- .then(files => {
147
- displayFileList(files, accessToken, modal, addon);
148
- fileListContainer.style.display = "block";
149
- })
150
- .catch(error => {
151
- console.error("ファイル一覧取得エラー:", error);
152
- showAlert(addon, "error", "ファイル一覧の取得に失敗しました");
153
- });
154
- }
155
-
156
- modal.backdrop.addEventListener("click", modal.remove);
157
- modal.closeButton.addEventListener("click", modal.remove);
158
- }
159
-
160
- function startGoogleLogin(modal, addon) {
161
- const messageListener = (event) => {
162
- if (event.origin === "https://soiz1-penguin-upload.hf.space" && event.data.token) {
163
- window.removeEventListener("message", messageListener);
164
- accessToken = event.data.token;
165
- currentAccountEmail = event.data.email || null;
166
- currentAccountName = event.data.name || null;
167
-
168
- localStorage.setItem('googleDriveAccessToken', accessToken);
169
- if (currentAccountEmail) {
170
- localStorage.setItem('googleDriveAccountEmail', currentAccountEmail);
171
  }
172
- if (currentAccountName) {
173
- localStorage.setItem('googleDriveAccountName', currentAccountName);
174
- }
175
-
176
- showMainModal(addon);
177
- modal.remove();
178
- }
179
- };
180
- window.addEventListener("message", messageListener);
181
-
182
- const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
183
- `client_id=${CLIENT_ID}` +
184
- `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
185
- `&response_type=token` +
186
- `&scope=${encodeURIComponent(SCOPES)}`;
187
-
188
- window.open(authUrl, "_blank", "width=500,height=600");
189
- }
190
-
191
- async function fetchDriveFiles(accessToken) {
192
- const response = await fetch("https://www.googleapis.com/drive/v3/files?q=(mimeType='application/x-scratch' or mimeType='image/png')", {
193
- headers: {
194
- Authorization: `Bearer ${accessToken}`,
195
- },
196
- });
197
-
198
- if (!response.ok) {
199
- throw new Error(await response.text());
200
- }
201
-
202
- const data = await response.json();
203
- return data.files || [];
204
- }
205
-
206
- function displayFileList(files, accessToken, modal, addon) {
207
- const fileList = modal.content.querySelector("#file-list");
208
- fileList.innerHTML = "";
209
-
210
- // プロジェクトファイルとサムネイルを関連付ける
211
- const projectFiles = files.filter(file => file.mimeType === 'application/x-scratch');
212
- const thumbnailFiles = files.filter(file => file.mimeType === 'image/png');
213
-
214
- if (projectFiles.length === 0) {
215
- fileList.innerHTML = "<p style='grid-column: 1 / -1; text-align: center;'>保存されたファイルが見つかりません</p>";
216
- return;
217
- }
218
-
219
- projectFiles.forEach(project => {
220
- // 対応するサムネイルを探す
221
- const thumbnail = thumbnailFiles.find(
222
- thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
223
- );
224
-
225
- const fileItem = document.createElement("div");
226
- fileItem.style.border = "1px solid #ddd";
227
- fileItem.style.borderRadius = "8px";
228
- fileItem.style.padding = "1rem";
229
- fileItem.style.display = "flex";
230
- fileItem.style.flexDirection = "column";
231
- fileItem.style.gap = "0.75rem";
232
- fileItem.style.background = "#fff";
233
- fileItem.style.height = "100%";
234
-
235
- // サムネイル表示
236
- const thumbnailContainer = document.createElement("div");
237
- thumbnailContainer.style.position = "relative";
238
- thumbnailContainer.style.aspectRatio = "4/3";
239
- thumbnailContainer.style.maxHeight = "150px";
240
- thumbnailContainer.style.overflow = "hidden";
241
-
242
- if (thumbnail) {
243
- const thumbnailImg = document.createElement("img");
244
- thumbnailImg.src = `https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`;
245
- thumbnailImg.style.width = "100%";
246
- thumbnailImg.style.height = "100%";
247
- thumbnailImg.style.borderRadius = "4px";
248
- thumbnailImg.style.objectFit = "contain";
249
- thumbnailImg.style.backgroundColor = "#f0f0f0";
250
- thumbnailContainer.appendChild(thumbnailImg);
251
- } else {
252
- const thumbnailPlaceholder = document.createElement("div");
253
- thumbnailPlaceholder.style.width = "100%";
254
- thumbnailPlaceholder.style.height = "100%";
255
- thumbnailPlaceholder.style.backgroundColor = "#f0f0f0";
256
- thumbnailPlaceholder.style.display = "flex";
257
- thumbnailPlaceholder.style.alignItems = "center";
258
- thumbnailPlaceholder.style.justifyContent = "center";
259
- thumbnailPlaceholder.style.borderRadius = "4px";
260
- thumbnailPlaceholder.textContent = "サムネイルなし";
261
- thumbnailContainer.appendChild(thumbnailPlaceholder);
262
- }
263
- fileItem.appendChild(thumbnailContainer);
264
-
265
- // プロジェクト名
266
- const fileName = document.createElement("h3");
267
- fileName.textContent = project.name.replace('.s4s.txt', '');
268
- fileName.style.margin = "0";
269
- fileName.style.fontSize = "1rem";
270
- fileName.style.textAlign = "center";
271
- fileName.style.fontWeight = "bold";
272
- fileName.style.color = "#333";
273
- fileItem.appendChild(fileName);
274
-
275
- // 共有リンク
276
- const linkContainer = document.createElement("div");
277
- linkContainer.style.display = "flex";
278
- linkContainer.style.flexDirection = "column";
279
- linkContainer.style.gap = "0.25rem";
280
- linkContainer.style.marginBottom = "0.5rem";
281
-
282
- const linkHeader = document.createElement("div");
283
- linkHeader.style.display = "flex";
284
- linkHeader.style.justifyContent = "space-between";
285
- linkHeader.style.alignItems = "center";
286
-
287
- const linkLabel = document.createElement("span");
288
- linkLabel.textContent = "共有リンク:";
289
- linkLabel.style.fontSize = "0.8em";
290
- linkLabel.style.color = "#555";
291
-
292
- const copyButton = document.createElement("button");
293
- copyButton.textContent = "コピー";
294
- copyButton.className = "button";
295
- copyButton.style.fontSize = "0.8em";
296
- copyButton.style.padding = "0.1rem 0.3rem";
297
- copyButton.style.backgroundColor = "#e9e9e9";
298
- copyButton.style.color = "#333";
299
-
300
- copyButton.addEventListener("click", (e) => {
301
- e.stopPropagation();
302
- navigator.clipboard.writeText(`${SHORT_URL}${project.id}`)
303
- .then(() => showAlert(addon, "success", "リンクをクリップボードにコピーしました"))
304
- .catch(() => showAlert(addon, "error", "リンクのコピーに失敗しました"));
305
- });
306
-
307
- linkHeader.appendChild(linkLabel);
308
- linkHeader.appendChild(copyButton);
309
- linkContainer.appendChild(linkHeader);
310
-
311
- const linkUrl = document.createElement("a");
312
- linkUrl.href = `${SHORT_URL}${project.id}`;
313
- linkUrl.textContent = `${SHORT_URL}${project.id}`;
314
- linkUrl.target = "_blank";
315
- linkUrl.rel = "noopener noreferrer";
316
- linkUrl.style.fontSize = "0.9em";
317
- linkUrl.style.wordBreak = "break-all";
318
- linkUrl.style.color = "#1155cc";
319
- linkUrl.style.textDecoration = "none";
320
- linkUrl.style.borderBottom = "1px solid #1155cc";
321
- linkContainer.appendChild(linkUrl);
322
-
323
- fileItem.appendChild(linkContainer);
324
-
325
- // 操作ボタン
326
- const buttonContainer = document.createElement("div");
327
- buttonContainer.style.display = "grid";
328
- buttonContainer.style.gridTemplateColumns = "1fr 1fr";
329
- buttonContainer.style.gap = "0.5rem";
330
-
331
- const loadButton = document.createElement("button");
332
- loadButton.textContent = "読み込む";
333
- loadButton.className = "button";
334
- loadButton.style.width = "100%";
335
-
336
- loadButton.addEventListener("click", (e) => {
337
- e.stopPropagation();
338
- if (confirm(`"${project.name}"を読み込みますか?現在のプロジェクトは失われます。`)) {
339
- const url = `${PROXY_URL}${project.id}`;
340
- window.location.href = `?project_url=${encodeURIComponent(url)}`;
341
- }
342
- });
343
-
344
- const replaceButton = document.createElement("button");
345
- replaceButton.textContent = "上書き";
346
- replaceButton.className = "button";
347
- replaceButton.style.width = "100%";
348
-
349
- replaceButton.addEventListener("click", (e) => {
350
- e.stopPropagation();
351
- if (confirm(`"${project.name}"を現在のプロジェクトで上書きしますか?`)) {
352
- saveToGoogleDrive(project.id, project.name, () => {
353
- fetchDriveFiles(accessToken)
354
- .then(files => displayFileList(files, accessToken, modal, addon))
355
- .catch(error => console.error("更新エラー:", error));
356
- }, addon)
357
- .catch(error => {
358
- console.error("ファイル上書きエラー:", error);
359
- showAlert(addon, "error", "ファイルの上書きに失敗しました");
360
- });
361
- }
362
- });
363
-
364
- const shareButton = document.createElement("button");
365
- shareButton.textContent = "共有";
366
- shareButton.className = "button";
367
- shareButton.style.width = "100%";
368
- shareButton.style.backgroundColor = "#4CAF50";
369
- shareButton.style.color = "white";
370
-
371
- shareButton.addEventListener("click", (e) => {
372
- e.stopPropagation();
373
- window.open(`${SHARE_URL}${project.id}`, "_blank");
374
  });
375
 
376
- const deleteButton = document.createElement("button");
377
- deleteButton.textContent = "削除";
378
- deleteButton.className = "button";
379
- deleteButton.style.width = "100%";
380
- deleteButton.style.backgroundColor = "#ff4444";
381
- deleteButton.style.color = "white";
382
-
383
- deleteButton.addEventListener("click", async (e) => {
384
- e.stopPropagation();
385
- if (confirm(`"${project.name}"とそのサムネイルを完全に削除しますか?この操作は元に戻せません。`)) {
386
- try {
387
- // プロジェクトファイルを削除
388
- await deleteFile(project.id, accessToken);
389
-
390
- // 対応するサムネイルを探して削除
391
- const thumbnailToDelete = thumbnailFiles.find(
392
- thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
393
- );
394
-
395
- if (thumbnailToDelete) {
396
- await deleteFile(thumbnailToDelete.id, accessToken);
397
- }
398
-
399
- showAlert(addon, "success", "ファイルを削除しました");
400
- // リストを更新
401
- fetchDriveFiles(accessToken)
402
- .then(files => displayFileList(files, accessToken, modal, addon))
403
- .catch(error => console.error("更新エラー:", error));
404
- } catch (error) {
405
- console.error("削除エラー:", error);
406
- showAlert(addon, "error", "ファイルの削除に失敗しました");
407
- }
408
- }
409
- });
410
-
411
- buttonContainer.appendChild(loadButton);
412
- buttonContainer.appendChild(replaceButton);
413
- buttonContainer.appendChild(shareButton);
414
- buttonContainer.appendChild(deleteButton);
415
- fileItem.appendChild(buttonContainer);
416
-
417
- fileList.appendChild(fileItem);
418
- });
419
- }
420
-
421
- async function deleteFile(fileId, accessToken) {
422
- const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
423
- method: "DELETE",
424
- headers: {
425
- Authorization: `Bearer ${accessToken}`,
426
- },
427
- });
428
-
429
- if (!response.ok) {
430
- throw new Error(await response.text());
431
  }
432
  }
433
-
434
- async function saveToGoogleDrive(fileId, fileName, onSuccess, addon) {
435
- try {
436
- // プロジェクトを保存
437
- const blob = await window.vm.saveProjectSb3();
438
- const projectName = window.vm.runtime.projectName || getProjectFilename || "無題";
439
- const nameToUse = fileName || `${projectName}.s4s.txt`;
440
-
441
- const metadata = {
442
- name: nameToUse,
443
- mimeType: "application/x-scratch",
444
- };
445
-
446
- const url = fileId
447
- ? `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`
448
- : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
449
-
450
- const form = new FormData();
451
- form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
452
- form.append("file", blob);
453
-
454
- const method = fileId ? "PATCH" : "POST";
455
-
456
- const uploadResponse = await fetch(url, {
457
- method,
458
- headers: {
459
- Authorization: `Bearer ${accessToken}`,
460
- },
461
- body: form,
462
- });
463
-
464
- if (!uploadResponse.ok) {
465
- throw new Error(await uploadResponse.text());
466
- }
467
-
468
- const fileData = await uploadResponse.json();
469
-
470
- // サムネイルを保存
471
- try {
472
- const thumbnailDataUrl = await getProjectThumbnail();
473
- const thumbnailBlob = await (await fetch(thumbnailDataUrl)).blob();
474
- const thumbnailMetadata = {
475
- name: `Penguin-Thumbnail-${fileData.id}.png`,
476
- mimeType: "image/png",
477
- };
478
-
479
- const existingThumbnailResponse = await fetch(
480
- `https://www.googleapis.com/drive/v3/files?q=name='${thumbnailMetadata.name}'`,
481
- {
482
- headers: {
483
- Authorization: `Bearer ${accessToken}`,
484
- },
485
- }
486
- );
487
-
488
- const existingThumbnailData = await existingThumbnailResponse.json();
489
- const thumbnailFileId = existingThumbnailData.files?.[0]?.id;
490
-
491
- const thumbnailUrl = thumbnailFileId
492
- ? `https://www.googleapis.com/upload/drive/v3/files/${thumbnailFileId}?uploadType=multipart`
493
- : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
494
-
495
- const thumbnailForm = new FormData();
496
- thumbnailForm.append("metadata", new Blob([JSON.stringify(thumbnailMetadata)], { type: "application/json" }));
497
- thumbnailForm.append("file", thumbnailBlob);
498
-
499
- const thumbnailMethod = thumbnailFileId ? "PATCH" : "POST";
500
-
501
- await fetch(thumbnailUrl, {
502
- method: thumbnailMethod,
503
- headers: {
504
- Authorization: `Bearer ${accessToken}`,
505
- },
506
- body: thumbnailForm,
507
- });
508
- } catch (thumbnailError) {
509
- console.warn("サムネイルの保存に失敗しました:", thumbnailError);
510
- }
511
-
512
- if (!fileId) {
513
- await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
514
- method: "POST",
515
- headers: {
516
- Authorization: `Bearer ${accessToken}`,
517
- "Content-Type": "application/json",
518
- },
519
- body: JSON.stringify({
520
- role: "reader",
521
- type: "anyone",
522
- }),
523
- });
524
- }
525
-
526
- showAlert(addon, "success", fileId ? "上書き保存しました" : "新規保存しました");
527
- if (onSuccess) onSuccess();
528
- } catch (error) {
529
- console.error("保存エラー:", error);
530
- showAlert(addon, "error", `保存に失敗しました: ${error.message}`);
531
- throw error;
532
- }
533
- }
534
-
535
- function showAlert(addon, type, message) {
536
- addon.tab.redux.dispatch({
537
- type: "scratch-gui/alerts/SHOW_ALERT",
538
- payload: {
539
- alertType: type,
540
- message: message
541
- }
542
- });
543
- }
544
- };
 
 
 
 
 
 
 
 
 
 
1
  export default async ({ addon, console, msg }) => {
 
 
 
 
 
 
 
 
 
 
 
2
  while (true) {
3
  const targetElem = await addon.tab.waitForElement(
4
  'div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)',
5
  { markAsSeen: true }
6
  );
7
 
8
+ if (!document.querySelector('.sa-eruda-button')) {
9
+ const erudaButton = document.createElement("div");
10
+ erudaButton.className = "sa-eruda-button " + targetElem.className;
11
+ erudaButton.textContent = "開発者ツール";
12
+ erudaButton.style.cursor = "pointer";
13
+ erudaButton.style.marginLeft = "10px";
14
+
15
+ erudaButton.addEventListener("click", () => {
16
+ if (!window.eruda) {
17
+ const script = document.createElement('script');
18
+ script.src = 'https://cdn.jsdelivr.net/npm/eruda';
19
+ script.onload = () => {
20
+ eruda.init();
21
+ };
22
+ document.body.appendChild(script);
23
+ } else {
24
+ eruda.show();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  });
27
 
28
+ targetElem.parentElement.appendChild(erudaButton);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
  }
31
+ };