dsh-design-studio 0.9.0 → 0.11.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.
- package/README.en.md +6 -4
- package/README.md +6 -4
- package/assets/demo.gif +0 -0
- package/assets/hero-sm.jpg +0 -0
- package/cordis.patch.yml +5 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
- package/gallery-seed/index.json +1 -0
- package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
- package/lib/browser.js +23 -9
- package/lib/bundle.js +12 -3
- package/lib/client.js +3106 -111
- package/lib/core.js +44 -11
- package/lib/gallery/css.js +230 -0
- package/lib/gallery/html.js +313 -0
- package/lib/gallery/index.js +22 -0
- package/lib/gallery/market-routes.js +306 -0
- package/lib/gallery/market.js +623 -0
- package/lib/gallery/pack.js +311 -0
- package/lib/gallery/policy.js +1560 -0
- package/lib/gallery/qrfind.js +181 -0
- package/lib/gallery/render.js +148 -0
- package/lib/gallery/routes.js +360 -0
- package/lib/gallery/tar.js +118 -0
- package/lib/gallery/use.js +125 -0
- package/lib/gallery/zip.js +212 -0
- package/lib/handoff.js +291 -29
- package/lib/history.js +3 -1
- package/lib/index.js +118 -24
- package/lib/inspire.js +142 -0
- package/lib/offline.js +17 -0
- package/lib/picker.js +425 -32
- package/lib/serve.js +88 -11
- package/lib/works.js +238 -0
- package/locale/en.json +2 -1
- package/locale/offline/en.json +6 -0
- package/locale/offline/zh.json +6 -0
- package/locale/zh.json +2 -1
- package/package.json +10 -3
- package/skills/ui-design/SKILL.md +9 -0
package/lib/picker.js
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
s.textContent = '*{scrollbar-width:none}*::-webkit-scrollbar{display:none}'
|
|
37
37
|
;(document.head || document.documentElement).appendChild(s)
|
|
38
38
|
}
|
|
39
|
+
// 别人做的设计(设计库来的、没改过):服务端在脚本标签上标了 data-strict(远程看时在 cfg 里)。
|
|
40
|
+
// 点站外链接不跳(页面本来就跑不了脚本、交不出表单,这里再挡住「点一下跳到别的网站」),告诉右侧栏拦下了
|
|
41
|
+
const STRICT = !!cfg.strict || !!(document.currentScript && document.currentScript.getAttribute('data-strict') === '1')
|
|
39
42
|
const SEED_KEY = '__dsh_design_seed__'
|
|
40
43
|
// 远程看(右侧栏把这一屏打包放进沙盒 iframe 的 srcdoc,location 是 about:srcdoc):这一屏原本的地址由右侧栏给
|
|
41
44
|
const HERE = cfg.remote && typeof cfg.href === 'string' ? cfg.href : ''
|
|
@@ -58,6 +61,80 @@
|
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
// 拖着文件进了这一页(页面铺满右侧栏时,外面的右侧栏收不到拖动):告诉右侧栏亮出「松开打开 / 当参考图」的提示,
|
|
65
|
+
// 松手也在这里接住、把文件转给右侧栏。提示只是一层画、不挡事件:页面自己的上传区照样收得到后面的拖动
|
|
66
|
+
// 页面自己里面拖的(拖一下页面上的图、页面自己的拖放)不算:那不是要打开什么
|
|
67
|
+
// 用 drag 不用 dragstart:页面取消了 dragstart(轮播图常这么做)就不会有 drag,也就不会有 dragend 来清掉标记
|
|
68
|
+
let ownDrag = false
|
|
69
|
+
let dropOwn = false
|
|
70
|
+
addEventListener('drag', () => { ownDrag = true }, true)
|
|
71
|
+
addEventListener('dragend', () => { ownDrag = false }, true)
|
|
72
|
+
addEventListener('pointerup', () => { ownDrag = false }, true)
|
|
73
|
+
const hasFiles = (dt) => !!dt && [...(dt.types || [])].includes('Files')
|
|
74
|
+
// 页面自己接这次拖动:上传区在 dragover 上 preventDefault(或者拦住不让冒泡,见下面捕获阶段那个),或者落在 <input type=file> 上
|
|
75
|
+
const pageTakes = (e) => e.defaultPrevented || (typeof e.composedPath === 'function' ? e.composedPath() : [e.target]).some((n) => n && n.tagName === 'INPUT' && String(n.type).toLowerCase() === 'file')
|
|
76
|
+
let lastIn = 0
|
|
77
|
+
let took = false // 刚才那下是页面自己接的:收起提示,松手也归页面
|
|
78
|
+
const seen = new WeakSet() // 冒泡阶段见到过的 dragover(按事件记:页面忙时几个 dragover 会连着处理)
|
|
79
|
+
const pageTook = () => {
|
|
80
|
+
if (took) return
|
|
81
|
+
took = true
|
|
82
|
+
lastIn = 0
|
|
83
|
+
post({ type: 'dragOut' })
|
|
84
|
+
}
|
|
85
|
+
// 只看 dragover:同一个元素上拖第二次时浏览器不一定再给 dragenter;dragenter 时也还不知道页面接不接
|
|
86
|
+
// 挂在 window 冒泡阶段的最后,页面的处理先跑完;页面全局拦了 dragover 防误拖的就当页面接了(那种页面用「浏览…」或地址栏打开)
|
|
87
|
+
const onOver = (e) => {
|
|
88
|
+
seen.add(e)
|
|
89
|
+
const dt = e.dataTransfer
|
|
90
|
+
if (ownDrag || !hasFiles(dt)) return
|
|
91
|
+
if (pageTakes(e)) return pageTook()
|
|
92
|
+
took = false
|
|
93
|
+
e.preventDefault() // 这里能松手:不然浏览器把文件当网页打开(或者根本不给 drop)
|
|
94
|
+
dt.dropEffect = 'copy'
|
|
95
|
+
const now = Date.now()
|
|
96
|
+
if (now - lastIn < 200) return // 拖着时 dragover 一直来,隔一会儿报一次就够
|
|
97
|
+
lastIn = now
|
|
98
|
+
post({ type: 'dragIn', items: [...(dt.items || [])].slice(0, 20).map((x) => ({ kind: String(x.kind), type: String(x.type) })) })
|
|
99
|
+
}
|
|
100
|
+
const onDrop = (e) => {
|
|
101
|
+
const dt = e.dataTransfer
|
|
102
|
+
const own = dropOwn
|
|
103
|
+
dropOwn = false
|
|
104
|
+
if (own || !hasFiles(dt)) return
|
|
105
|
+
if (took || pageTakes(e)) { took = false; return }
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
const files = []
|
|
108
|
+
for (const x of [...(dt.items || [])].slice(0, 20)) {
|
|
109
|
+
if (x.kind !== 'file') continue
|
|
110
|
+
// 文件夹只有 entry 分得出来,得在 drop 里同步取
|
|
111
|
+
let dir = false
|
|
112
|
+
try { const en = typeof x.webkitGetAsEntry === 'function' ? x.webkitGetAsEntry() : null; dir = !!(en && en.isDirectory) } catch { dir = false }
|
|
113
|
+
let f = null
|
|
114
|
+
try { f = x.getAsFile() } catch { f = null }
|
|
115
|
+
if (f) files.push({ f, dir })
|
|
116
|
+
}
|
|
117
|
+
// File 能直接跟着消息过去(桌面版照样拿得到它的真实路径)
|
|
118
|
+
post({ type: 'dropIn', files })
|
|
119
|
+
}
|
|
120
|
+
// 页面在 window 上挂的整页上传常常比这里晚注册(type=module 的脚本、框架挂载后):每次事件在捕获阶段把上面两个摘下来重挂,
|
|
121
|
+
// 排到 window 冒泡监听的最后,页面的处理(不管什么时候挂的)都先跑完,才看它接没接。
|
|
122
|
+
// 重挂放在 document 的捕获阶段:window 捕获阶段的监听(页面的也算)都跑过了才轮到它
|
|
123
|
+
const last = (type, fn) => { removeEventListener(type, fn); addEventListener(type, fn) }
|
|
124
|
+
last('dragover', onOver)
|
|
125
|
+
last('drop', onDrop)
|
|
126
|
+
document.addEventListener('dragover', () => last('dragover', onOver), true)
|
|
127
|
+
document.addEventListener('drop', () => last('drop', onDrop), true)
|
|
128
|
+
addEventListener('dragover', (e) => {
|
|
129
|
+
if (ownDrag || !hasFiles(e.dataTransfer)) return
|
|
130
|
+
// 上传区拦住 dragover 不让冒泡(stopPropagation)时上面那个收不到:这一轮事件发完还没见到它,就是页面接了
|
|
131
|
+
setTimeout(() => { if (!seen.has(e)) pageTook() }, 0)
|
|
132
|
+
}, true)
|
|
133
|
+
addEventListener('drop', () => {
|
|
134
|
+
dropOwn = ownDrag
|
|
135
|
+
ownDrag = false
|
|
136
|
+
}, true)
|
|
137
|
+
|
|
61
138
|
// ── 描述一个元素 ──────────────────────────────────────────────────────────
|
|
62
139
|
|
|
63
140
|
const esc = (s) => (window.CSS && CSS.escape ? CSS.escape(s) : String(s).replace(/[^\w-]/g, '\\$&'))
|
|
@@ -107,7 +184,7 @@
|
|
|
107
184
|
}
|
|
108
185
|
const full = el.outerHTML.replace(/\s+/g, ' ')
|
|
109
186
|
if (full.length <= max) return full
|
|
110
|
-
const open = shallow.outerHTML.replace(/<\/[^>]+>$/, '')
|
|
187
|
+
const open = shallow.outerHTML.replace(/<\/[^>]+>$/, '').replace(/\s+/g, ' ')
|
|
111
188
|
const kids = el.children.length
|
|
112
189
|
const txt = textOf(el)
|
|
113
190
|
const inner = kids ? ` …${kids} 个子元素… ` : clip(txt, 60)
|
|
@@ -1188,6 +1265,36 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1188
1265
|
post({ type: 'links', links })
|
|
1189
1266
|
}
|
|
1190
1267
|
|
|
1268
|
+
if (STRICT) {
|
|
1269
|
+
const offsite = (raw) => {
|
|
1270
|
+
const v = String(raw || '').trim()
|
|
1271
|
+
if (!v || v.startsWith('#')) return ''
|
|
1272
|
+
let u
|
|
1273
|
+
try { u = new URL(v, here()) } catch { return v.slice(0, 200) }
|
|
1274
|
+
return u.origin === hereUrl().origin ? '' : u.href.slice(0, 200)
|
|
1275
|
+
}
|
|
1276
|
+
addEventListener('click', (e) => {
|
|
1277
|
+
const a = e.target && e.target.closest ? e.target.closest('a[href], area[href]') : null
|
|
1278
|
+
if (!a) return
|
|
1279
|
+
const dl = a.hasAttribute('download')
|
|
1280
|
+
const out = offsite(a.getAttribute('href'))
|
|
1281
|
+
// 下载、开新窗口沙盒本来就不让;站外的地址这里直接挡(带 download 的按「下载」报)
|
|
1282
|
+
if (!out && !dl) return
|
|
1283
|
+
e.preventDefault()
|
|
1284
|
+
e.stopImmediatePropagation()
|
|
1285
|
+
post({ type: 'blocked', href: dl ? 'download' : out })
|
|
1286
|
+
}, true)
|
|
1287
|
+
if (window.navigation && typeof navigation.addEventListener === 'function') {
|
|
1288
|
+
navigation.addEventListener('navigate', (e) => {
|
|
1289
|
+
if (!e.cancelable || e.navigationType === 'traverse') return
|
|
1290
|
+
const out = offsite(e.destination && e.destination.url)
|
|
1291
|
+
if (!out && !e.downloadRequest) return
|
|
1292
|
+
e.preventDefault()
|
|
1293
|
+
post({ type: 'blocked', href: out || 'download' })
|
|
1294
|
+
})
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1191
1298
|
// 演示:点了跳页的地方,不在 iframe 里直接跳,告诉右侧栏换一屏(带过场动画)
|
|
1192
1299
|
// 远程看也是:沙盒里的页面跳不过去(地址指着电脑本机),由右侧栏打包那一页换上来
|
|
1193
1300
|
let demo = false
|
|
@@ -1385,17 +1492,27 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1385
1492
|
const URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g
|
|
1386
1493
|
|
|
1387
1494
|
/** 把一段 CSS 值里的 url(...) 都换成 data URL */
|
|
1388
|
-
async function inlineUrls(value, base, inline) {
|
|
1495
|
+
async function inlineUrls(value, base, inline, idp = '') {
|
|
1389
1496
|
if (!value || value.indexOf('url(') < 0) return value
|
|
1497
|
+
// 指着这一页里的东西(SVG 的渐变、裁剪、滤镜:url(#area)):留成页内引用,别当文件去下载
|
|
1498
|
+
const page = String(base || '').split('#')[0]
|
|
1499
|
+
const local = new Map()
|
|
1390
1500
|
const jobs = []
|
|
1391
|
-
value.replace(URL_RE, (m, q, u) => {
|
|
1501
|
+
value.replace(URL_RE, (m, q, u) => {
|
|
1502
|
+
if (u.startsWith('#')) local.set(u, '#' + idp + u.slice(1))
|
|
1503
|
+
else {
|
|
1504
|
+
try { const a = new URL(u, base); if (a.hash && a.href.split('#')[0] === page) local.set(u, '#' + idp + a.hash.slice(1)) } catch { /* 怪地址 */ }
|
|
1505
|
+
}
|
|
1506
|
+
if (!local.has(u)) jobs.push(u)
|
|
1507
|
+
return m
|
|
1508
|
+
})
|
|
1392
1509
|
const map = new Map()
|
|
1393
1510
|
await Promise.all(jobs.map(async (u) => {
|
|
1394
1511
|
let abs = u
|
|
1395
1512
|
try { abs = new URL(u, base).href } catch { /* 原样 */ }
|
|
1396
1513
|
map.set(u, await inline(abs))
|
|
1397
1514
|
}))
|
|
1398
|
-
return value.replace(URL_RE, (m, q, u) => (map.get(u) ? `url("${map.get(u)}")` : 'none'))
|
|
1515
|
+
return value.replace(URL_RE, (m, q, u) => (local.has(u) ? `url("${local.get(u)}")` : map.get(u) ? `url("${map.get(u)}")` : 'none'))
|
|
1399
1516
|
}
|
|
1400
1517
|
|
|
1401
1518
|
function styleText(cs, def, out) {
|
|
@@ -1416,7 +1533,7 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1416
1533
|
let text = false
|
|
1417
1534
|
for (const n of el.childNodes) if (n.nodeType === 3 && n.nodeValue.trim()) { text = true; break }
|
|
1418
1535
|
if (!text) return false
|
|
1419
|
-
const r =
|
|
1536
|
+
const r = el.ownerDocument.createRange()
|
|
1420
1537
|
r.selectNodeContents(el)
|
|
1421
1538
|
const rects = [...r.getClientRects()].filter((x) => x.width && x.height)
|
|
1422
1539
|
if (!rects.length || rects.length > 40) return false
|
|
@@ -1453,6 +1570,24 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1453
1570
|
const H = Math.ceil(full ? Math.max(de.scrollHeight, document.body ? document.body.scrollHeight : 0, innerHeight) : innerHeight)
|
|
1454
1571
|
const box = makeSandbox()
|
|
1455
1572
|
const inline = makeInliner()
|
|
1573
|
+
// 只截看得到的这一屏(不是整页):页面滚过、里面的滚动区滚过、有贴住的(sticky)元素,画成图时都得照现在的样子摆
|
|
1574
|
+
const view = !full
|
|
1575
|
+
const stuck = view ? stickyShift(document) : new Map()
|
|
1576
|
+
// 页面滚过时 body 整体往上挪(transform),body 就成了里面固定元素的定位基准:跟着窗口走的固定元素按 body 的位置换算回去,还贴在窗口原来的地方
|
|
1577
|
+
const lift = view && !!(scrollX || scrollY) && !!document.body
|
|
1578
|
+
let bodyAt = null
|
|
1579
|
+
if (lift) {
|
|
1580
|
+
const br = document.body.getBoundingClientRect()
|
|
1581
|
+
const bcs = getComputedStyle(document.body)
|
|
1582
|
+
bodyAt = { l: br.left + (parseFloat(bcs.borderLeftWidth) || 0), t: br.top + (parseFloat(bcs.borderTopWidth) || 0) }
|
|
1583
|
+
}
|
|
1584
|
+
// 怪异模式下 body.scrollTop 就是页面滚动;内嵌页面的根元素滚过(它自己的页面滚动)也照样往回挪
|
|
1585
|
+
// 内嵌页面画的总是它现在看得到的那块:里面的滚动(整页导出时也算)都补
|
|
1586
|
+
const ownScroll = (el) => el.ownerDocument !== document || (view && el !== de && (el !== document.body || document.compatMode === 'CSS1Compat'))
|
|
1587
|
+
// 资源地址按元素所在的那页算(内嵌页面有自己的地址)
|
|
1588
|
+
const baseOf = (el) => { const b = el.ownerDocument.baseURI || ''; return /^https?:/.test(b) ? b : here() }
|
|
1589
|
+
// 子树里还在找定位基准的绝对 / 固定定位(a / f):滚动区的孩子要是改成相对定位,会把这些绝对定位抢过来(换了基准、还被滚动区裁掉)
|
|
1590
|
+
const openOf = new Map()
|
|
1456
1591
|
const pseudoRules = []
|
|
1457
1592
|
const families = new Set()
|
|
1458
1593
|
const jobs = []
|
|
@@ -1487,10 +1622,14 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1487
1622
|
return p
|
|
1488
1623
|
}
|
|
1489
1624
|
|
|
1490
|
-
|
|
1625
|
+
// ctx:往下传「以哪个滚过的滚动区为定位基准」——a:绝对定位的后代该往回挪多少,f:固定定位的(滚动区有变换时)
|
|
1626
|
+
const NO_SHIFT = { a: [0, 0], f: [0, 0] }
|
|
1627
|
+
const idPre = new Map([[document, '']])
|
|
1628
|
+
function walk(node, ctx = NO_SHIFT) {
|
|
1491
1629
|
if (node.nodeType === 3) return document.createTextNode(node.nodeValue)
|
|
1492
1630
|
if (node.nodeType !== 1) return null
|
|
1493
1631
|
const el = node
|
|
1632
|
+
const idp = idPre.get(el.ownerDocument) || ''
|
|
1494
1633
|
if (SKIP_TAGS.has(el.tagName.toUpperCase())) return null
|
|
1495
1634
|
if (el.tagName === 'STYLE' && el.namespaceURI !== SVGNS) return null
|
|
1496
1635
|
const cs = getComputedStyle(el)
|
|
@@ -1508,7 +1647,14 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1508
1647
|
for (const a of el.attributes) {
|
|
1509
1648
|
if (a.name === 'style' || a.name.startsWith('on') || a.name === 'class' && el.namespaceURI !== SVGNS) continue
|
|
1510
1649
|
if (a.name === 'srcset' || a.name === 'sizes' || a.name === 'loading') continue
|
|
1511
|
-
|
|
1650
|
+
let v = a.value
|
|
1651
|
+
// 内嵌页面的 id 和页内引用加上这一页的前缀:几页里同名的渐变、裁剪不会串
|
|
1652
|
+
if (idp) {
|
|
1653
|
+
if (a.name === 'id') v = idp + v
|
|
1654
|
+
else if (/(^|:)href$/.test(a.name) && v.startsWith('#')) v = '#' + idp + v.slice(1)
|
|
1655
|
+
else if (v.indexOf('url(') >= 0) v = v.replace(/url\(\s*(['"]?)#([^'")]+)\1\s*\)/g, (m, q, id) => `url(#${idp}${id})`)
|
|
1656
|
+
}
|
|
1657
|
+
try { c.setAttributeNS(a.namespaceURI, a.name, v) } catch { /* 怪属性名 */ }
|
|
1512
1658
|
}
|
|
1513
1659
|
}
|
|
1514
1660
|
if (el.namespaceURI === SVGNS && el.getAttribute('class')) c.setAttribute('class', el.getAttribute('class'))
|
|
@@ -1517,17 +1663,31 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1517
1663
|
const def = box.defaults(el)
|
|
1518
1664
|
let st = styleText(cs, def, withUrls)
|
|
1519
1665
|
if (el === de) st += 'overflow:hidden;'
|
|
1666
|
+
// iframe / 视频换成了 div:默认的 300×150、行内排列也得写明(和 iframe 默认一样的样式上面省掉了)
|
|
1667
|
+
if (tag === 'IFRAME' || tag === 'VIDEO' || tag === 'OBJECT' || tag === 'EMBED') {
|
|
1668
|
+
st += `display:${cs.display === 'inline' ? 'inline-block' : cs.display};width:${cs.width};height:${cs.height};box-sizing:${cs.boxSizing};vertical-align:${cs.verticalAlign};overflow:hidden;`
|
|
1669
|
+
}
|
|
1670
|
+
// 贴住了:画成图时没有滚动,按量出来的偏移摆回现在的位置
|
|
1671
|
+
if (stuck.has(el)) { const [dx, dy] = stuck.get(el); st += `position:relative;inset:auto;left:${dx}px;top:${dy}px;` }
|
|
1672
|
+
// 以滚过的滚动区为基准的绝对 / 固定定位:跟着内容滚,往回挪(不管隔了几层)
|
|
1673
|
+
const owe = el.namespaceURI === SVGNS ? null : cs.position === 'absolute' ? ctx.a : cs.position === 'fixed' ? ctx.f : null
|
|
1674
|
+
if (owe && (owe[0] || owe[1])) st += shiftCss(cs, owe[0], owe[1])
|
|
1675
|
+
if (lift && cs.position === 'fixed' && el.namespaceURI !== SVGNS && el.ownerDocument === document && document.body.contains(el) && !fixedHeld(el)) {
|
|
1676
|
+
const x = parseFloat(cs.left)
|
|
1677
|
+
const y = parseFloat(cs.top)
|
|
1678
|
+
if (Number.isFinite(x) && Number.isFinite(y)) st += `left:${x - bodyAt.l}px;top:${y - bodyAt.t}px;right:auto;bottom:auto;`
|
|
1679
|
+
}
|
|
1520
1680
|
// 原来一行放得下的字,画成图时也别折行(宽度是定死的,字体渲染差一丁点就会挤到第二行)
|
|
1521
1681
|
if (el.namespaceURI !== SVGNS && cs.whiteSpace !== 'nowrap' && oneLine(el)) st += 'white-space:nowrap;'
|
|
1522
1682
|
c.setAttribute('style', st)
|
|
1523
1683
|
families.add(cs.fontFamily)
|
|
1524
1684
|
if (withUrls.length) {
|
|
1685
|
+
// 等图片转好再换进去;换的时候读那一刻的 style(滚动区补的偏移是后来加上的,别盖掉)
|
|
1686
|
+
const vals = withUrls.map((p) => cs.getPropertyValue(p))
|
|
1525
1687
|
jobs.push((async () => {
|
|
1688
|
+
const done = await Promise.all(vals.map((v) => inlineUrls(v, baseOf(el), inline, idp)))
|
|
1526
1689
|
let s = c.getAttribute('style')
|
|
1527
|
-
|
|
1528
|
-
const v = cs.getPropertyValue(p)
|
|
1529
|
-
s = s.replace(`${p}:${v};`, `${p}:${await inlineUrls(v, here(), inline)};`)
|
|
1530
|
-
}
|
|
1690
|
+
withUrls.forEach((p, i) => { s = s.replace(`${p}:${vals[i]};`, `${p}:${done[i]};`) })
|
|
1531
1691
|
c.setAttribute('style', s)
|
|
1532
1692
|
})())
|
|
1533
1693
|
}
|
|
@@ -1538,12 +1698,12 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1538
1698
|
}
|
|
1539
1699
|
if (el.namespaceURI === SVGNS && el.localName === 'image') {
|
|
1540
1700
|
const href = el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
|
|
1541
|
-
if (href) jobs.push(inline(new URL(href,
|
|
1701
|
+
if (href) jobs.push(inline(new URL(href, baseOf(el)).href).then((d) => { if (d) c.setAttribute('href', d) }))
|
|
1542
1702
|
}
|
|
1543
1703
|
if (el.namespaceURI === SVGNS && el.localName === 'use') {
|
|
1544
1704
|
const href = el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || ''
|
|
1545
1705
|
let abs = ''
|
|
1546
|
-
try { abs = href && !href.startsWith('#') ? new URL(href,
|
|
1706
|
+
try { abs = href && !href.startsWith('#') ? new URL(href, baseOf(el)).href : '' } catch { /* 怪地址 */ }
|
|
1547
1707
|
if (abs && abs.includes('#')) {
|
|
1548
1708
|
jobs.push(spriteOf(abs).then((id) => {
|
|
1549
1709
|
if (!id) return
|
|
@@ -1558,7 +1718,9 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1558
1718
|
if (tag === 'TEXTAREA') c.textContent = el.value
|
|
1559
1719
|
if (tag === 'SELECT') c.setAttribute('data-v', el.value)
|
|
1560
1720
|
|
|
1561
|
-
// ::before / ::after
|
|
1721
|
+
// ::before / ::after(绝对 / 固定定位的:跟着它的定位基准滚,也算还在往外找基准的)
|
|
1722
|
+
let pa = false
|
|
1723
|
+
let pf = false
|
|
1562
1724
|
if (el.namespaceURI !== SVGNS) {
|
|
1563
1725
|
const pes = (tag === 'INPUT' || tag === 'TEXTAREA') && el.getAttribute('placeholder') ? ['::before', '::after', '::placeholder'] : ['::before', '::after']
|
|
1564
1726
|
for (const pe of pes) {
|
|
@@ -1568,18 +1730,82 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1568
1730
|
const cls = 'ddpx' + (++seq)
|
|
1569
1731
|
c.setAttribute('class', ((c.getAttribute('class') || '') + ' ' + cls).trim())
|
|
1570
1732
|
const urls = []
|
|
1571
|
-
|
|
1733
|
+
let body = styleText(ps, box.defaults(el, pe), urls)
|
|
1734
|
+
const pp = ps.position
|
|
1735
|
+
if (pp === 'absolute' || pp === 'fixed') {
|
|
1736
|
+
const mine = pp === 'absolute' ? holds(cs) : holds(cs, true)
|
|
1737
|
+
const owe = mine ? (ownScroll(el) ? [el.scrollLeft, el.scrollTop] : [0, 0]) : pp === 'absolute' ? ctx.a : ctx.f
|
|
1738
|
+
if (owe[0] || owe[1]) body += shiftCss(ps, owe[0], owe[1])
|
|
1739
|
+
if (!mine) { if (pp === 'absolute') pa = true; else pf = true }
|
|
1740
|
+
// 贴着窗口的(比如 body::before 铺的固定背景):页面滚过、body 整体挪了,按 body 的位置换算回去
|
|
1741
|
+
if (pp === 'fixed' && !mine && lift && el.ownerDocument === document && document.body.contains(el) && !fixedHeld(el)) {
|
|
1742
|
+
const x = parseFloat(ps.left)
|
|
1743
|
+
const y = parseFloat(ps.top)
|
|
1744
|
+
if (Number.isFinite(x) && Number.isFinite(y)) body += `left:${x - bodyAt.l}px;top:${y - bodyAt.t}px;right:auto;bottom:auto;`
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1572
1747
|
const i = pseudoRules.length
|
|
1573
1748
|
pseudoRules.push(`.${cls}${pe}{${body}}`)
|
|
1574
|
-
if (urls.length) jobs.push(inlineUrls(pseudoRules[i],
|
|
1749
|
+
if (urls.length) jobs.push(inlineUrls(pseudoRules[i], baseOf(el), inline, idp).then((t) => { pseudoRules[i] = t }))
|
|
1575
1750
|
}
|
|
1576
1751
|
}
|
|
1577
1752
|
|
|
1578
|
-
|
|
1753
|
+
// 下面这几种不往里走:自己是绝对 / 固定定位的也得记上(外层滚动区挪的时候要知道)
|
|
1754
|
+
if ((tag === 'IFRAME' || tag === 'CANVAS' || tag === 'VIDEO' || tag === 'TEXTAREA') && (cs.position === 'absolute' || cs.position === 'fixed')) {
|
|
1755
|
+
openOf.set(el, { a: cs.position === 'absolute', f: cs.position === 'fixed' })
|
|
1756
|
+
}
|
|
1757
|
+
if (tag === 'IFRAME') {
|
|
1758
|
+
// 同源的内嵌页面:把它的页面也复制进来(框住、按它自己的滚动摆);拿不到的(跨域)铺一层浅灰,看得出这里有东西没截到
|
|
1759
|
+
let doc = null
|
|
1760
|
+
try { doc = el.contentDocument } catch { doc = null }
|
|
1761
|
+
let inner = null
|
|
1762
|
+
if (doc && doc.documentElement && depth < 3) {
|
|
1763
|
+
depth++
|
|
1764
|
+
try { inner = walkIn(doc) } finally { depth-- }
|
|
1765
|
+
}
|
|
1766
|
+
// 里面固定定位的元素以这一框为准:框自己没变换的,补一个不动的变换
|
|
1767
|
+
if (inner) {
|
|
1768
|
+
c.setAttribute('style', c.getAttribute('style') + 'overflow:hidden;' + (cs.transform !== 'none' ? '' : 'transform:translate(0,0);'))
|
|
1769
|
+
c.appendChild(inner)
|
|
1770
|
+
} else c.setAttribute('style', c.getAttribute('style') + 'background-color:rgba(127,127,127,.15);')
|
|
1771
|
+
return c
|
|
1772
|
+
}
|
|
1773
|
+
if (tag === 'CANVAS' || tag === 'VIDEO' || tag === 'TEXTAREA') return c
|
|
1774
|
+
// 自己滚过的滚动区:里面的东西按滚动距离往回挪(图里没有滚动)
|
|
1775
|
+
const ox = el.namespaceURI !== SVGNS && ownScroll(el) ? el.scrollLeft : 0
|
|
1776
|
+
const oy = el.namespaceURI !== SVGNS && ownScroll(el) ? el.scrollTop : 0
|
|
1777
|
+
// 跟着内容滚的背景(background-attachment: local)也往回挪
|
|
1778
|
+
if ((ox || oy) && /local/.test(cs.backgroundAttachment)) {
|
|
1779
|
+
const att = cs.backgroundAttachment.split(',').map((x) => x.trim())
|
|
1780
|
+
const pos = cs.backgroundPosition.split(',').map((p, i) => {
|
|
1781
|
+
if (att[i % att.length] !== 'local') return p.trim()
|
|
1782
|
+
const [x = '0%', y = '0%'] = splitTop(p.trim())
|
|
1783
|
+
return `calc(${x} - ${ox}px) calc(${y} - ${oy}px)`
|
|
1784
|
+
})
|
|
1785
|
+
c.setAttribute('style', c.getAttribute('style') + `background-position:${pos.join(', ')};`)
|
|
1786
|
+
}
|
|
1787
|
+
let oa = false
|
|
1788
|
+
let of = false
|
|
1789
|
+
const kids = []
|
|
1790
|
+
// 自己是定位基准的:它的后代按它的滚动挪;不是的,接着用外面传进来的
|
|
1791
|
+
const hF = holds(cs, true)
|
|
1792
|
+
const hA = hF || cs.position !== 'static' // 和 holds(cs) 一样,少算一遍
|
|
1793
|
+
const sub = hA || hF ? { a: hA ? [ox, oy] : ctx.a, f: hF ? [ox, oy] : ctx.f } : ctx
|
|
1579
1794
|
for (const child of el.childNodes) {
|
|
1580
|
-
const k = walk(child)
|
|
1581
|
-
if (k)
|
|
1795
|
+
const k = walk(child, sub)
|
|
1796
|
+
if (k) kids.push([child, k])
|
|
1797
|
+
const o = child.nodeType === 1 && openOf.get(child)
|
|
1798
|
+
if (o) { oa = oa || o.a; of = of || o.f }
|
|
1582
1799
|
}
|
|
1800
|
+
if (ox || oy) scrollBack(kids, cs, ox, oy, stuck, openOf)
|
|
1801
|
+
for (const [, k] of kids) c.appendChild(k)
|
|
1802
|
+
if (hA) oa = false
|
|
1803
|
+
if (hF) of = false
|
|
1804
|
+
if (pa) oa = true
|
|
1805
|
+
if (pf) of = true
|
|
1806
|
+
if (cs.position === 'absolute') oa = true
|
|
1807
|
+
if (cs.position === 'fixed') of = true
|
|
1808
|
+
if (oa || of) openOf.set(el, { a: oa, f: of })
|
|
1583
1809
|
if (tag === 'SELECT') {
|
|
1584
1810
|
const o = el.selectedOptions && el.selectedOptions[0]
|
|
1585
1811
|
c.textContent = ''
|
|
@@ -1589,6 +1815,20 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1589
1815
|
}
|
|
1590
1816
|
return c
|
|
1591
1817
|
}
|
|
1818
|
+
// 内嵌页面的那一份:它自己的根元素也当滚动区(滚过就往回挪),不算外层页面的根
|
|
1819
|
+
let depth = 0
|
|
1820
|
+
const docs = new Set([document])
|
|
1821
|
+
function walkIn(doc) {
|
|
1822
|
+
// 内嵌页面里贴住的元素也按它现在的样子摆
|
|
1823
|
+
for (const [el, v] of stickyShift(doc)) stuck.set(el, v)
|
|
1824
|
+
docs.add(doc)
|
|
1825
|
+
if (!idPre.has(doc)) idPre.set(doc, `ddpf${++seq}-`)
|
|
1826
|
+
const r = doc.documentElement
|
|
1827
|
+
// 以整页为准的绝对定位跟着这一页滚
|
|
1828
|
+
const k = walk(r, { a: [r.scrollLeft, r.scrollTop], f: [0, 0] })
|
|
1829
|
+
if (k) k.setAttribute('style', (k.getAttribute('style') || '') + 'overflow:hidden;')
|
|
1830
|
+
return k
|
|
1831
|
+
}
|
|
1592
1832
|
|
|
1593
1833
|
try {
|
|
1594
1834
|
const top = topDark() // 和复制页面同一刻量(后面等图片、字体时页面可能又变了)
|
|
@@ -1596,19 +1836,23 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1596
1836
|
// 字体:页面样式表里的 @font-face,只带用到的字族
|
|
1597
1837
|
const faces = []
|
|
1598
1838
|
const used = [...families].join(',').toLowerCase()
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1839
|
+
// 内嵌页面的规则是它那个窗口的类,instanceof 认不出来,按 type 认(5 = @font-face)
|
|
1840
|
+
const isFace = (r) => r.type === 5 || r instanceof CSSFontFaceRule
|
|
1841
|
+
for (const d of docs) {
|
|
1842
|
+
for (const sheet of d.styleSheets) {
|
|
1843
|
+
let rules
|
|
1844
|
+
try { rules = sheet.cssRules } catch { continue }
|
|
1845
|
+
const base = d === document ? sheetHref(sheet) : sheet.href || d.baseURI
|
|
1846
|
+
const scan = (list) => {
|
|
1847
|
+
for (const r of list) {
|
|
1848
|
+
if (r.cssRules && !isFace(r)) { try { scan(r.cssRules) } catch { /* 读不了 */ } continue }
|
|
1849
|
+
if (!isFace(r)) continue
|
|
1850
|
+
const fam = (r.style.getPropertyValue('font-family') || '').replace(/['"]/g, '').trim().toLowerCase()
|
|
1851
|
+
if (fam && used.includes(fam)) faces.push(inlineUrls(r.cssText, base, inline))
|
|
1852
|
+
}
|
|
1609
1853
|
}
|
|
1854
|
+
scan(rules)
|
|
1610
1855
|
}
|
|
1611
|
-
scan(rules)
|
|
1612
1856
|
}
|
|
1613
1857
|
await Promise.all(jobs)
|
|
1614
1858
|
const fontCss = (await Promise.all(faces)).join('\n')
|
|
@@ -1618,7 +1862,10 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1618
1862
|
;(body || root).insertBefore(style, (body || root).firstChild)
|
|
1619
1863
|
if (sprites.childNodes.length) (body || root).appendChild(sprites)
|
|
1620
1864
|
if (!full && (scrollX || scrollY) && body) {
|
|
1621
|
-
|
|
1865
|
+
// 用 translate 属性挪:它在 transform / scale / rotate 外面一层,body 自己缩放旋转过也照样整体挪滚动距离
|
|
1866
|
+
const t0 = String(getComputedStyle(document.body).translate || 'none')
|
|
1867
|
+
const [tx = '0px', ty = '0px', tz = ''] = t0 === 'none' ? [] : splitTop(t0)
|
|
1868
|
+
body.setAttribute('style', body.getAttribute('style') + `translate:calc(${tx} - ${scrollX}px) calc(${ty} - ${scrollY}px)${tz ? ' ' + tz : ''};`)
|
|
1622
1869
|
}
|
|
1623
1870
|
const xml = new XMLSerializer().serializeToString(root)
|
|
1624
1871
|
const svg = `<svg xmlns="${SVGNS}" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}"><foreignObject x="0" y="0" width="100%" height="100%">${xml}</foreignObject></svg>`
|
|
@@ -1641,13 +1888,159 @@ background:rgba(20,20,22,.86);color:#e8e8ea;font:12px/18px -apple-system,system-
|
|
|
1641
1888
|
}
|
|
1642
1889
|
}
|
|
1643
1890
|
|
|
1891
|
+
/** 贴住的(sticky)元素现在被贴偏了多少:量现在的位置,临时取消贴住再量一次,差就是偏移(同一刻改回去,不重画) */
|
|
1892
|
+
function stickyShift(doc) {
|
|
1893
|
+
const out = new Map()
|
|
1894
|
+
const list = []
|
|
1895
|
+
for (const el of doc.querySelectorAll('body *')) {
|
|
1896
|
+
if (getComputedStyle(el).position === 'sticky') list.push(el)
|
|
1897
|
+
if (list.length >= 400) break
|
|
1898
|
+
}
|
|
1899
|
+
if (!list.length) return out
|
|
1900
|
+
const live = list.map((el) => [el.offsetLeft, el.offsetTop])
|
|
1901
|
+
const st = doc.createElement('style')
|
|
1902
|
+
st.textContent = '[data-dsh-unstick]{position:relative!important;inset:auto!important}'
|
|
1903
|
+
doc.documentElement.appendChild(st)
|
|
1904
|
+
try {
|
|
1905
|
+
for (const el of list) el.setAttribute('data-dsh-unstick', '')
|
|
1906
|
+
// 按排版坐标量(offsetLeft / offsetTop 不受缩放、旋转影响;相对最近的定位祖先,外层也贴住了的那份不会重复算)
|
|
1907
|
+
list.forEach((el, i) => {
|
|
1908
|
+
const dx = live[i][0] - el.offsetLeft
|
|
1909
|
+
const dy = live[i][1] - el.offsetTop
|
|
1910
|
+
if (dx || dy) out.set(el, [dx, dy])
|
|
1911
|
+
})
|
|
1912
|
+
} finally {
|
|
1913
|
+
for (const el of list) el.removeAttribute('data-dsh-unstick')
|
|
1914
|
+
st.remove()
|
|
1915
|
+
}
|
|
1916
|
+
return out
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/** 固定元素是不是被某个祖先(transform、filter 之类)接管了位置:接管了就跟着那个祖先走,不用挪 */
|
|
1920
|
+
function fixedHeld(el) {
|
|
1921
|
+
for (let a = el.parentElement; a; a = a.parentElement) if (holds(getComputedStyle(a), true)) return true
|
|
1922
|
+
return false
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* 滚过的滚动区:里面的东西按滚动距离往回挪(图里没有滚动)。kids:[原节点, 复制出来的] 按顺序,挪的时候可能换掉复制出来的那个。
|
|
1927
|
+
* 能不动定位就不动(改成相对定位会打乱上下层、把往外找基准的绝对定位框进来):
|
|
1928
|
+
* 竖着滚的块容器 / 竖排 flex,给第一个在文档流里的块加负的上边距,后面的跟着往上走;横着滚的横排 flex 同理加左边距,块容器给每个块加左边距。
|
|
1929
|
+
* 别的排法(grid 之类)退回一个个用相对定位挪;绝对 / 固定定位的(以滚动区为准的)改它自己的 left / top
|
|
1930
|
+
*/
|
|
1931
|
+
function scrollBack(kids, pcs, ox, oy, stuck, openOf) {
|
|
1932
|
+
const flex = /flex/.test(pcs.display)
|
|
1933
|
+
const grid = /grid/.test(pcs.display)
|
|
1934
|
+
const dir = String(pcs.flexDirection || 'row')
|
|
1935
|
+
const row = flex && dir === 'row'
|
|
1936
|
+
const col = flex && dir === 'column'
|
|
1937
|
+
const rtl = pcs.direction === 'rtl'
|
|
1938
|
+
const add = (k, css) => k.setAttribute('style', (k.getAttribute('style') || '') + css)
|
|
1939
|
+
const info = kids.map(([child, k]) => {
|
|
1940
|
+
if (child.nodeType !== 1) return { child, k, flow: true, text: !!(child.nodeValue || '').trim(), blank: !(child.nodeValue || '').trim() }
|
|
1941
|
+
const ccs = getComputedStyle(child)
|
|
1942
|
+
const out = ccs.position === 'absolute' || ccs.position === 'fixed'
|
|
1943
|
+
return { child, k, ccs, flow: !out, inline: !flex && !grid && /^inline/.test(ccs.display) }
|
|
1944
|
+
})
|
|
1945
|
+
const flow = info.filter((x) => x.flow && !x.blank)
|
|
1946
|
+
const first = flow[0]
|
|
1947
|
+
// 负边距能整列带着走的:普通的块(不浮动、不是 display:contents)
|
|
1948
|
+
const block = (x) => x && !x.text && !x.inline && x.ccs.float === 'none' && x.ccs.display !== 'contents'
|
|
1949
|
+
const floats = flow.some((x) => x.ccs && x.ccs.float !== 'none')
|
|
1950
|
+
// flex 主轴不是从头排的(居中、两端对齐……):加了负边距会重新排,退回一个个挪
|
|
1951
|
+
const startish = (v) => /^(normal|flex-start|start|left|stretch)$/.test(String(v || 'normal'))
|
|
1952
|
+
let fy = oy
|
|
1953
|
+
let fx = ox
|
|
1954
|
+
let spacer = null
|
|
1955
|
+
if (oy && block(first) && !floats && !flex && !grid) {
|
|
1956
|
+
// 块容器:前面垫一个高 0、上边距为负的块(自己是 BFC,边距不和后面的叠),后面的照原来的边距(包括往里叠的)往下排
|
|
1957
|
+
spacer = document.createElementNS(XHTML, 'div')
|
|
1958
|
+
spacer.setAttribute('style', `display:flow-root;height:0;margin:-${oy}px 0 0;padding:0;border:0`)
|
|
1959
|
+
fy = 0
|
|
1960
|
+
} else if (oy && block(first) && col && startish(pcs.justifyContent)) {
|
|
1961
|
+
// flex 里边距不叠:直接给第一个加
|
|
1962
|
+
add(first.k, `margin-top:${(parseFloat(first.ccs.marginTop) || 0) - oy}px;`)
|
|
1963
|
+
fy = 0
|
|
1964
|
+
}
|
|
1965
|
+
if (ox && !rtl && block(first) && row && startish(pcs.justifyContent)) {
|
|
1966
|
+
add(first.k, `margin-left:${(parseFloat(first.ccs.marginLeft) || 0) - ox}px;`)
|
|
1967
|
+
fx = 0
|
|
1968
|
+
} else if (ox && !rtl && !flex && !grid && !floats && flow.length && flow.every(block)) {
|
|
1969
|
+
// 块是一个个摆的,横着各挪各的(宽度已经按原来的定死,不会被负边距撑宽)
|
|
1970
|
+
for (const x of flow) add(x.k, `margin-left:${(parseFloat(x.ccs.marginLeft) || 0) - ox}px;`)
|
|
1971
|
+
fx = 0
|
|
1972
|
+
}
|
|
1973
|
+
// 绝对 / 固定定位的在 walk 里按定位基准挪过了;剩下没挪成的一个个挪
|
|
1974
|
+
for (const x of info) {
|
|
1975
|
+
if (x.blank || !x.flow) continue
|
|
1976
|
+
if (fx || fy) x.k = scrolledKid(x.k, x.child, pcs, fx, fy, stuck, openOf.get(x.child))
|
|
1977
|
+
}
|
|
1978
|
+
info.forEach((x, i) => { kids[i][1] = x.k })
|
|
1979
|
+
if (spacer) kids.splice(kids.findIndex(([c]) => c === first.child), 0, [null, spacer])
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/** 按空格拆一个 CSS 值(括号里的空格不拆:calc()、clamp() 整个算一段) */
|
|
1983
|
+
function splitTop(v) {
|
|
1984
|
+
const out = []
|
|
1985
|
+
let cur = ''
|
|
1986
|
+
let depth = 0
|
|
1987
|
+
for (const ch of String(v)) {
|
|
1988
|
+
if (ch === '(') depth++
|
|
1989
|
+
else if (ch === ')') depth = Math.max(0, depth - 1)
|
|
1990
|
+
if (/\s/.test(ch) && !depth) { if (cur) out.push(cur); cur = '' } else cur += ch
|
|
1991
|
+
}
|
|
1992
|
+
if (cur) out.push(cur)
|
|
1993
|
+
return out
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/** 定位元素往回挪 (dx, dy):只动挪了的那个方向;从右往左排的横向按 right 挪 */
|
|
1997
|
+
function shiftCss(cs, dx, dy) {
|
|
1998
|
+
let css = ''
|
|
1999
|
+
if (dy) { const y = parseFloat(cs.top); if (Number.isFinite(y)) css += `top:${y - dy}px;bottom:auto;` }
|
|
2000
|
+
if (dx) {
|
|
2001
|
+
if (cs.direction === 'rtl') { const r = parseFloat(cs.right); if (Number.isFinite(r)) css += `right:${r + dx}px;left:auto;` } else { const x = parseFloat(cs.left); if (Number.isFinite(x)) css += `left:${x - dx}px;right:auto;` }
|
|
2002
|
+
}
|
|
2003
|
+
return css
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* 滚过的滚动区里的一个孩子,用相对定位往回挪。
|
|
2008
|
+
* 不跟着滚的不挪:固定定位(滚动区没接管它)、包含块在滚动区外面的绝对定位。
|
|
2009
|
+
* 本来没定位、里面又有还在往外找基准的绝对定位的(比如以外层为准的弹层):改成相对定位会把它们框进来,这一块宁可不挪
|
|
2010
|
+
*/
|
|
2011
|
+
function scrolledKid(k, child, pcs, ox, oy, stuck, open) {
|
|
2012
|
+
if (child.nodeType === 3) {
|
|
2013
|
+
if (!child.nodeValue.trim()) return k
|
|
2014
|
+
const s = document.createElementNS(XHTML, 'span')
|
|
2015
|
+
s.setAttribute('style', `position:relative;left:${-ox}px;top:${-oy}px;`)
|
|
2016
|
+
s.appendChild(k)
|
|
2017
|
+
return s
|
|
2018
|
+
}
|
|
2019
|
+
if (child.nodeType !== 1) return k
|
|
2020
|
+
const ccs = getComputedStyle(child)
|
|
2021
|
+
const pos = ccs.position
|
|
2022
|
+
if ((pos === 'fixed' && !holds(pcs, true)) || (pos === 'absolute' && !holds(pcs))) return k
|
|
2023
|
+
if (pos === 'static' && open && open.a) return k
|
|
2024
|
+
let x = 0
|
|
2025
|
+
let y = 0
|
|
2026
|
+
if (stuck.has(child)) [x, y] = stuck.get(child)
|
|
2027
|
+
else if (pos !== 'static' && pos !== 'sticky') { x = parseFloat(ccs.left) || 0; y = parseFloat(ccs.top) || 0 }
|
|
2028
|
+
const keep = pos === 'absolute' || pos === 'fixed'
|
|
2029
|
+
// 从右往左排的:右边的偏移才算数
|
|
2030
|
+
const hx = ccs.direction === 'rtl' && !stuck.has(child) && pos !== 'static' && pos !== 'sticky'
|
|
2031
|
+
? `right:${(parseFloat(ccs.right) || 0) + ox}px;left:auto;`
|
|
2032
|
+
: `left:${x - ox}px;right:auto;`
|
|
2033
|
+
k.setAttribute('style', (k.getAttribute('style') || '') + `position:${keep ? pos : 'relative'};${ox ? hx : ''}${oy ? `top:${y - oy}px;bottom:auto;` : ''}`)
|
|
2034
|
+
return k
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1644
2037
|
async function onCapture(d) {
|
|
1645
2038
|
try {
|
|
1646
2039
|
const r = await capture(d)
|
|
1647
2040
|
// 整页图从文档顶上画起,链接加上滚动距离;只截视口时就是视口里的位置
|
|
1648
2041
|
const links = linksOf().map((l) => (d.full ? { ...l, x: l.x + scrollX, y: l.y + scrollY } : l))
|
|
1649
2042
|
.filter((l) => l.x < r.w && l.y < r.h && l.x + l.w > 0 && l.y + l.h > 0)
|
|
1650
|
-
post({ type: 'captured', id: d.id, png: r.png, w: r.w, h: r.h, scale: r.scale, title: document.title, links, top: r.top })
|
|
2043
|
+
post({ type: 'captured', id: d.id, png: r.png, w: r.w, h: r.h, scale: r.scale, title: document.title, links, top: r.top, href: here() })
|
|
1651
2044
|
} catch (e) {
|
|
1652
2045
|
post({ type: 'captured', id: d.id, error: String((e && e.message) || e) })
|
|
1653
2046
|
}
|