@zumer/snapdom 1.9.10-dev.1 → 1.9.10-dev.2
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/README.md +32 -3
- package/dist/snapdom.js +568 -185
- package/dist/snapdom.min.js +4 -4
- package/dist/snapdom.min.mjs +5 -5
- package/dist/snapdom.mjs +419 -46
- package/package.json +1 -1
- package/types/snapdom.d.ts +15 -1
package/dist/snapdom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
/*
|
|
3
3
|
* snapdom
|
|
4
|
-
* v.1.9.10-dev.
|
|
4
|
+
* v.1.9.10-dev.2
|
|
5
5
|
* Author Juan Martin Muda
|
|
6
6
|
* License MIT
|
|
7
7
|
**/
|
|
@@ -42,10 +42,13 @@ function applyCachePolicy(policy = "soft") {
|
|
|
42
42
|
cache.session.styleMap = /* @__PURE__ */ new Map();
|
|
43
43
|
cache.session.nodeMap = /* @__PURE__ */ new Map();
|
|
44
44
|
cache.session.styleCache = /* @__PURE__ */ new WeakMap();
|
|
45
|
-
cache.
|
|
46
|
-
cache.
|
|
47
|
-
cache.
|
|
48
|
-
cache.
|
|
45
|
+
cache.computedStyle = /* @__PURE__ */ new WeakMap();
|
|
46
|
+
cache.baseStyle = /* @__PURE__ */ new Map();
|
|
47
|
+
cache.defaultStyle = /* @__PURE__ */ new Map();
|
|
48
|
+
cache.image = /* @__PURE__ */ new Map();
|
|
49
|
+
cache.background = /* @__PURE__ */ new Map();
|
|
50
|
+
cache.resource = /* @__PURE__ */ new Map();
|
|
51
|
+
cache.font = /* @__PURE__ */ new Set();
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
default: {
|
|
@@ -637,6 +640,11 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
|
637
640
|
const ctx = _resolveCtx(sessionOrCtx, opts);
|
|
638
641
|
const resetMode = ctx.options && ctx.options.cache || "auto";
|
|
639
642
|
if (resetMode !== "disabled") setupInvalidationOnce(document.documentElement);
|
|
643
|
+
if (resetMode === "disabled" && !ctx.session.__bumpedForDisabled) {
|
|
644
|
+
bumpEpoch();
|
|
645
|
+
snapshotKeyCache.clear();
|
|
646
|
+
ctx.session.__bumpedForDisabled = true;
|
|
647
|
+
}
|
|
640
648
|
if (NO_DEFAULTS_TAGS.has(source.tagName?.toLowerCase())) {
|
|
641
649
|
const author = source.getAttribute?.("style");
|
|
642
650
|
if (author) clone.setAttribute("style", author);
|
|
@@ -658,6 +666,31 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
|
|
|
658
666
|
}
|
|
659
667
|
|
|
660
668
|
// src/core/clone.js
|
|
669
|
+
function idleCallback(childList, callback, fast) {
|
|
670
|
+
return Promise.all(childList.map((child) => {
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
function deal() {
|
|
673
|
+
idle(
|
|
674
|
+
(deadline) => {
|
|
675
|
+
if (deadline instanceof IdleDeadline) {
|
|
676
|
+
if (deadline.timeRemaining()) {
|
|
677
|
+
callback(child, resolve);
|
|
678
|
+
} else {
|
|
679
|
+
deal();
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
callback(child, resolve);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
fast
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
deal();
|
|
691
|
+
});
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
661
694
|
function addNotSlottedRightmost(sel) {
|
|
662
695
|
sel = sel.trim();
|
|
663
696
|
if (!sel) return sel;
|
|
@@ -779,10 +812,84 @@ function markSlottedSubtree(root) {
|
|
|
779
812
|
root.querySelectorAll("*").forEach((el) => el.setAttribute("data-sd-slotted", ""));
|
|
780
813
|
}
|
|
781
814
|
}
|
|
782
|
-
function
|
|
815
|
+
async function getAccessibleIframeDocument(iframe, attempts = 3) {
|
|
816
|
+
const probe = () => {
|
|
817
|
+
try {
|
|
818
|
+
return iframe.contentDocument || iframe.contentWindow?.document || null;
|
|
819
|
+
} catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
let doc = probe();
|
|
824
|
+
let i = 0;
|
|
825
|
+
while (i < attempts && (!doc || !doc.body && !doc.documentElement)) {
|
|
826
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
827
|
+
doc = probe();
|
|
828
|
+
i++;
|
|
829
|
+
}
|
|
830
|
+
return doc && (doc.body || doc.documentElement) ? doc : null;
|
|
831
|
+
}
|
|
832
|
+
function measureContentBox(el) {
|
|
833
|
+
const rect = el.getBoundingClientRect();
|
|
834
|
+
let bl = 0, br = 0, bt = 0, bb = 0;
|
|
835
|
+
try {
|
|
836
|
+
const cs = getComputedStyle(el);
|
|
837
|
+
bl = parseFloat(cs.borderLeftWidth) || 0;
|
|
838
|
+
br = parseFloat(cs.borderRightWidth) || 0;
|
|
839
|
+
bt = parseFloat(cs.borderTopWidth) || 0;
|
|
840
|
+
bb = parseFloat(cs.borderBottomWidth) || 0;
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
const contentWidth = Math.max(0, Math.round(rect.width - (bl + br)));
|
|
844
|
+
const contentHeight = Math.max(0, Math.round(rect.height - (bt + bb)));
|
|
845
|
+
return { contentWidth, contentHeight, rect };
|
|
846
|
+
}
|
|
847
|
+
function pinIframeViewport(doc, w, h) {
|
|
848
|
+
const style = doc.createElement("style");
|
|
849
|
+
style.setAttribute("data-sd-iframe-pin", "");
|
|
850
|
+
style.textContent = `html, body {margin: 0 !important;padding: 0 !important;width: ${w}px !important;height: ${h}px !important;min-width: ${w}px !important;min-height: ${h}px !important;box-sizing: border-box !important;overflow: hidden !important;background-clip: border-box !important;}`;
|
|
851
|
+
(doc.head || doc.documentElement).appendChild(style);
|
|
852
|
+
return () => {
|
|
853
|
+
try {
|
|
854
|
+
style.remove();
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function rasterizeIframe(iframe, sessionCache, options) {
|
|
860
|
+
const doc = await getAccessibleIframeDocument(iframe, 3);
|
|
861
|
+
if (!doc) throw new Error("iframe document not accessible/ready");
|
|
862
|
+
const { contentWidth, contentHeight, rect } = measureContentBox(iframe);
|
|
863
|
+
const snap = options?.snap;
|
|
864
|
+
if (!snap || typeof snap.toPng !== "function") {
|
|
865
|
+
throw new Error("snapdom.toPng not available in iframe or window");
|
|
866
|
+
}
|
|
867
|
+
const nested = { ...options, scale: 1 };
|
|
868
|
+
const unpin = pinIframeViewport(doc, contentWidth, contentHeight);
|
|
869
|
+
let imgEl;
|
|
870
|
+
try {
|
|
871
|
+
imgEl = await snap.toPng(doc.documentElement, nested);
|
|
872
|
+
} finally {
|
|
873
|
+
unpin();
|
|
874
|
+
}
|
|
875
|
+
imgEl.style.display = "block";
|
|
876
|
+
imgEl.style.width = `${contentWidth}px`;
|
|
877
|
+
imgEl.style.height = `${contentHeight}px`;
|
|
878
|
+
const wrapper = document.createElement("div");
|
|
879
|
+
sessionCache.nodeMap.set(wrapper, iframe);
|
|
880
|
+
inlineAllStyles(iframe, wrapper, sessionCache, options);
|
|
881
|
+
wrapper.style.overflow = "hidden";
|
|
882
|
+
wrapper.style.display = "block";
|
|
883
|
+
if (!wrapper.style.width) wrapper.style.width = `${Math.round(rect.width)}px`;
|
|
884
|
+
if (!wrapper.style.height) wrapper.style.height = `${Math.round(rect.height)}px`;
|
|
885
|
+
wrapper.appendChild(imgEl);
|
|
886
|
+
return wrapper;
|
|
887
|
+
}
|
|
888
|
+
async function deepClone(node, sessionCache, options) {
|
|
783
889
|
if (!node) throw new Error("Invalid node");
|
|
784
890
|
const clonedAssignedNodes = /* @__PURE__ */ new Set();
|
|
785
891
|
let pendingSelectValue = null;
|
|
892
|
+
let pendingTextAreaValue = null;
|
|
786
893
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
787
894
|
const tag = (node.localName || node.tagName || "").toLowerCase();
|
|
788
895
|
if (node.id === "snapdom-sandbox" || node.hasAttribute("data-snapdom-sandbox")) {
|
|
@@ -831,6 +938,20 @@ function deepClone(node, sessionCache, options) {
|
|
|
831
938
|
}
|
|
832
939
|
}
|
|
833
940
|
if (node.tagName === "IFRAME") {
|
|
941
|
+
let sameOrigin = false;
|
|
942
|
+
try {
|
|
943
|
+
sameOrigin = !!(node.contentDocument || node.contentWindow?.document);
|
|
944
|
+
} catch {
|
|
945
|
+
sameOrigin = false;
|
|
946
|
+
}
|
|
947
|
+
if (sameOrigin) {
|
|
948
|
+
try {
|
|
949
|
+
const wrapper = await rasterizeIframe(node, sessionCache, options);
|
|
950
|
+
return wrapper;
|
|
951
|
+
} catch (err) {
|
|
952
|
+
console.warn("[SnapDOM] iframe rasterization failed, fallback:", err);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
834
955
|
if (options.placeholders) {
|
|
835
956
|
const fallback = document.createElement("div");
|
|
836
957
|
fallback.style.cssText = `width:${node.offsetWidth}px;height:${node.offsetHeight}px;background-image:repeating-linear-gradient(45deg,#ddd,#ddd 5px,#f9f9f9 5px,#f9f9f9 10px);display:flex;align-items:center;justify-content:center;font-size:12px;color:#555;border:1px solid #aaa;`;
|
|
@@ -870,14 +991,31 @@ function deepClone(node, sessionCache, options) {
|
|
|
870
991
|
sessionCache.nodeMap.set(clone, node);
|
|
871
992
|
if (node.tagName === "IMG") {
|
|
872
993
|
freezeImgSrcset(node, clone);
|
|
994
|
+
try {
|
|
995
|
+
const rect = node.getBoundingClientRect();
|
|
996
|
+
let w = Math.round(rect.width || 0);
|
|
997
|
+
let h = Math.round(rect.height || 0);
|
|
998
|
+
if (!w || !h) {
|
|
999
|
+
const computed = window.getComputedStyle(node);
|
|
1000
|
+
const cssW = parseFloat(computed.width) || 0;
|
|
1001
|
+
const cssH = parseFloat(computed.height) || 0;
|
|
1002
|
+
const attrW = parseInt(node.getAttribute("width") || "", 10) || 0;
|
|
1003
|
+
const attrH = parseInt(node.getAttribute("height") || "", 10) || 0;
|
|
1004
|
+
const propW = node.width || node.naturalWidth || 0;
|
|
1005
|
+
const propH = node.height || node.naturalHeight || 0;
|
|
1006
|
+
w = Math.round(w || cssW || attrW || propW || 0);
|
|
1007
|
+
h = Math.round(h || cssH || attrH || propH || 0);
|
|
1008
|
+
}
|
|
1009
|
+
if (w) clone.dataset.snapdomWidth = String(w);
|
|
1010
|
+
if (h) clone.dataset.snapdomHeight = String(h);
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
873
1013
|
}
|
|
874
1014
|
} catch (err) {
|
|
875
1015
|
console.error("[Snapdom] Failed to clone node:", node, err);
|
|
876
1016
|
throw err;
|
|
877
1017
|
}
|
|
878
1018
|
if (node instanceof HTMLTextAreaElement) {
|
|
879
|
-
clone.textContent = node.value;
|
|
880
|
-
clone.value = node.value;
|
|
881
1019
|
const rect = node.getBoundingClientRect();
|
|
882
1020
|
clone.style.width = `${rect.width}px`;
|
|
883
1021
|
clone.style.height = `${rect.height}px`;
|
|
@@ -894,8 +1032,22 @@ function deepClone(node, sessionCache, options) {
|
|
|
894
1032
|
if (node instanceof HTMLSelectElement) {
|
|
895
1033
|
pendingSelectValue = node.value;
|
|
896
1034
|
}
|
|
1035
|
+
if (node instanceof HTMLTextAreaElement) {
|
|
1036
|
+
pendingTextAreaValue = node.value;
|
|
1037
|
+
}
|
|
897
1038
|
inlineAllStyles(node, clone, sessionCache, options);
|
|
898
1039
|
if (node.shadowRoot) {
|
|
1040
|
+
let callback2 = function(child, resolve) {
|
|
1041
|
+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
|
|
1042
|
+
return resolve(null);
|
|
1043
|
+
} else {
|
|
1044
|
+
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1045
|
+
resolve(clonedChild || null);
|
|
1046
|
+
}).catch((e) => {
|
|
1047
|
+
resolve(null);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
899
1051
|
try {
|
|
900
1052
|
const slots = node.shadowRoot.querySelectorAll("slot");
|
|
901
1053
|
for (const s of slots) {
|
|
@@ -921,31 +1073,38 @@ function deepClone(node, sessionCache, options) {
|
|
|
921
1073
|
const seed = buildSeedCustomPropsRule(node, neededVars, scopeSelector);
|
|
922
1074
|
injectScopedStyle(clone, seed + rewritten, scopeId);
|
|
923
1075
|
const shadowFrag = document.createDocumentFragment();
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const clonedChild = deepClone(child, sessionCache, options);
|
|
927
|
-
if (clonedChild) shadowFrag.appendChild(clonedChild);
|
|
928
|
-
}
|
|
1076
|
+
const cloneList2 = await idleCallback(Array.from(node.shadowRoot.childNodes), callback2, options.fast);
|
|
1077
|
+
shadowFrag.append(...cloneList2.filter((clonedChild) => !!clonedChild));
|
|
929
1078
|
clone.appendChild(shadowFrag);
|
|
930
1079
|
}
|
|
931
1080
|
if (node.tagName === "SLOT") {
|
|
1081
|
+
let callback2 = function(child, resolve) {
|
|
1082
|
+
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1083
|
+
if (clonedChild) {
|
|
1084
|
+
markSlottedSubtree(clonedChild);
|
|
1085
|
+
}
|
|
1086
|
+
resolve(clonedChild || null);
|
|
1087
|
+
}).catch((e) => {
|
|
1088
|
+
resolve(null);
|
|
1089
|
+
});
|
|
1090
|
+
};
|
|
932
1091
|
const assigned = node.assignedNodes?.({ flatten: true }) || [];
|
|
933
1092
|
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
|
|
934
1093
|
const fragment = document.createDocumentFragment();
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (clonedChild) {
|
|
938
|
-
markSlottedSubtree(clonedChild);
|
|
939
|
-
fragment.appendChild(clonedChild);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
1094
|
+
const cloneList2 = await idleCallback(Array.from(nodesToClone), callback2, options.fast);
|
|
1095
|
+
fragment.append(...cloneList2.filter((clonedChild) => !!clonedChild));
|
|
942
1096
|
return fragment;
|
|
943
1097
|
}
|
|
944
|
-
|
|
945
|
-
if (clonedAssignedNodes.has(child))
|
|
946
|
-
|
|
947
|
-
|
|
1098
|
+
function callback(child, resolve) {
|
|
1099
|
+
if (clonedAssignedNodes.has(child)) return resolve(null);
|
|
1100
|
+
deepClone(child, sessionCache, options).then((clonedChild) => {
|
|
1101
|
+
resolve(clonedChild || null);
|
|
1102
|
+
}).catch((e) => {
|
|
1103
|
+
resolve(null);
|
|
1104
|
+
});
|
|
948
1105
|
}
|
|
1106
|
+
const cloneList = await idleCallback(Array.from(node.childNodes), callback, options.fast);
|
|
1107
|
+
clone.append(...cloneList.filter((clonedChild) => !!clonedChild));
|
|
949
1108
|
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
|
|
950
1109
|
clone.value = pendingSelectValue;
|
|
951
1110
|
for (const opt of clone.options) {
|
|
@@ -956,6 +1115,9 @@ function deepClone(node, sessionCache, options) {
|
|
|
956
1115
|
}
|
|
957
1116
|
}
|
|
958
1117
|
}
|
|
1118
|
+
if (pendingTextAreaValue !== null && clone instanceof HTMLTextAreaElement) {
|
|
1119
|
+
clone.textContent = pendingTextAreaValue;
|
|
1120
|
+
}
|
|
959
1121
|
return clone;
|
|
960
1122
|
}
|
|
961
1123
|
|
|
@@ -1826,7 +1988,7 @@ async function prepareClone(element, options = {}) {
|
|
|
1826
1988
|
console.warn("inlineExternal defs or symbol failed:", e);
|
|
1827
1989
|
}
|
|
1828
1990
|
try {
|
|
1829
|
-
clone = deepClone(element, sessionCache, options, element);
|
|
1991
|
+
clone = await deepClone(element, sessionCache, options, element);
|
|
1830
1992
|
} catch (e) {
|
|
1831
1993
|
console.warn("deepClone failed:", e);
|
|
1832
1994
|
throw e;
|
|
@@ -2064,6 +2226,30 @@ async function inlineImages(clone, options = {}) {
|
|
|
2064
2226
|
if (!img.height) img.height = img.naturalHeight || 100;
|
|
2065
2227
|
return;
|
|
2066
2228
|
}
|
|
2229
|
+
const { defaultImageUrl } = options || {};
|
|
2230
|
+
if (defaultImageUrl) {
|
|
2231
|
+
try {
|
|
2232
|
+
const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
|
|
2233
|
+
const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
|
|
2234
|
+
const attrW = parseInt(img.getAttribute("width") || "", 10) || 0;
|
|
2235
|
+
const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
|
|
2236
|
+
const styleW = parseFloat(img.style?.width || "") || 0;
|
|
2237
|
+
const styleH = parseFloat(img.style?.height || "") || 0;
|
|
2238
|
+
const width = dsW || styleW || attrW || img.width || void 0;
|
|
2239
|
+
const height = dsH || styleH || attrH || img.height || void 0;
|
|
2240
|
+
const fallbackUrl = typeof defaultImageUrl === "function" ? await defaultImageUrl({ width, height, src, element: img }) : defaultImageUrl;
|
|
2241
|
+
if (fallbackUrl) {
|
|
2242
|
+
const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
|
|
2243
|
+
img.src = fallbackData.data;
|
|
2244
|
+
if (!img.width && width) img.width = width;
|
|
2245
|
+
if (!img.height && height) img.height = height;
|
|
2246
|
+
if (!img.width) img.width = img.naturalWidth || 100;
|
|
2247
|
+
if (!img.height) img.height = img.naturalHeight || 100;
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2067
2253
|
const w = img.width || img.naturalWidth || 100;
|
|
2068
2254
|
const h = img.height || img.naturalHeight || 100;
|
|
2069
2255
|
if (options.placeholders !== false) {
|
|
@@ -2177,7 +2363,13 @@ async function captureDOM(element, options) {
|
|
|
2177
2363
|
);
|
|
2178
2364
|
await ensureFontsReady(families, 2);
|
|
2179
2365
|
}
|
|
2180
|
-
fontsCSS = await embedCustomFonts({
|
|
2366
|
+
fontsCSS = await embedCustomFonts({
|
|
2367
|
+
required,
|
|
2368
|
+
usedCodepoints,
|
|
2369
|
+
preCached: false,
|
|
2370
|
+
exclude: options.excludeFonts,
|
|
2371
|
+
useProxy: options.useProxy
|
|
2372
|
+
});
|
|
2181
2373
|
resolve();
|
|
2182
2374
|
}, { fast });
|
|
2183
2375
|
});
|
|
@@ -2197,12 +2389,13 @@ async function captureDOM(element, options) {
|
|
|
2197
2389
|
}
|
|
2198
2390
|
await new Promise((resolve) => {
|
|
2199
2391
|
idle(() => {
|
|
2200
|
-
const rect = element
|
|
2392
|
+
const rect = getNaturalBorderBoxSize(element);
|
|
2201
2393
|
let w = rect.width;
|
|
2202
2394
|
let h = rect.height;
|
|
2203
2395
|
const hasW = Number.isFinite(options.width);
|
|
2204
2396
|
const hasH = Number.isFinite(options.height);
|
|
2205
|
-
const
|
|
2397
|
+
const scaleOpt = options.scale ?? 1;
|
|
2398
|
+
const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
|
|
2206
2399
|
if (!hasScale) {
|
|
2207
2400
|
const aspect = rect.width / rect.height;
|
|
2208
2401
|
if (hasW && hasH) {
|
|
@@ -2216,30 +2409,79 @@ async function captureDOM(element, options) {
|
|
|
2216
2409
|
w = h * aspect;
|
|
2217
2410
|
}
|
|
2218
2411
|
}
|
|
2219
|
-
w = Math.ceil(w);
|
|
2220
|
-
h = Math.ceil(h);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2412
|
+
w = Math.max(1, Math.ceil(w));
|
|
2413
|
+
h = Math.max(1, Math.ceil(h));
|
|
2414
|
+
const csEl = getComputedStyle(element);
|
|
2415
|
+
const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
|
|
2416
|
+
const ind = readIndividualTransforms(element);
|
|
2417
|
+
let outW, outH, tx, ty, cancelTranslateX = 0, cancelTranslateY = 0;
|
|
2418
|
+
if (!hasBBoxAffectingTransform(element)) {
|
|
2419
|
+
const r = element.getBoundingClientRect();
|
|
2420
|
+
const vp = inflateRect(r, { top: 0, right: 0, bottom: 0, left: 0 });
|
|
2421
|
+
outW = Math.max(1, vp.width);
|
|
2422
|
+
outH = Math.max(1, vp.height);
|
|
2423
|
+
tx = -(vp.x - Math.floor(r.left));
|
|
2424
|
+
ty = -(vp.y - Math.floor(r.top));
|
|
2425
|
+
} else {
|
|
2426
|
+
const TOTAL = readTotalTransformMatrix({
|
|
2427
|
+
baseTransform,
|
|
2428
|
+
rotate: ind.rotate || "0deg",
|
|
2429
|
+
scale: ind.scale,
|
|
2430
|
+
translate: ind.translate
|
|
2431
|
+
});
|
|
2432
|
+
const e = TOTAL.e ?? TOTAL.m41 ?? 0;
|
|
2433
|
+
const f = TOTAL.f ?? TOTAL.m42 ?? 0;
|
|
2434
|
+
cancelTranslateX = -e;
|
|
2435
|
+
cancelTranslateY = -f;
|
|
2436
|
+
const M2D = TOTAL.is2D ? matrix2DNoTranslate(TOTAL) : matrix2DNoTranslate(new DOMMatrix(TOTAL.toString()));
|
|
2437
|
+
const bb = bboxFromMatrix(w, h, M2D);
|
|
2438
|
+
outW = Math.max(1, Math.ceil(bb.width));
|
|
2439
|
+
outH = Math.max(1, Math.ceil(bb.height));
|
|
2440
|
+
tx = -bb.minX;
|
|
2441
|
+
ty = -bb.minY;
|
|
2231
2442
|
}
|
|
2232
2443
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
2233
2444
|
const fo = document.createElementNS(svgNS, "foreignObject");
|
|
2234
2445
|
fo.setAttribute("width", "100%");
|
|
2235
2446
|
fo.setAttribute("height", "100%");
|
|
2447
|
+
fo.setAttribute("x", String(tx));
|
|
2448
|
+
fo.setAttribute("y", String(ty));
|
|
2449
|
+
fo.style.overflow = "visible";
|
|
2236
2450
|
const styleTag = document.createElement("style");
|
|
2237
|
-
styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
|
|
2451
|
+
styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
|
|
2238
2452
|
fo.appendChild(styleTag);
|
|
2239
|
-
|
|
2453
|
+
const container = document.createElement("div");
|
|
2454
|
+
container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2455
|
+
container.style.width = `${w}px`;
|
|
2456
|
+
container.style.height = `${h}px`;
|
|
2457
|
+
container.style.overflow = "visible";
|
|
2458
|
+
container.style.transformOrigin = "0 0";
|
|
2459
|
+
const cancels = [];
|
|
2460
|
+
if (cancelTranslateX || cancelTranslateY) {
|
|
2461
|
+
cancels.push(`translate(${cancelTranslateX}px, ${cancelTranslateY}px)`);
|
|
2462
|
+
}
|
|
2463
|
+
if (!hasScale && (hasW || hasH)) {
|
|
2464
|
+
const sxWH = rect.width ? w / rect.width : 1;
|
|
2465
|
+
const syWH = rect.height ? h / rect.height : 1;
|
|
2466
|
+
cancels.push(`scale(${sxWH}, ${syWH})`);
|
|
2467
|
+
} else if (hasScale) {
|
|
2468
|
+
cancels.push(`scale(${scaleOpt})`);
|
|
2469
|
+
}
|
|
2470
|
+
if (cancels.length) container.style.transform = cancels.join(" ");
|
|
2471
|
+
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2472
|
+
clone.style.transformOrigin = "0 0";
|
|
2473
|
+
clone.style.position = "static";
|
|
2474
|
+
clone.style.left = "0";
|
|
2475
|
+
clone.style.top = "0";
|
|
2476
|
+
if (baseTransform) clone.style.transform = baseTransform;
|
|
2477
|
+
clone.style.rotate = ind.rotate || "0deg";
|
|
2478
|
+
clone.style.scale = ind.scale || "1";
|
|
2479
|
+
clone.style.translate = ind.translate || "0 0";
|
|
2480
|
+
container.appendChild(clone);
|
|
2481
|
+
fo.appendChild(container);
|
|
2240
2482
|
const serializer = new XMLSerializer();
|
|
2241
2483
|
const foString = serializer.serializeToString(fo);
|
|
2242
|
-
const svgHeader = `<svg xmlns="${svgNS}" width="${
|
|
2484
|
+
const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
|
|
2243
2485
|
const svgFooter = "</svg>";
|
|
2244
2486
|
svgString = svgHeader + foString + svgFooter;
|
|
2245
2487
|
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
@@ -2250,6 +2492,128 @@ async function captureDOM(element, options) {
|
|
|
2250
2492
|
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
|
|
2251
2493
|
return dataURL;
|
|
2252
2494
|
}
|
|
2495
|
+
function getNaturalBorderBoxSize(el) {
|
|
2496
|
+
const cs = getComputedStyle(el);
|
|
2497
|
+
const px = (v) => parseFloat(v) || 0;
|
|
2498
|
+
let w = px(cs.width);
|
|
2499
|
+
let h = px(cs.height);
|
|
2500
|
+
if (cs.boxSizing === "content-box") {
|
|
2501
|
+
w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
|
|
2502
|
+
h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
|
|
2503
|
+
}
|
|
2504
|
+
return { width: w, height: h };
|
|
2505
|
+
}
|
|
2506
|
+
function matrixFromComputed(el) {
|
|
2507
|
+
const tr = getComputedStyle(el).transform;
|
|
2508
|
+
if (!tr || tr === "none") return new DOMMatrix();
|
|
2509
|
+
try {
|
|
2510
|
+
return new DOMMatrix(tr);
|
|
2511
|
+
} catch {
|
|
2512
|
+
return new WebKitCSSMatrix(tr);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function matrix2DNoTranslate(M) {
|
|
2516
|
+
return new DOMMatrix([M.a, M.b, M.c, M.d, 0, 0]);
|
|
2517
|
+
}
|
|
2518
|
+
function bboxFromMatrix(w, h, M2D) {
|
|
2519
|
+
const pts = [
|
|
2520
|
+
{ x: 0, y: 0 },
|
|
2521
|
+
{ x: w, y: 0 },
|
|
2522
|
+
{ x: 0, y: h },
|
|
2523
|
+
{ x: w, y: h }
|
|
2524
|
+
];
|
|
2525
|
+
const t = pts.map((p) => ({
|
|
2526
|
+
x: M2D.a * p.x + M2D.c * p.y,
|
|
2527
|
+
y: M2D.b * p.x + M2D.d * p.y
|
|
2528
|
+
}));
|
|
2529
|
+
const minX = Math.min(...t.map((p) => p.x));
|
|
2530
|
+
const maxX = Math.max(...t.map((p) => p.x));
|
|
2531
|
+
const minY = Math.min(...t.map((p) => p.y));
|
|
2532
|
+
const maxY = Math.max(...t.map((p) => p.y));
|
|
2533
|
+
return { minX, minY, width: maxX - minX, height: maxY - minY };
|
|
2534
|
+
}
|
|
2535
|
+
function readIndividualTransforms(el) {
|
|
2536
|
+
const out = { rotate: "0deg", scale: null, translate: null };
|
|
2537
|
+
const map = el.computedStyleMap?.();
|
|
2538
|
+
if (map) {
|
|
2539
|
+
const rot = map.get("rotate");
|
|
2540
|
+
if (rot) {
|
|
2541
|
+
const ang = rot.angle ?? rot;
|
|
2542
|
+
if (ang && "unit" in ang) {
|
|
2543
|
+
out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
|
|
2544
|
+
} else {
|
|
2545
|
+
out.rotate = String(rot);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const sc = map.get("scale");
|
|
2549
|
+
if (sc && sc.length) {
|
|
2550
|
+
const sx = sc[0]?.value ?? 1;
|
|
2551
|
+
const sy = sc[1]?.value ?? sx;
|
|
2552
|
+
out.scale = `${sx} ${sy}`;
|
|
2553
|
+
}
|
|
2554
|
+
const tr = map.get("translate");
|
|
2555
|
+
if (tr && tr.length) {
|
|
2556
|
+
const tx = tr[0]?.value ?? 0;
|
|
2557
|
+
const ty = tr[1]?.value ?? 0;
|
|
2558
|
+
const ux = tr[0]?.unit ?? "px";
|
|
2559
|
+
const uy = tr[1]?.unit ?? "px";
|
|
2560
|
+
out.translate = `${tx}${ux} ${ty}${uy}`;
|
|
2561
|
+
}
|
|
2562
|
+
return out;
|
|
2563
|
+
}
|
|
2564
|
+
const cs = getComputedStyle(el);
|
|
2565
|
+
out.rotate = cs.rotate && cs.rotate !== "none" ? cs.rotate : "0deg";
|
|
2566
|
+
out.scale = cs.scale && cs.scale !== "none" ? cs.scale : null;
|
|
2567
|
+
out.translate = cs.translate && cs.translate !== "none" ? cs.translate : null;
|
|
2568
|
+
return out;
|
|
2569
|
+
}
|
|
2570
|
+
function hasBBoxAffectingTransform(el) {
|
|
2571
|
+
const t = getComputedStyle(el).transform || "none";
|
|
2572
|
+
if (t === "none" || t === "matrix(1, 0, 0, 1, 0, 0)") return false;
|
|
2573
|
+
if (/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,/i.test(t)) return false;
|
|
2574
|
+
return true;
|
|
2575
|
+
}
|
|
2576
|
+
function inflateRect(r, p) {
|
|
2577
|
+
const x = Math.floor(r.left - p.left);
|
|
2578
|
+
const y = Math.floor(r.top - p.top);
|
|
2579
|
+
const w = Math.ceil(r.width + p.left + p.right);
|
|
2580
|
+
const h = Math.ceil(r.height + p.top + p.bottom);
|
|
2581
|
+
return { x, y, width: w, height: h };
|
|
2582
|
+
}
|
|
2583
|
+
var __measureHost = null;
|
|
2584
|
+
function getMeasureHost() {
|
|
2585
|
+
if (__measureHost) return __measureHost;
|
|
2586
|
+
const n = document.createElement("div");
|
|
2587
|
+
n.id = "snapdom-measure-slot";
|
|
2588
|
+
n.setAttribute("aria-hidden", "true");
|
|
2589
|
+
Object.assign(n.style, {
|
|
2590
|
+
position: "absolute",
|
|
2591
|
+
left: "-99999px",
|
|
2592
|
+
top: "0px",
|
|
2593
|
+
width: "0px",
|
|
2594
|
+
height: "0px",
|
|
2595
|
+
overflow: "hidden",
|
|
2596
|
+
opacity: "0",
|
|
2597
|
+
pointerEvents: "none",
|
|
2598
|
+
contain: "size layout style"
|
|
2599
|
+
});
|
|
2600
|
+
document.documentElement.appendChild(n);
|
|
2601
|
+
__measureHost = n;
|
|
2602
|
+
return n;
|
|
2603
|
+
}
|
|
2604
|
+
function readTotalTransformMatrix(t) {
|
|
2605
|
+
const host = getMeasureHost();
|
|
2606
|
+
const tmp = document.createElement("div");
|
|
2607
|
+
tmp.style.transformOrigin = "0 0";
|
|
2608
|
+
if (t.baseTransform) tmp.style.transform = t.baseTransform;
|
|
2609
|
+
if (t.rotate) tmp.style.rotate = t.rotate;
|
|
2610
|
+
if (t.scale) tmp.style.scale = t.scale;
|
|
2611
|
+
if (t.translate) tmp.style.translate = t.translate;
|
|
2612
|
+
host.appendChild(tmp);
|
|
2613
|
+
const M = matrixFromComputed(tmp);
|
|
2614
|
+
host.removeChild(tmp);
|
|
2615
|
+
return M;
|
|
2616
|
+
}
|
|
2253
2617
|
|
|
2254
2618
|
// src/core/context.js
|
|
2255
2619
|
function normalizeCachePolicy(v) {
|
|
@@ -2282,6 +2646,7 @@ function createContext(options = {}) {
|
|
|
2282
2646
|
iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
|
|
2283
2647
|
localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
|
|
2284
2648
|
excludeFonts: options.excludeFonts ?? void 0,
|
|
2649
|
+
defaultImageUrl: options.defaultImageUrl ?? void 0,
|
|
2285
2650
|
/** Cache policy (disabled|full|auto|soft). Default: "soft" */
|
|
2286
2651
|
/** @type {CachePolicy} */
|
|
2287
2652
|
cache: cachePolicy,
|
|
@@ -2315,7 +2680,7 @@ async function toImg(url, options) {
|
|
|
2315
2680
|
|
|
2316
2681
|
// src/exporters/toCanvas.js
|
|
2317
2682
|
async function toCanvas(url, options) {
|
|
2318
|
-
const { scale
|
|
2683
|
+
const { scale, dpr } = options;
|
|
2319
2684
|
const safari = isSafari();
|
|
2320
2685
|
const img = new Image();
|
|
2321
2686
|
img.crossOrigin = "anonymous";
|
|
@@ -2334,8 +2699,8 @@ async function toCanvas(url, options) {
|
|
|
2334
2699
|
if (safari) {
|
|
2335
2700
|
await new Promise((r) => setTimeout(r, 100));
|
|
2336
2701
|
}
|
|
2337
|
-
const width = img.naturalWidth *
|
|
2338
|
-
const height = img.naturalHeight *
|
|
2702
|
+
const width = img.naturalWidth * scale;
|
|
2703
|
+
const height = img.naturalHeight * scale;
|
|
2339
2704
|
const canvas = document.createElement("canvas");
|
|
2340
2705
|
canvas.width = Math.ceil(width * dpr);
|
|
2341
2706
|
canvas.height = Math.ceil(height * dpr);
|
|
@@ -2409,6 +2774,14 @@ async function snapdom(element, userOptions) {
|
|
|
2409
2774
|
if (!element) throw new Error("Element cannot be null or undefined");
|
|
2410
2775
|
const context = createContext(userOptions);
|
|
2411
2776
|
if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
|
|
2777
|
+
if (!context.snap) {
|
|
2778
|
+
context.snap = {
|
|
2779
|
+
/** Delegates to this module’s PNG path (tu build devuelve <img>). */
|
|
2780
|
+
toPng: (el, opts) => snapdom.toPng(el, opts),
|
|
2781
|
+
/** Útil si en algún flujo necesitás vector o <img> directo. */
|
|
2782
|
+
toImg: (el, opts) => snapdom.toImg(el, opts)
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2412
2785
|
return snapdom.capture(element, context, INTERNAL_TOKEN);
|
|
2413
2786
|
}
|
|
2414
2787
|
snapdom.capture = async (el, context, _token) => {
|
package/package.json
CHANGED
package/types/snapdom.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* snapDOM – ultra-fast DOM-to-image capture
|
|
3
3
|
* TypeScript definitions
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Notes:
|
|
6
6
|
* - Style compression is always enabled internally (no user option).
|
|
7
7
|
* - Icon fonts are always embedded; `embedFonts` controls non-icon fonts only.
|
|
@@ -101,6 +101,20 @@ export interface CaptureOptions {
|
|
|
101
101
|
* - "none": do not reset anything (fastest, may reuse stale resources).
|
|
102
102
|
*/
|
|
103
103
|
reset?: "soft" | "hard" | "none";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fallback image source when an <img> fails to load.
|
|
107
|
+
* - String: use as-is.
|
|
108
|
+
* - Callback: receives measured width/height and original src, returns a URL string.
|
|
109
|
+
*/
|
|
110
|
+
defaultImageUrl?:
|
|
111
|
+
| string
|
|
112
|
+
| ((args: {
|
|
113
|
+
width?: number;
|
|
114
|
+
height?: number;
|
|
115
|
+
src?: string;
|
|
116
|
+
element: HTMLImageElement;
|
|
117
|
+
}) => string | Promise<string>);
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
export interface BlobOptions {
|