diff --git a/assets/css/routes/profile/layout.css b/assets/css/routes/profile/layout.css index 1a962bdf..77bfce81 100644 --- a/assets/css/routes/profile/layout.css +++ b/assets/css/routes/profile/layout.css @@ -94,6 +94,9 @@ box-shadow: 0 12px 32px rgba(0,0,0,.22); backdrop-filter: blur(12px); } +.aksrt-cover-menu__panel[hidden] { + display: none !important; +} .aksrt-cover-menu__panel [role="menuitem"] { display: flex; align-items: center; @@ -133,6 +136,11 @@ border-radius: 6px; } +#aksrt-bg-file-input[hidden], +#aksrt-avatar-file-input[hidden] { + display: none !important; +} + /* 对齐 zibll: 140px, 6px 圆角 */ .uc-header__avatar img { display: block; diff --git a/assets/js/lazy-image-attr.js b/assets/js/lazy-image-attr.js new file mode 100644 index 00000000..60149451 --- /dev/null +++ b/assets/js/lazy-image-attr.js @@ -0,0 +1,34 @@ +/** + * Build lazy-image attributes for dynamically rendered AKSRT markup. + * Zibll owns the lazySizes runtime; this file only provides the shared bridge. + */ +(function (window) { + 'use strict'; + + function escapeAttribute(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + } + + window.aksrtGetLazyImageAttr = function (key, src, className, size) { + var config = window.aksrtData || {}; + var scopes = config.lazyScopes || {}; + var classes = String(className || '').trim(); + var realSrc = escapeAttribute(src); + + if (!src || scopes[key] === false) { + return ' class="' + escapeAttribute(classes) + '" src="' + realSrc + '"'; + } + + var thumbs = config.lazyThumbs || {}; + var placeholder = key === 'lazy_avatar' + ? (config.lazyAvatar || thumbs.sm || '') + : (thumbs[size || 'default'] || thumbs.default || ''); + + return ' class="lazyload ' + escapeAttribute(classes) + '" src="' + + escapeAttribute(placeholder) + '" data-src="' + realSrc + '"'; + }; +})(window); diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index e2391cee..79e23d9a 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -165,7 +165,7 @@ trait ScriptsPart1b { wp_enqueue_script( 'aksrt-pay-vip', $base_uri . '/assets/js/pay-vip.js', - ['loader_js', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'], + ['loader_js', 'aksrt-lazy-image-attr', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'], (string) filemtime($vip_js), true ); @@ -415,7 +415,7 @@ trait ScriptsPart1b { wp_enqueue_script( 'aksrt-order-center', $base_uri . '/assets/js/order-center.js', - ['loader_js', 'aksrt-rehydrate', 'aksrt-core-request', 'aksrt-core-modal-runtime'], + ['loader_js', 'aksrt-lazy-image-attr', 'aksrt-rehydrate', 'aksrt-core-request', 'aksrt-core-modal-runtime'], (string) filemtime($order_center_js), true ); @@ -501,7 +501,7 @@ trait ScriptsPart1b { wp_enqueue_script( 'aksrt-page-navs', $base_uri . '/assets/js/page-navs.js', - [], + ['aksrt-lazy-image-attr'], (string) filemtime($page_navs_js), true ); @@ -755,7 +755,7 @@ trait ScriptsPart1b { wp_enqueue_script( 'aksrt-credits', $base_uri . '/assets/js/credits.js', - ['loader_js', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'], + ['loader_js', 'aksrt-lazy-image-attr', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'], file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version, true ); @@ -765,7 +765,7 @@ trait ScriptsPart1b { wp_enqueue_script( 'aksrt-user-badges', $base_uri . '/assets/js/user-badges.js', - ['loader_js', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-modal-triggers'], + ['loader_js', 'aksrt-lazy-image-attr', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-modal-triggers'], (string) filemtime($badges_js_path), true ); @@ -786,7 +786,7 @@ trait ScriptsPart1b { wp_enqueue_script( 'aksrt-user-badges', $base_uri . '/assets/js/user-badges.js', - ['loader_js', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-modal-triggers'], + ['loader_js', 'aksrt-lazy-image-attr', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-core-modal-triggers'], (string) filemtime($badges_js_path_global), true ); diff --git a/src/Core/ScriptsPart1c.php b/src/Core/ScriptsPart1c.php index e415ffa3..7df67aea 100644 --- a/src/Core/ScriptsPart1c.php +++ b/src/Core/ScriptsPart1c.php @@ -99,11 +99,20 @@ trait ScriptsPart1c { ] ); + $lazy_image_attr_path = get_template_directory() . '/assets/js/lazy-image-attr.js'; + wp_enqueue_script( + 'aksrt-lazy-image-attr', + $base_uri . '/assets/js/lazy-image-attr.js', + ['loader_js'], + file_exists($lazy_image_attr_path) ? (string) filemtime($lazy_image_attr_path) : $version, + true + ); + // AKSRT-only modules register after the Zibll loader; navigation is owned by main.min.js. wp_enqueue_script( 'aksrt-rehydrate', $base_uri . '/assets/js/rehydrate.js', - ['loader_js'], + ['loader_js', 'aksrt-lazy-image-attr'], file_exists(get_template_directory() . '/assets/js/rehydrate.js') ? (string) filemtime(get_template_directory() . '/assets/js/rehydrate.js') : $version, diff --git a/tests/widget-system/lazyload-contract.test.cjs b/tests/widget-system/lazyload-contract.test.cjs index 72e7125a..60edbd6e 100644 --- a/tests/widget-system/lazyload-contract.test.cjs +++ b/tests/widget-system/lazyload-contract.test.cjs @@ -9,6 +9,26 @@ const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); +function enqueueBlocksFor(source, file) { + const uri = `/assets/js/${file}`; + const blocks = []; + let cursor = 0; + + while (true) { + const uriAt = source.indexOf(uri, cursor); + if (uriAt === -1) break; + + const start = source.lastIndexOf('wp_enqueue_script(', uriAt); + const end = source.indexOf(');', uriAt); + assert.notEqual(start, -1, `missing wp_enqueue_script() for ${file}`); + assert.notEqual(end, -1, `unterminated wp_enqueue_script() for ${file}`); + blocks.push(source.slice(start, end + 2)); + cursor = uriAt + uri.length; + } + + return blocks; +} + const lazyDefaults = { lazy_posts_thumb: true, lazy_avatar: true, @@ -102,19 +122,30 @@ echo aksrt_lazy_content_images($html); assert.doesNotMatch(avatar, /loading=/); }); -test('one lazySizes runtime owns dynamic media, backgrounds, videos, and IAS', () => { - const runtime = read('assets/js/lazysizes.js'); +test('Zibll owns lazySizes while AKSRT consumers share one image-attribute bridge', () => { + const runtime = read('assets/vendor/zibll/js/main.js'); + const bridge = read('assets/js/lazy-image-attr.js'); const scripts = read('src/Core/ScriptsPart1c.php'); + const routeScripts = read('src/Core/ScriptsPart1b.php'); const pagination = read('assets/js/core/pagination.js'); const widgets = read('assets/widgets/frontend.js'); - assert.match(scripts, /'aksrt-lazysizes'/); - assert.match(scripts, /'aksrt-rehydrate'[\s\S]*?\['aksrt-lazysizes'\]/); + assert.match(runtime, /window\.lazySizes = lazySizes/); + assert.match(bridge, /window\.aksrtGetLazyImageAttr = function/); + assert.match(scripts, /'aksrt-lazy-image-attr'/); + assert.match(scripts, /'aksrt-rehydrate'[\s\S]*?\['loader_js', 'aksrt-lazy-image-attr'\]/); + for (const file of ['pay-vip.js', 'order-center.js', 'page-navs.js', 'credits.js', 'user-badges.js']) { + const blocks = enqueueBlocksFor(routeScripts, file); + assert.ok(blocks.length > 0, `${file} must be enqueued`); + for (const block of blocks) { + assert.match(block, /aksrt-lazy-image-attr/, `${file} must depend on the image-attribute bridge`); + } + } assert.match(runtime, /lazybeforeunveil/); - assert.match(runtime, /source\[data-src\],source\[data-srcset\]/); - assert.match(runtime, /data-bg/); - assert.match(runtime, /lazyload-action/); - assert.match(runtime, /element\.hasAttribute\('remote-box'\)[\s\S]*?element\.click\(\)/); + assert.match(runtime, /getElementsByTagName\('source'\), handleSources/); + assert.match(runtime, /var bg = _this\.attr\('data-bg'\)[\s\S]*?background-image/); + assert.match(runtime, /var lazyload_action = _this\.attr\('lazyload-action'\)/); + assert.match(runtime, /if \(_this\.attr\('remote-box'\)\)[\s\S]*?_this\.click\(\)/); assert.doesNotMatch(pagination, /IntersectionObserver|new Image\(/); assert.doesNotMatch(widgets, /IntersectionObserver/); }); @@ -130,9 +161,9 @@ test('post lists have no second lazy switch and keep Zibll fallback thumbnails', for (const source of [config, postData, markup, phpSchema, vueSchema]) { assert.equal(source.includes(legacyKey), false); } - assert.match(postData, /aksrt_get_lazy_attr\('lazy_posts_thumb', \$img, \$fit_class\)/); - assert.match(markup, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['img_url'\], \$fit_class\)/); - assert.match(config, /\$fallback\s*=\s*sanitize_key[\s\S]*?\$fallback\s*===\s*'placeholder'[\s\S]*?aksrt_get_lazy_thumb\(\)/); + assert.match(postData, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/); + assert.match(markup, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/); + assert.match(config, /\$primary = aksrt_renderer_spare_image\(\$thumb\);[\s\S]*?\$primary = \(string\) aksrt_get_lazy_thumb\(\);/); assert.doesNotMatch(config, /rand_thumb/); }); @@ -148,7 +179,7 @@ test('legacy lazy identifiers and theme-native loading attributes are gone', () 'thumb_' + 'lazy', ]; for (const token of forbidden) assert.equal(source.includes(token), false, token); - assert.doesNotMatch(source, /aksrt-lazy(?!sizes)/); + assert.doesNotMatch(source, /aksrt-lazy(?!sizes|-image-attr)/); assert.doesNotMatch(source, /aksrt-loaded/); assert.doesNotMatch(source, /loading=["'](?:lazy|eager)["']/); assert.doesNotMatch(source, /\.loading\s*=\s*["'](?:lazy|eager)["']/); diff --git a/tests/widget-system/post-visual-contract.test.cjs b/tests/widget-system/post-visual-contract.test.cjs index be156ddf..cf3c5b0c 100644 --- a/tests/widget-system/post-visual-contract.test.cjs +++ b/tests/widget-system/post-visual-contract.test.cjs @@ -289,6 +289,7 @@ test('profile routes load versioned leaf styles and keep dialogs hidden initiall const accountModals = read('inc/helpers/user-center-renderers/profile-account-modals.php'); const userCenterJs = read('assets/js/routes/user-center.js'); const userHomeCss = read('assets/css/routes/user-home.css'); + const profileLayoutCss = read('assets/css/routes/profile/layout.css'); assert.ok(scripts.includes("$profile_parts = ['layout', 'lists', 'modals'];")); for (const part of ['user-center/shell', 'user-center/credits', 'user-center/vip', 'user-center/level', 'user-center/settings', 'revenue', 'user-center/revenue', 'user-center/revenue-earnings', 'user-center/responsive']) { @@ -304,4 +305,6 @@ test('profile routes load versioned leaf styles and keep dialogs hidden initiall assert.match(accountModals, /id="email-modal" class="aksrt-modal" hidden/); assert.match(userCenterJs, /function closeModal\(m\)[\s\S]*?m\.hidden = true;/); assert.match(userHomeCss, /\.aksrt-profile-details-modal\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/); + assert.match(profileLayoutCss, /\.aksrt-cover-menu__panel\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/); + assert.match(profileLayoutCss, /#aksrt-bg-file-input\[hidden\],[\s\S]*?#aksrt-avatar-file-input\[hidden\][\s\S]*?display:\s*none\s*!important;/); });