@open-wxa/wxa-runner-core 0.4.1 → 0.6.0

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.
@@ -433,8 +433,11 @@
433
433
  var SOURCES = ["camera", "album"];
434
434
  var SOURCE_LABELS = { camera: "\u62CD\u6444", album: "\u4ECE\u624B\u673A\u76F8\u518C\u9009\u62E9" };
435
435
  var DEFAULT_COUNT = 9;
436
+ var MEDIA_KINDS = ["image", "video"];
437
+ var FIRST_FRAME_TIMEOUT = 1e3;
438
+ var countOf = (args) => Math.max(1, Number(args.count) || DEFAULT_COUNT);
436
439
  function createMediaApis({ showActionSheet, chooseFiles: chooseFiles2, hasUserActivation: hasUserActivation2, showImagePreview }) {
437
- async function chooseImages(args) {
440
+ async function pickFiles(args, accept) {
438
441
  const allowed = SOURCES.filter((source2) => args.sourceType?.includes(source2));
439
442
  const sources = allowed.length ? allowed : SOURCES;
440
443
  let source = sources[0];
@@ -443,28 +446,33 @@
443
446
  if (index === null) return null;
444
447
  source = sources[index];
445
448
  }
446
- const count = Math.max(1, Number(args.count) || DEFAULT_COUNT);
447
- const files = await chooseFiles2({ accept: "image/*", multiple: count > 1, capture: source === "camera" });
448
- return files && files.slice(0, count);
449
+ return chooseFiles2({ accept, multiple: countOf(args) > 1, capture: source === "camera" });
449
450
  }
450
- const saveAll = (files) => Promise.all(files.map((file) => saveTempFile(file, tempFileType(file.name, file.type, "jpg"))));
451
+ const saveImage = (file) => saveTempFile(file, tempFileType(file.name, file.type, "jpg"));
451
452
  return {
452
453
  chooseImage: async (args, { api }) => {
453
- const files = await chooseImages(args);
454
+ const files = (await pickFiles(args, "image/*"))?.slice(0, countOf(args));
454
455
  if (!files) return fail(api, "cancel");
455
- const paths = await saveAll(files);
456
+ const paths = await Promise.all(files.map(saveImage));
456
457
  return ok(api, { tempFilePaths: paths, tempFiles: files.map((file, i) => ({ path: paths[i], size: file.size })) });
457
458
  },
458
459
  chooseMedia: async (args, { api }) => {
459
- const mediaType = args.mediaType?.length ? args.mediaType : ["image", "video"];
460
- if (!mediaType.includes("image") && !mediaType.includes("mix")) return fail(api, "wxa-runner \u6682\u4E0D\u652F\u6301\u9009\u62E9\u89C6\u9891");
461
- const files = await chooseImages(args);
462
- if (!files) return fail(api, "cancel");
463
- const paths = await saveAll(files);
464
- return ok(api, {
465
- type: "image",
466
- tempFiles: files.map((file, i) => ({ tempFilePath: paths[i], size: file.size, fileType: "image" }))
467
- });
460
+ const mediaType = args.mediaType?.length ? args.mediaType : MEDIA_KINDS;
461
+ const mix = mediaType.includes("mix");
462
+ const requested = MEDIA_KINDS.filter((kind) => mix || mediaType.includes(kind));
463
+ const kinds = requested.length ? requested : MEDIA_KINDS;
464
+ const picked = await pickFiles(args, kinds.map((kind) => `${kind}/*`).join(","));
465
+ if (!picked) return fail(api, "cancel");
466
+ const kindOf = (file) => kinds.length === 1 ? kinds[0] : file.type.startsWith("video/") ? "video" : "image";
467
+ const videos = picked.filter((file) => kindOf(file) === "video");
468
+ const files = (mix || !videos.length ? picked : videos).slice(0, countOf(args));
469
+ const tempFiles = await Promise.all(files.map(async (file) => kindOf(file) === "video" ? { ...await saveVideo(file), fileType: "video" } : { tempFilePath: await saveImage(file), size: file.size, fileType: "image" }));
470
+ return ok(api, { type: mix ? "mix" : tempFiles[0].fileType, tempFiles });
471
+ },
472
+ chooseVideo: async (args, { api }) => {
473
+ const file = (await pickFiles({ sourceType: args.sourceType, count: 1 }, "video/*"))?.[0];
474
+ if (!file) return fail(api, "cancel");
475
+ return ok(api, await saveVideo(file));
468
476
  },
469
477
  // 基础库已经把 current 换成了 urls 里的地址;也兼容直接传下标
470
478
  previewImage: (args) => {
@@ -493,6 +501,45 @@
493
501
  }
494
502
  };
495
503
  }
504
+ async function saveVideo(file) {
505
+ const [tempFilePath, { duration, width, height, thumb }] = await Promise.all([
506
+ saveTempFile(file, tempFileType(file.name, file.type, "mp4")),
507
+ readVideo(file)
508
+ ]);
509
+ const thumbTempFilePath = thumb ? await saveTempFile(thumb, "jpg") : "";
510
+ return { tempFilePath, size: file.size, duration, height, width, thumbTempFilePath };
511
+ }
512
+ async function readVideo(file) {
513
+ const video = document.createElement("video");
514
+ video.muted = true;
515
+ video.preload = "auto";
516
+ video.src = URL.createObjectURL(file);
517
+ try {
518
+ await new Promise((resolve, reject) => {
519
+ video.addEventListener("loadedmetadata", resolve, { once: true });
520
+ video.addEventListener("error", reject, { once: true });
521
+ });
522
+ const { videoWidth: width, videoHeight: height } = video;
523
+ const duration = Number.isFinite(video.duration) ? video.duration : 0;
524
+ return { duration, width, height, thumb: width && height ? await firstFrame(video) : null };
525
+ } catch {
526
+ return { duration: 0, width: 0, height: 0, thumb: null };
527
+ } finally {
528
+ URL.revokeObjectURL(video.src);
529
+ }
530
+ }
531
+ async function firstFrame(video) {
532
+ const loaded = video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA || await Promise.race([
533
+ new Promise((resolve) => video.addEventListener("loadeddata", () => resolve(true), { once: true })),
534
+ new Promise((resolve) => setTimeout(() => resolve(false), FIRST_FRAME_TIMEOUT))
535
+ ]);
536
+ if (!loaded) return null;
537
+ const canvas = document.createElement("canvas");
538
+ canvas.width = video.videoWidth;
539
+ canvas.height = video.videoHeight;
540
+ canvas.getContext("2d").drawImage(video, 0, 0);
541
+ return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg"));
542
+ }
496
543
  async function fetchFile(path) {
497
544
  const url = /^[a-z][a-z\d+.-]*:/i.test(path) ? path : "/" + path.replace(/^\//, "");
498
545
  const res = await fetch(url);
@@ -798,11 +845,10 @@
798
845
  });
799
846
  return regionTree;
800
847
  };
801
- const showCascade = (model, headerText) => {
848
+ const showCascade = (model) => {
802
849
  const session = showPicker({
803
850
  columns: model.columns(),
804
851
  current: model.current(),
805
- headerText,
806
852
  onColumnChange(column, index) {
807
853
  model.select(column, index);
808
854
  const columns = model.columns();
@@ -816,8 +862,7 @@
816
862
  showPickerView: async (args, { api }) => {
817
863
  const result = await showPicker({
818
864
  columns: [labels(args.array)],
819
- current: [Number(args.current) || 0],
820
- headerText: args.headerText
865
+ current: [Number(args.current) || 0]
821
866
  }).result;
822
867
  return result ? ok(api, { index: result[0] }) : fail(api, "cancel");
823
868
  },
@@ -825,7 +870,6 @@
825
870
  const session = showPicker({
826
871
  columns: (args.array || []).map(labels),
827
872
  current: (args.current || []).map((index) => Number(index) || 0),
828
- headerText: args.headerText,
829
873
  onColumnChange: (column, current) => emitToWebview(webviewId, "onMultiPickerViewChange", { column, current })
830
874
  });
831
875
  multiPicker = session;
@@ -840,7 +884,7 @@
840
884
  },
841
885
  showDatePickerView: async (args, { api }) => {
842
886
  const model = new DateCascade(args.mode === "time" ? "time" : "date", args.fields, args.range || {}, String(args.current || ""));
843
- const result = await showCascade(model, args.headerText);
887
+ const result = await showCascade(model);
844
888
  return result ? ok(api, { value: model.value() }) : fail(api, "cancel");
845
889
  },
846
890
  showAddressPickerView: async (args, { api }) => {
@@ -920,7 +964,7 @@
920
964
  }
921
965
  label(column, value) {
922
966
  if (this.mode === "time") return pad(value);
923
- return value + ["\u5E74", "\u6708", "\u65E5"][column];
967
+ return column === 0 ? value + "\u5E74" : pad(value) + ["\u6708", "\u65E5"][column - 1];
924
968
  }
925
969
  };
926
970
  function parseParts(text) {
@@ -1257,6 +1301,7 @@
1257
1301
  names.add(normalizeName(pkg.root));
1258
1302
  if (pkg.name) names.add(normalizeName(pkg.name));
1259
1303
  }
1304
+ if (typeof options.workers === "object" && options.workers.isSubpackage) names.add("workers.js");
1260
1305
  let taskSeq2 = 0;
1261
1306
  const createLoadSubPackageTask = (args, { api }) => {
1262
1307
  const moduleName = args.moduleName;
@@ -1578,6 +1623,18 @@
1578
1623
  if (this.bridge && this.win) fn(this.bridge, this.win);
1579
1624
  else this.pending.push(fn);
1580
1625
  }
1626
+ /**
1627
+ * 把逻辑层的画布放进这一页编号为 canvasNumber 的 wx-canvas,盖在它自己的 canvas 上(样式见 webviewHtml 的 .wxa-service-canvas)。
1628
+ * 编号在 wx-canvas 的组件实例上,DOM 节点经 __wxElement 找到组件实例
1629
+ */
1630
+ placeCanvas(canvas, canvasNumber) {
1631
+ const target = Array.from(this.win?.document.querySelectorAll("wx-canvas") ?? []).find((element2) => element2.__wxElement?._canvasNumber === canvasNumber);
1632
+ const own = target?.querySelector(":scope > canvas");
1633
+ if (!own) return false;
1634
+ canvas.classList.add("wxa-service-canvas");
1635
+ own.after(canvas);
1636
+ return true;
1637
+ }
1581
1638
  /** 页面样式里的 env(safe-area-inset-*) 构建时已改写成这些变量,见 rewriteSafeAreaInsets */
1582
1639
  setSafeAreaInsets(insets) {
1583
1640
  this.dispatch((_bridge, win) => {
@@ -1694,7 +1751,7 @@
1694
1751
  var DEVICE_BEZEL = 13;
1695
1752
  var STAGE_PADDING = 24;
1696
1753
  var STAGE_GAP = 16;
1697
- var PICKER_ITEM_HEIGHT = 34;
1754
+ var PICKER_ITEM_HEIGHT = 48;
1698
1755
  var SHELL_CSS = `
1699
1756
  html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50% 0%, #f6f7f9 0%, #e6e9ee 100%); font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; }
1700
1757
  .wxa-stage { min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: ${STAGE_GAP}px; padding: ${STAGE_PADDING}px; box-sizing: border-box; }
@@ -1748,18 +1805,23 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1748
1805
  .wxa-actionsheet-alert { padding: 16px; font-size: 13px; color: #888; text-align: center; background: #fff; }
1749
1806
  .wxa-actionsheet button { display: block; width: 100%; height: 56px; border: 0; border-top: 1px solid rgba(0,0,0,.06); background: #fff; font-size: 17px; cursor: pointer; }
1750
1807
  .wxa-actionsheet .wxa-actionsheet-cancel { margin-top: 8px; border-top: 0; }
1751
- .wxa-picker { position: absolute; left: 0; right: 0; bottom: 0; padding-bottom: var(--wxa-safe-bottom, 0px); background: #fff; border-radius: 12px 12px 0 0; pointer-events: auto; user-select: none; }
1752
- .wxa-picker-header { display: flex; align-items: center; height: 56px; padding: 0 8px; }
1753
- .wxa-picker-header button { flex: none; border: 0; background: none; padding: 8px 12px; font-size: 17px; cursor: pointer; }
1754
- .wxa-picker-cancel { color: rgba(0,0,0,.9); }
1755
- .wxa-picker-confirm { color: #07c160; font-weight: 600; }
1756
- .wxa-picker-title { flex: 1; overflow: hidden; font-size: 17px; font-weight: 600; text-align: center; white-space: nowrap; text-overflow: ellipsis; }
1757
- .wxa-picker-body { position: relative; display: flex; height: ${PICKER_ITEM_HEIGHT * 5}px; margin: 0 16px 24px; }
1808
+ .wxa-picker-layer .wxa-mask.is-dim { background: rgba(0,0,0,.6); animation: wxa-fade-in .3s ease forwards; }
1809
+ .wxa-picker { position: absolute; left: 0; right: 0; bottom: 0; padding-bottom: var(--wxa-safe-bottom, 0px); background: #fff; pointer-events: auto; user-select: none; animation: wxa-slide-up .3s ease forwards; }
1810
+ .wxa-picker-header { position: relative; display: flex; padding: 16px; font-size: 17px; line-height: 1.4; }
1811
+ .wxa-picker-header::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; border-bottom: 1px solid rgba(0,0,0,.1); transform: scaleY(.5); transform-origin: 0 100%; }
1812
+ .wxa-picker-header button { flex: 1; padding: 0; border: 0; background: none; font: inherit; cursor: pointer; }
1813
+ .wxa-picker-cancel { color: rgba(0,0,0,.5); text-align: left; }
1814
+ .wxa-picker-confirm { color: #07c160; text-align: right; }
1815
+ .wxa-picker-body { position: relative; display: flex; height: ${PICKER_ITEM_HEIGHT * 5}px; overflow: hidden; }
1758
1816
  .wxa-picker-column { flex: 1; min-width: 0; overflow-y: auto; padding: ${PICKER_ITEM_HEIGHT * 2}px 0; box-sizing: border-box; scroll-snap-type: y mandatory; overscroll-behavior: contain; scrollbar-width: none; }
1759
1817
  .wxa-picker-column::-webkit-scrollbar { display: none; }
1760
- .wxa-picker-item { height: ${PICKER_ITEM_HEIGHT}px; line-height: ${PICKER_ITEM_HEIGHT}px; padding: 0 4px; overflow: hidden; font-size: 17px; color: rgba(0,0,0,.4); text-align: center; white-space: nowrap; text-overflow: ellipsis; scroll-snap-align: center; cursor: pointer; }
1761
- .wxa-picker-item.is-selected { color: rgba(0,0,0,.9); }
1762
- .wxa-picker-indicator { position: absolute; left: 0; right: 0; top: ${PICKER_ITEM_HEIGHT * 2}px; height: ${PICKER_ITEM_HEIGHT}px; border-radius: 8px; background: rgba(0,0,0,.05); pointer-events: none; }
1818
+ /* \u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u9009\u9879\u6CA1\u6709\u5355\u72EC\u8BBE\u7F6E\u5B57\u53F7\uFF0C\u7EE7\u627F\u5230\u7684\u662F 11px\uFF08\u6309\u5B83\u7684\u622A\u56FE\u91CF\u5F97\uFF09 */
1819
+ .wxa-picker-item { height: ${PICKER_ITEM_HEIGHT}px; line-height: ${PICKER_ITEM_HEIGHT}px; overflow: hidden; font-size: 11px; color: rgba(0,0,0,.9); text-align: center; white-space: nowrap; text-overflow: ellipsis; scroll-snap-align: center; cursor: pointer; }
1820
+ .wxa-picker-mask { position: absolute; inset: 0; z-index: 1; pointer-events: none; background-image: linear-gradient(180deg, rgba(255,255,255,.95), rgba(255,255,255,.6)), linear-gradient(0deg, rgba(255,255,255,.95), rgba(255,255,255,.6)); background-position: top, bottom; background-size: 100% 92px; background-repeat: no-repeat; }
1821
+ .wxa-picker-indicator { position: absolute; left: 0; right: 0; top: 92px; z-index: 1; height: 56px; pointer-events: none; }
1822
+ .wxa-picker-indicator::before, .wxa-picker-indicator::after { content: ''; position: absolute; left: 0; right: 0; height: 1px; transform: scaleY(.5); }
1823
+ .wxa-picker-indicator::before { top: 0; border-top: 1px solid rgba(0,0,0,.1); transform-origin: 0 0; }
1824
+ .wxa-picker-indicator::after { bottom: 0; border-bottom: 1px solid rgba(0,0,0,.1); transform-origin: 0 100%; }
1763
1825
  .wxa-preview { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #000; pointer-events: auto; }
1764
1826
  .wxa-preview-image { max-width: 100%; max-height: 100%; object-fit: contain; }
1765
1827
  .wxa-preview-position { position: absolute; top: 60px; left: 0; right: 0; color: #fff; font-size: 15px; text-align: center; pointer-events: none; }
@@ -1778,6 +1840,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1778
1840
  .wxa-notice-close { flex: none; width: 20px; height: 20px; padding: 0; border: 0; border-radius: 50%; background: none; color: #999; font-size: 16px; line-height: 20px; cursor: pointer; }
1779
1841
  .wxa-notice-close:hover { background: rgba(0,0,0,.06); color: #111214; }
1780
1842
  @keyframes wxa-spin { to { transform: rotate(360deg); } }
1843
+ @keyframes wxa-fade-in { from { opacity: 0; } }
1844
+ @keyframes wxa-slide-up { from { transform: translateY(100%); } }
1781
1845
  @keyframes wxa-notice-in { from { opacity: 0; transform: translateY(-8px); } }
1782
1846
  `;
1783
1847
 
@@ -1795,10 +1859,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1795
1859
  const header = element("div", "wxa-picker-header");
1796
1860
  const cancel = element("button", "wxa-picker-cancel", "\u53D6\u6D88");
1797
1861
  const confirm = element("button", "wxa-picker-confirm", "\u786E\u5B9A");
1798
- header.append(cancel, element("div", "wxa-picker-title", options.headerText || ""), confirm);
1862
+ header.append(cancel, confirm);
1799
1863
  const body = element("div", "wxa-picker-body");
1800
1864
  const columns = options.columns.map((_, index) => new PickerColumn((next) => options.onColumnChange?.(index, next)));
1801
- body.append(...columns.map((column) => column.el), element("div", "wxa-picker-indicator"));
1865
+ body.append(...columns.map((column) => column.el), element("div", "wxa-picker-mask"), element("div", "wxa-picker-indicator"));
1802
1866
  sheet.append(header, body);
1803
1867
  layer.append(mask, sheet);
1804
1868
  container.appendChild(layer);
@@ -2200,6 +2264,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2200
2264
  // src/runtime/host/Host.ts
2201
2265
  var ROUTE_DONE_DELAY = 300;
2202
2266
  var PAGE_LOAD_TIMEOUT = 15e3;
2267
+ var BINDING_CANVAS_TYPES = /* @__PURE__ */ new Set(["2d", "webgl", "webgl2"]);
2268
+ var PROBE_TOP = -54321;
2203
2269
  var Host = class {
2204
2270
  constructor(info, root) {
2205
2271
  this.info = info;
@@ -2222,6 +2288,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2222
2288
  this.nextWebviewId = 1;
2223
2289
  this.routeChain = Promise.resolve();
2224
2290
  this.routeListeners = [];
2291
+ /** 渲染层报来的 2d、webgl 画布,按编号记下所在页面和最近的位置,供 placeCanvas 认出逻辑层的画布元素 */
2292
+ this.canvases = /* @__PURE__ */ new Map();
2225
2293
  this.device = info.device;
2226
2294
  this.shell = new PhoneShell(root, info.device);
2227
2295
  this.tabPaths = new Set((info.wxConfig.tabBar?.list || []).map((item) => stripHtml(item.pagePath)));
@@ -2260,6 +2328,30 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2260
2328
  registerAppContext(win) {
2261
2329
  this.appContext = win;
2262
2330
  }
2331
+ /**
2332
+ * 逻辑层的画布元素上没有编号,只能问基础库:给每个还没放好的同类型画布发一条 top 带记号的 canvasUpdated,
2333
+ * 基础库把位置写到那个编号的元素上,记号落在这个元素上就是它;随后用原来的位置再发一次,恢复原样。
2334
+ * 基础库只在类型与画布一致时才调到元素的 getContext,按类型筛选不会漏掉
2335
+ */
2336
+ placeCanvas(canvas, type) {
2337
+ const service = this.service;
2338
+ if (!service) return;
2339
+ const update = (canvasNumber, record, top) => service.bridge.__dispatchSub(
2340
+ "canvasUpdated",
2341
+ this.cloneForService({ data: { canvasNumber, ...record.box, top }, options: { timestamp: Date.now() } }),
2342
+ record.webviewId
2343
+ );
2344
+ for (const [canvasNumber, record] of this.canvases) {
2345
+ if (record.placed || record.type !== type) continue;
2346
+ update(canvasNumber, record, PROBE_TOP);
2347
+ const matched = canvas.style.top === `${PROBE_TOP}px`;
2348
+ update(canvasNumber, record, record.box.top);
2349
+ if (matched) {
2350
+ record.placed = this.pages.get(record.webviewId)?.placeCanvas(canvas, canvasNumber) ?? false;
2351
+ return;
2352
+ }
2353
+ }
2354
+ }
2263
2355
  frameLoaded(kind, webviewId) {
2264
2356
  if (kind === "service") this.serviceLoaded();
2265
2357
  else this.pages.get(webviewId)?.loaded();
@@ -2283,11 +2375,27 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2283
2375
  }
2284
2376
  if (event === "operateMapCallback" && this.mapOperations.receive(data)) return;
2285
2377
  const fromId = this.webviewIdOf(win);
2378
+ this.trackCanvas(event, data?.data, fromId);
2286
2379
  const service = this.service;
2287
2380
  if (!service) return;
2288
2381
  const payload = this.cloneForService(data);
2289
2382
  queueMicrotask(() => service.bridge.__dispatchSub(event, payload, fromId));
2290
2383
  }
2384
+ /**
2385
+ * xr-frame 的画布不记:基础库收到它的 canvasUpdated 会派发 resize 给 xr-frame,探测会带来多余的 resize,
2386
+ * 而 xr-frame 在 wxa-runner 里没有验证过,保持原样
2387
+ */
2388
+ trackCanvas(event, data, webviewId) {
2389
+ if (!data) return;
2390
+ const record = this.canvases.get(data.canvasNumber);
2391
+ if (event === "canvasInserted" && data.type && BINDING_CANVAS_TYPES.has(data.type) && !data.isXrFrame) {
2392
+ this.canvases.set(data.canvasNumber, { webviewId, type: data.type, box: { top: 0, left: 0, width: 0, height: 0, ...boxOf(data) }, placed: false });
2393
+ } else if (event === "canvasUpdated" && record) {
2394
+ record.box = { ...record.box, ...boxOf(data) };
2395
+ } else if (event === "canvasRemoved") {
2396
+ this.canvases.delete(data.canvasNumber);
2397
+ }
2398
+ }
2291
2399
  /** 不指定目标时发给所有页面:旧版 canvas 的绘制指令就是这样发的,由 canvasNumber 找到对应画布 */
2292
2400
  publishToPages(event, data, webviewIds) {
2293
2401
  const targets = webviewIds?.length ? webviewIds.map((id) => this.pages.get(Number(id))) : this.livePages();
@@ -2445,6 +2553,9 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2445
2553
  if (this.tabPages.get(page.path) === page) this.tabPages.delete(page.path);
2446
2554
  this.pages.delete(page.id);
2447
2555
  page.destroy();
2556
+ for (const [canvasNumber, record] of this.canvases) {
2557
+ if (record.webviewId === page.id) this.canvases.delete(canvasNumber);
2558
+ }
2448
2559
  }
2449
2560
  activate(page, openType) {
2450
2561
  for (const other of this.pages.values()) {
@@ -2537,6 +2648,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2537
2648
  ...createStorageApis(this.storage),
2538
2649
  ...createSubpackageApis({
2539
2650
  subPackages: this.info.wxConfig.subPackages || [],
2651
+ workers: this.info.wxConfig.workers,
2540
2652
  emit: (event, data) => this.emitToService(event, data)
2541
2653
  }),
2542
2654
  ...createSavedFileApis(this.storage),
@@ -2593,6 +2705,13 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2593
2705
  }
2594
2706
  };
2595
2707
  }
2708
+ function boxOf(data) {
2709
+ const box = {};
2710
+ for (const key of ["top", "left", "width", "height"]) {
2711
+ if (data[key] !== void 0) box[key] = data[key];
2712
+ }
2713
+ return box;
2714
+ }
2596
2715
  function stripHtml(path) {
2597
2716
  return path.replace(/\.html$/, "");
2598
2717
  }