@elementor/editor-audits 4.4.0-1105 → 4.4.0-1107
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/dist/index.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +491 -294
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +480 -273
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -13
- package/src/api/page-context-client.ts +8 -5
- package/src/audits/accessibility-statement.ts +39 -0
- package/src/audits/accessibility-widget.ts +41 -0
- package/src/audits/google-consent-mode.ts +4 -7
- package/src/audits/images-too-large.ts +23 -9
- package/src/components/violation-cta-button.tsx +3 -2
- package/src/components/violation-row.tsx +13 -4
- package/src/register-audits.ts +4 -0
- package/src/runner.ts +9 -5
- package/src/types.ts +8 -1
- package/src/utils/atomic-background-image-sources.ts +56 -0
- package/src/utils/atomic-image-sources.ts +62 -0
- package/src/utils/background-image-sources.ts +32 -0
- package/src/utils/image-alt.ts +3 -1
- package/src/utils/image-like-sources.ts +5 -2
- package/src/utils/image-size-key.ts +8 -0
- package/src/utils/page-attachments.ts +16 -7
- package/src/utils/scan-accessibility-widgets.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-audits",
|
|
3
|
-
"version": "4.4.0-
|
|
3
|
+
"version": "4.4.0-1107",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Elementor Team",
|
|
6
6
|
"homepage": "https://elementor.com/",
|
|
@@ -39,19 +39,21 @@
|
|
|
39
39
|
"dev": "tsup --config=../../tsup.dev.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@elementor/editor": "4.4.0-
|
|
43
|
-
"@elementor/editor-mcp": "4.4.0-
|
|
44
|
-
"@elementor/editor-app-bar": "4.4.0-
|
|
45
|
-
"@elementor/editor-elements": "4.4.0-
|
|
46
|
-
"@elementor/editor-floating-panels": "4.4.0-
|
|
47
|
-
"@elementor/editor-notifications": "4.4.0-
|
|
48
|
-
"@elementor/editor-
|
|
49
|
-
"@elementor/
|
|
50
|
-
"@elementor/
|
|
42
|
+
"@elementor/editor": "4.4.0-1107",
|
|
43
|
+
"@elementor/editor-mcp": "4.4.0-1107",
|
|
44
|
+
"@elementor/editor-app-bar": "4.4.0-1107",
|
|
45
|
+
"@elementor/editor-elements": "4.4.0-1107",
|
|
46
|
+
"@elementor/editor-floating-panels": "4.4.0-1107",
|
|
47
|
+
"@elementor/editor-notifications": "4.4.0-1107",
|
|
48
|
+
"@elementor/editor-props": "4.4.0-1107",
|
|
49
|
+
"@elementor/editor-styles": "4.4.0-1107",
|
|
50
|
+
"@elementor/editor-v1-adapters": "4.4.0-1107",
|
|
51
|
+
"@elementor/events": "4.4.0-1107",
|
|
52
|
+
"@elementor/http-client": "4.4.0-1107",
|
|
51
53
|
"@elementor/icons": "~1.77.0",
|
|
52
|
-
"@elementor/locations": "4.4.0-
|
|
53
|
-
"@elementor/session": "4.4.0-
|
|
54
|
-
"@elementor/store": "4.4.0-
|
|
54
|
+
"@elementor/locations": "4.4.0-1107",
|
|
55
|
+
"@elementor/session": "4.4.0-1107",
|
|
56
|
+
"@elementor/store": "4.4.0-1107",
|
|
55
57
|
"@elementor/ui": "1.37.5"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
@@ -4,13 +4,16 @@ import { type PageContextResponse } from '../types';
|
|
|
4
4
|
import { isNonceInvalidError, refreshAuditsNonce } from '../utils/session-expiration';
|
|
5
5
|
import { getWindowConfig } from '../utils/window-config';
|
|
6
6
|
|
|
7
|
-
export async function fetchPageContext(
|
|
8
|
-
|
|
7
|
+
export async function fetchPageContext(
|
|
8
|
+
documentId: number,
|
|
9
|
+
imageSizeRequests: string[]
|
|
10
|
+
): Promise< PageContextResponse > {
|
|
11
|
+
return requestPageContext( documentId, imageSizeRequests, true );
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
async function requestPageContext(
|
|
12
15
|
documentId: number,
|
|
13
|
-
|
|
16
|
+
imageSizeRequests: string[],
|
|
14
17
|
allowNonceRetry: boolean
|
|
15
18
|
): Promise< PageContextResponse > {
|
|
16
19
|
const { restNamespace, nonce } = getWindowConfig();
|
|
@@ -20,7 +23,7 @@ async function requestPageContext(
|
|
|
20
23
|
const response = await httpService().get< PageContextResponse >( url, {
|
|
21
24
|
params: {
|
|
22
25
|
document_id: documentId,
|
|
23
|
-
|
|
26
|
+
image_size_requests: imageSizeRequests,
|
|
24
27
|
},
|
|
25
28
|
headers: { 'X-WP-Nonce': nonce },
|
|
26
29
|
} );
|
|
@@ -33,6 +36,6 @@ async function requestPageContext(
|
|
|
33
36
|
|
|
34
37
|
await refreshAuditsNonce();
|
|
35
38
|
|
|
36
|
-
return requestPageContext( documentId,
|
|
39
|
+
return requestPageContext( documentId, imageSizeRequests, false );
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/accessibility-statement',
|
|
7
|
+
title: __( 'Create an accessibility statement', 'elementor' ),
|
|
8
|
+
description: __(
|
|
9
|
+
'An accessibility statement showcases your efforts to create an inclusive online space, highlighting helpful features and a commitment to accessibility.',
|
|
10
|
+
'elementor'
|
|
11
|
+
),
|
|
12
|
+
fixHint: __( 'Use Ally to create and publish an accessibility statement for your site.', 'elementor' ),
|
|
13
|
+
categories: [ 'accessibility' ],
|
|
14
|
+
severity: 'info',
|
|
15
|
+
weight: 1,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
const isReady = ctx.pageContext.ally_plugin_active;
|
|
18
|
+
|
|
19
|
+
if ( isReady && ctx.pageContext.ally_accessibility_statement_created ) {
|
|
20
|
+
return { status: 'pass' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
status: 'fail',
|
|
25
|
+
violations: [
|
|
26
|
+
{
|
|
27
|
+
auditId: audit.id,
|
|
28
|
+
label: __( 'No accessibility statement has been created for this site.', 'elementor' ),
|
|
29
|
+
externalUrl: isReady
|
|
30
|
+
? ctx.pageContext.ally_accessibility_statement_url
|
|
31
|
+
: ctx.pageContext.ally_plugin_url,
|
|
32
|
+
ctaLabel: isReady ? __( 'Create', 'elementor' ) : undefined,
|
|
33
|
+
secondaryCtaLabel: __( 'Learn more', 'elementor' ),
|
|
34
|
+
secondaryCtaUrl: 'https://go.elementor.com/acc-plg-learn-more',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
import { hasAccessibilityWidgetScript } from '../utils/scan-accessibility-widgets';
|
|
5
|
+
|
|
6
|
+
export const audit: Audit = {
|
|
7
|
+
id: 'audits/accessibility-widget',
|
|
8
|
+
title: __( 'Add an accessibility widget', 'elementor' ),
|
|
9
|
+
description: __( 'Make your site more inclusive by adding an accessibility widget.', 'elementor' ),
|
|
10
|
+
fixHint: __( 'Activate Ally’s accessibility widget to give visitors on-page accessibility tools.', 'elementor' ),
|
|
11
|
+
categories: [ 'accessibility' ],
|
|
12
|
+
severity: 'info',
|
|
13
|
+
weight: 1,
|
|
14
|
+
evaluate: ( ctx ) => {
|
|
15
|
+
if ( ! ctx.pageContext.frontend_url ) {
|
|
16
|
+
return { status: 'skipped', reason: __( 'Page is not published.', 'elementor' ) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if ( ! ctx.renderedHtml ) {
|
|
20
|
+
return { status: 'skipped', reason: __( 'Could not fetch the published page.', 'elementor' ) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ( hasAccessibilityWidgetScript( ctx.renderedHtml ) ) {
|
|
24
|
+
return { status: 'pass' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isReady = ctx.pageContext.ally_plugin_active;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
status: 'fail',
|
|
31
|
+
violations: [
|
|
32
|
+
{
|
|
33
|
+
auditId: audit.id,
|
|
34
|
+
label: __( 'No accessibility widget is active on this site.', 'elementor' ),
|
|
35
|
+
externalUrl: isReady ? ctx.pageContext.ally_widget_settings_url : ctx.pageContext.ally_plugin_url,
|
|
36
|
+
ctaLabel: __( 'Activate', 'elementor' ),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { __ } from '@wordpress/i18n';
|
|
2
2
|
|
|
3
3
|
import { type Audit } from '../types';
|
|
4
|
-
import { fetchRenderedHtml } from '../utils/fetch-rendered-html';
|
|
5
4
|
import { hasConsentDefaultCall, hasGoogleTracking } from '../utils/scan-consent-signals';
|
|
6
5
|
|
|
7
6
|
export const audit: Audit = {
|
|
@@ -18,22 +17,20 @@ export const audit: Audit = {
|
|
|
18
17
|
categories: [ 'compliance' ],
|
|
19
18
|
severity: 'warning',
|
|
20
19
|
weight: 1,
|
|
21
|
-
evaluate:
|
|
20
|
+
evaluate: ( ctx ) => {
|
|
22
21
|
if ( ! ctx.pageContext.frontend_url ) {
|
|
23
22
|
return { status: 'skipped', reason: __( 'Page is not published.', 'elementor' ) };
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if ( ! html ) {
|
|
25
|
+
if ( ! ctx.renderedHtml ) {
|
|
29
26
|
return { status: 'skipped', reason: __( 'Could not fetch the published page.', 'elementor' ) };
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
if ( ! hasGoogleTracking(
|
|
29
|
+
if ( ! hasGoogleTracking( ctx.renderedHtml ) ) {
|
|
33
30
|
return { status: 'skipped', reason: __( 'No Google tracking product detected.', 'elementor' ) };
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
if ( hasConsentDefaultCall(
|
|
33
|
+
if ( hasConsentDefaultCall( ctx.renderedHtml ) ) {
|
|
37
34
|
return { status: 'pass' };
|
|
38
35
|
}
|
|
39
36
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { __, sprintf } from '@wordpress/i18n';
|
|
2
2
|
|
|
3
|
-
import { type Audit, type AuditViolation } from '../types';
|
|
4
|
-
import {
|
|
3
|
+
import { type Audit, type AuditViolation, type ElementSnapshotNode } from '../types';
|
|
4
|
+
import { walkAtomicBackgroundImageSources } from '../utils/atomic-background-image-sources';
|
|
5
|
+
import { walkAtomicImageSources } from '../utils/atomic-image-sources';
|
|
6
|
+
import { walkBackgroundImageSources } from '../utils/background-image-sources';
|
|
7
|
+
import { type ImageLikeMedia, walkImageLikeSources } from '../utils/image-like-sources';
|
|
8
|
+
import { buildImageSizeKey } from '../utils/image-size-key';
|
|
5
9
|
|
|
6
10
|
const SIZE_THRESHOLD_BYTES = 500 * 1024;
|
|
7
11
|
const BYTES_PER_KB = 1024;
|
|
@@ -15,21 +19,22 @@ export const audit: Audit = {
|
|
|
15
19
|
severity: 'warning',
|
|
16
20
|
weight: 7,
|
|
17
21
|
evaluate: ( ctx ) => {
|
|
18
|
-
if ( ! hasPageImages( ctx.elements.tree ) ) {
|
|
19
|
-
return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
22
|
const widgetMaxKb = new Map< string, number >();
|
|
23
23
|
let oversizedImageCount = 0;
|
|
24
|
+
let hasAnyImage = false;
|
|
25
|
+
|
|
26
|
+
const evaluateSource = ( node: ElementSnapshotNode, media: ImageLikeMedia ) => {
|
|
27
|
+
if ( media.id || media.url ) {
|
|
28
|
+
hasAnyImage = true;
|
|
29
|
+
}
|
|
24
30
|
|
|
25
|
-
walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => {
|
|
26
31
|
const id = media.id;
|
|
27
32
|
|
|
28
33
|
if ( ! id ) {
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
const size = ctx.pageContext.image_sizes[ id ];
|
|
37
|
+
const size = ctx.pageContext.image_sizes[ buildImageSizeKey( { id, size: media.size } ) ];
|
|
33
38
|
|
|
34
39
|
if ( ! size || size.filesize_bytes <= SIZE_THRESHOLD_BYTES ) {
|
|
35
40
|
return;
|
|
@@ -40,7 +45,16 @@ export const audit: Audit = {
|
|
|
40
45
|
const kb = Math.round( size.filesize_bytes / BYTES_PER_KB );
|
|
41
46
|
const currentMax = widgetMaxKb.get( node.id ) ?? 0;
|
|
42
47
|
widgetMaxKb.set( node.id, Math.max( currentMax, kb ) );
|
|
43
|
-
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
51
|
+
walkBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
52
|
+
walkAtomicImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
53
|
+
walkAtomicBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
54
|
+
|
|
55
|
+
if ( ! hasAnyImage ) {
|
|
56
|
+
return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
|
|
57
|
+
}
|
|
44
58
|
|
|
45
59
|
const violations: AuditViolation[] = Array.from( widgetMaxKb.entries() ).map( ( [ elementId, kb ] ) => ( {
|
|
46
60
|
auditId: audit.id,
|
|
@@ -4,9 +4,10 @@ import { Button } from '@elementor/ui';
|
|
|
4
4
|
type Props = {
|
|
5
5
|
ctaLabel: string;
|
|
6
6
|
externalUrl: string;
|
|
7
|
+
variant?: 'outlined' | 'text';
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
|
|
10
|
+
export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined' }: Props ) {
|
|
10
11
|
const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
|
|
11
12
|
event.stopPropagation();
|
|
12
13
|
event.preventDefault();
|
|
@@ -15,7 +16,7 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
|
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
|
-
<Button variant=
|
|
19
|
+
<Button variant={ variant } color="secondary" size="small" onClick={ handleClick }>
|
|
19
20
|
{ ctaLabel }
|
|
20
21
|
</Button>
|
|
21
22
|
);
|
|
@@ -164,10 +164,19 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
164
164
|
/>
|
|
165
165
|
) }
|
|
166
166
|
{ violation.ctaLabel && violation.externalUrl ? (
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
<Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
|
|
168
|
+
<ViolationCtaButton
|
|
169
|
+
ctaLabel={ violation.ctaLabel }
|
|
170
|
+
externalUrl={ violation.externalUrl }
|
|
171
|
+
/>
|
|
172
|
+
{ violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
|
|
173
|
+
<ViolationCtaButton
|
|
174
|
+
ctaLabel={ violation.secondaryCtaLabel }
|
|
175
|
+
externalUrl={ violation.secondaryCtaUrl }
|
|
176
|
+
variant="text"
|
|
177
|
+
/>
|
|
178
|
+
) }
|
|
179
|
+
</Box>
|
|
171
180
|
) : (
|
|
172
181
|
<EyeIcon
|
|
173
182
|
className="violation-hover-icon"
|
package/src/register-audits.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as accessibilityPolicy from './audits/accessibility-policy';
|
|
2
|
+
import * as accessibilityStatement from './audits/accessibility-statement';
|
|
3
|
+
import * as accessibilityWidget from './audits/accessibility-widget';
|
|
2
4
|
import * as cookiePolicy from './audits/cookie-policy';
|
|
3
5
|
import * as deepNesting from './audits/deep-nesting';
|
|
4
6
|
import * as defaultDesignSystem from './audits/default-design-system';
|
|
@@ -48,6 +50,8 @@ const AUDITS: Audit[] = [
|
|
|
48
50
|
|
|
49
51
|
privacyPolicy.audit,
|
|
50
52
|
accessibilityPolicy.audit,
|
|
53
|
+
accessibilityStatement.audit,
|
|
54
|
+
accessibilityWidget.audit,
|
|
51
55
|
cookiePolicy.audit,
|
|
52
56
|
scanForCookies.audit,
|
|
53
57
|
googleConsentMode.audit,
|
package/src/runner.ts
CHANGED
|
@@ -12,19 +12,23 @@ import {
|
|
|
12
12
|
type PageAuditReport,
|
|
13
13
|
} from './types';
|
|
14
14
|
import { computeReport } from './utils/compute-report';
|
|
15
|
-
import {
|
|
15
|
+
import { fetchRenderedHtml } from './utils/fetch-rendered-html';
|
|
16
|
+
import { extractImageSizeRequests } from './utils/page-attachments';
|
|
16
17
|
import { readKitSnapshot } from './utils/read-kit-snapshot';
|
|
17
18
|
import { buildSnapshotTree } from './utils/v1-snapshot';
|
|
18
19
|
|
|
19
20
|
export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
|
|
20
21
|
const tree = buildSnapshotTree( getElements() );
|
|
21
|
-
const
|
|
22
|
-
const pageContext = await fetchPageContext( documentId,
|
|
22
|
+
const imageSizeRequests = extractImageSizeRequests( tree );
|
|
23
|
+
const pageContext = await fetchPageContext( documentId, imageSizeRequests );
|
|
23
24
|
|
|
24
25
|
const elements: ElementsModelSnapshot = { documentId, tree };
|
|
25
|
-
const kit: KitSnapshot = await
|
|
26
|
+
const [ kit, renderedHtml ]: [ KitSnapshot, string | null ] = await Promise.all( [
|
|
27
|
+
readKitSnapshot( pageContext.kit_id ),
|
|
28
|
+
fetchRenderedHtml( pageContext.frontend_url ),
|
|
29
|
+
] );
|
|
26
30
|
|
|
27
|
-
const ctx: AuditContext = { documentId, elements, pageContext, kit };
|
|
31
|
+
const ctx: AuditContext = { documentId, elements, pageContext, kit, renderedHtml };
|
|
28
32
|
const registered = getRegisteredAudits();
|
|
29
33
|
|
|
30
34
|
const auditResults: AuditRun[] = await Promise.all(
|
package/src/types.ts
CHANGED
|
@@ -40,14 +40,17 @@ export type AuditViolation = {
|
|
|
40
40
|
angieFix?: boolean;
|
|
41
41
|
angiePrompt?: string;
|
|
42
42
|
ctaLabel?: string;
|
|
43
|
+
secondaryCtaLabel?: string;
|
|
44
|
+
secondaryCtaUrl?: string;
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
export type PageContextResponse = {
|
|
46
48
|
post_title: string | null;
|
|
47
49
|
post_excerpt: string | null;
|
|
48
50
|
featured_image_id: number | null;
|
|
51
|
+
/** Keyed by the composite `buildImageSizeKey` result (`"{attachmentId}:{size}"`), since one attachment can resolve to different files at different sizes. */
|
|
49
52
|
image_sizes: Record<
|
|
50
|
-
|
|
53
|
+
string,
|
|
51
54
|
{
|
|
52
55
|
width: number;
|
|
53
56
|
height: number;
|
|
@@ -65,6 +68,9 @@ export type PageContextResponse = {
|
|
|
65
68
|
privacy_settings_url: string;
|
|
66
69
|
ally_plugin_active: boolean;
|
|
67
70
|
ally_plugin_url: string;
|
|
71
|
+
ally_accessibility_statement_created: boolean;
|
|
72
|
+
ally_accessibility_statement_url: string;
|
|
73
|
+
ally_widget_settings_url: string;
|
|
68
74
|
cookiez_plugin_active: boolean;
|
|
69
75
|
cookiez_plugin_url: string;
|
|
70
76
|
cookiez_plugin_installed: boolean;
|
|
@@ -108,6 +114,7 @@ export type AuditContext = {
|
|
|
108
114
|
elements: ElementsModelSnapshot;
|
|
109
115
|
pageContext: PageContextResponse;
|
|
110
116
|
kit: KitSnapshot;
|
|
117
|
+
renderedHtml: string | null;
|
|
111
118
|
};
|
|
112
119
|
|
|
113
120
|
export type PageAuditReport = {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getElementStyles } from '@elementor/editor-elements';
|
|
2
|
+
import {
|
|
3
|
+
backgroundImageOverlayPropTypeUtil,
|
|
4
|
+
backgroundOverlayPropTypeUtil,
|
|
5
|
+
backgroundPropTypeUtil,
|
|
6
|
+
} from '@elementor/editor-props';
|
|
7
|
+
import { type StyleDefinition } from '@elementor/editor-styles';
|
|
8
|
+
|
|
9
|
+
import { type ElementSnapshotNode } from '../types';
|
|
10
|
+
import { extractAtomicImageMedia } from './atomic-image-sources';
|
|
11
|
+
import { type ImageLikeMedia } from './image-like-sources';
|
|
12
|
+
import { walkElements } from './walk';
|
|
13
|
+
|
|
14
|
+
export type AtomicBackgroundImageSourceVisit = {
|
|
15
|
+
node: ElementSnapshotNode;
|
|
16
|
+
media: ImageLikeMedia;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function walkAtomicBackgroundImageSources(
|
|
20
|
+
tree: ElementSnapshotNode[],
|
|
21
|
+
visit: ( source: AtomicBackgroundImageSourceVisit ) => void
|
|
22
|
+
): void {
|
|
23
|
+
walkElements( tree, ( node ) => {
|
|
24
|
+
const styles = getElementStyles( node.id );
|
|
25
|
+
|
|
26
|
+
if ( ! styles ) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for ( const media of extractBackgroundImageMedia( styles ) ) {
|
|
31
|
+
visit( { node, media } );
|
|
32
|
+
}
|
|
33
|
+
} );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractBackgroundImageMedia( styles: Record< string, StyleDefinition > ): ImageLikeMedia[] {
|
|
37
|
+
const sources: ImageLikeMedia[] = [];
|
|
38
|
+
|
|
39
|
+
for ( const style of Object.values( styles ) ) {
|
|
40
|
+
for ( const variant of style.variants ) {
|
|
41
|
+
const background = backgroundPropTypeUtil.extract( variant.props.background );
|
|
42
|
+
const overlays = backgroundOverlayPropTypeUtil.extract( background?.[ 'background-overlay' ] ) ?? [];
|
|
43
|
+
|
|
44
|
+
for ( const overlay of overlays ) {
|
|
45
|
+
const imageOverlay = backgroundImageOverlayPropTypeUtil.extract( overlay );
|
|
46
|
+
const media = extractAtomicImageMedia( imageOverlay?.image );
|
|
47
|
+
|
|
48
|
+
if ( media ) {
|
|
49
|
+
sources.push( media );
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return sources;
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
imageAttachmentIdPropType,
|
|
3
|
+
imagePropTypeUtil,
|
|
4
|
+
imageSrcPropTypeUtil,
|
|
5
|
+
stringPropTypeUtil,
|
|
6
|
+
urlPropTypeUtil,
|
|
7
|
+
} from '@elementor/editor-props';
|
|
8
|
+
|
|
9
|
+
import { type ElementSnapshotNode } from '../types';
|
|
10
|
+
import { type ImageLikeMedia } from './image-like-sources';
|
|
11
|
+
import { walkElements } from './walk';
|
|
12
|
+
|
|
13
|
+
const ATOMIC_IMAGE_PROP_KEYS = [ 'image', 'poster' ] as const;
|
|
14
|
+
|
|
15
|
+
export type AtomicImageSourceVisit = {
|
|
16
|
+
node: ElementSnapshotNode;
|
|
17
|
+
media: ImageLikeMedia;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function walkAtomicImageSources(
|
|
21
|
+
tree: ElementSnapshotNode[],
|
|
22
|
+
visit: ( source: AtomicImageSourceVisit ) => void
|
|
23
|
+
): void {
|
|
24
|
+
walkElements( tree, ( node ) => {
|
|
25
|
+
if ( node.elType !== 'widget' ) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for ( const key of ATOMIC_IMAGE_PROP_KEYS ) {
|
|
30
|
+
const media = extractAtomicImageMedia( node.settings[ key ] );
|
|
31
|
+
|
|
32
|
+
if ( media ) {
|
|
33
|
+
visit( { node, media } );
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} );
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia | null {
|
|
40
|
+
const image = imagePropTypeUtil.extract( imageProp );
|
|
41
|
+
|
|
42
|
+
if ( ! image ) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const src = imageSrcPropTypeUtil.extract( image.src );
|
|
47
|
+
|
|
48
|
+
if ( ! src ) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const id = imageAttachmentIdPropType.extract( src.id ) ?? undefined;
|
|
53
|
+
const url = urlPropTypeUtil.extract( src.url ) ?? undefined;
|
|
54
|
+
|
|
55
|
+
if ( ! id && ! url ) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
|
|
60
|
+
|
|
61
|
+
return { id, url, size };
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
import { type ImageLikeMedia } from './image-like-sources';
|
|
3
|
+
import { walkElements } from './walk';
|
|
4
|
+
|
|
5
|
+
const BACKGROUND_IMAGE_SETTING_KEYS = [
|
|
6
|
+
'background_image',
|
|
7
|
+
'background_hover_image',
|
|
8
|
+
'background_overlay_image',
|
|
9
|
+
'background_overlay_hover_image',
|
|
10
|
+
'_background_image',
|
|
11
|
+
'_background_hover_image',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type BackgroundImageSourceVisit = {
|
|
15
|
+
node: ElementSnapshotNode;
|
|
16
|
+
media: ImageLikeMedia;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function walkBackgroundImageSources(
|
|
20
|
+
tree: ElementSnapshotNode[],
|
|
21
|
+
visit: ( source: BackgroundImageSourceVisit ) => void
|
|
22
|
+
): void {
|
|
23
|
+
walkElements( tree, ( node ) => {
|
|
24
|
+
for ( const key of BACKGROUND_IMAGE_SETTING_KEYS ) {
|
|
25
|
+
const media = node.settings[ key ] as ImageLikeMedia | undefined;
|
|
26
|
+
|
|
27
|
+
if ( media?.id || media?.url ) {
|
|
28
|
+
visit( { node, media } );
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} );
|
|
32
|
+
}
|
package/src/utils/image-alt.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PageContextResponse } from '../types';
|
|
2
2
|
import { type ImageLikeMedia } from './image-like-sources';
|
|
3
|
+
import { buildImageSizeKey } from './image-size-key';
|
|
3
4
|
|
|
4
5
|
export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContextResponse ): boolean {
|
|
5
6
|
if ( ! media.id && ! media.url ) {
|
|
@@ -7,7 +8,8 @@ export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContex
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
if ( media.id ) {
|
|
10
|
-
const
|
|
11
|
+
const key = buildImageSizeKey( { id: media.id, size: media.size } );
|
|
12
|
+
const alt = pageContext.image_sizes[ key ]?.alt ?? '';
|
|
11
13
|
return alt.trim().length > 0;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -7,6 +7,7 @@ export type ImageLikeMedia = {
|
|
|
7
7
|
id?: number;
|
|
8
8
|
url?: string;
|
|
9
9
|
alt?: string;
|
|
10
|
+
size?: string;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export type ImageLikeSourceVisit = {
|
|
@@ -51,7 +52,7 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
|
|
|
51
52
|
const image = node.settings.image as ImageLikeMedia | undefined;
|
|
52
53
|
|
|
53
54
|
if ( image?.id || image?.url ) {
|
|
54
|
-
sources.push( image );
|
|
55
|
+
sources.push( { ...image, size: node.settings.image_size as string | undefined } );
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
|
|
@@ -59,9 +60,11 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
|
|
|
59
60
|
| undefined;
|
|
60
61
|
|
|
61
62
|
if ( Array.isArray( gallery ) ) {
|
|
63
|
+
const gallerySize = node.settings.thumbnail_size as string | undefined;
|
|
64
|
+
|
|
62
65
|
for ( const item of gallery ) {
|
|
63
66
|
if ( item?.id || item?.url ) {
|
|
64
|
-
sources.push( item );
|
|
67
|
+
sources.push( { ...item, size: gallerySize } );
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { type ElementSnapshotNode } from '../types';
|
|
2
|
-
import {
|
|
2
|
+
import { walkAtomicBackgroundImageSources } from './atomic-background-image-sources';
|
|
3
|
+
import { walkAtomicImageSources } from './atomic-image-sources';
|
|
4
|
+
import { walkBackgroundImageSources } from './background-image-sources';
|
|
5
|
+
import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
|
|
6
|
+
import { buildImageSizeKey } from './image-size-key';
|
|
3
7
|
|
|
4
|
-
export function
|
|
5
|
-
const
|
|
8
|
+
export function extractImageSizeRequests( tree: ElementSnapshotNode[] ): string[] {
|
|
9
|
+
const keys = new Set< string >();
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
const collect = ( media: ImageLikeMedia ) => {
|
|
8
12
|
if ( media.id ) {
|
|
9
|
-
|
|
13
|
+
keys.add( buildImageSizeKey( { id: media.id, size: media.size } ) );
|
|
10
14
|
}
|
|
11
|
-
}
|
|
15
|
+
};
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
walkImageLikeSources( tree, ( { media } ) => collect( media ) );
|
|
18
|
+
walkBackgroundImageSources( tree, ( { media } ) => collect( media ) );
|
|
19
|
+
walkAtomicImageSources( tree, ( { media } ) => collect( media ) );
|
|
20
|
+
walkAtomicBackgroundImageSources( tree, ( { media } ) => collect( media ) );
|
|
21
|
+
|
|
22
|
+
return Array.from( keys ).sort();
|
|
14
23
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Script sources of known accessibility widgets. Ally is listed first; append third-party
|
|
2
|
+
// widget script sources here (UserWay, AccessiBe, EqualWeb, etc.) once confirmed.
|
|
3
|
+
const ACCESSIBILITY_WIDGET_SCRIPT_SOURCES = [ 'cdn.elementor.com/a11y/widget.js' ];
|
|
4
|
+
|
|
5
|
+
export function hasAccessibilityWidgetScript( html: string ): boolean {
|
|
6
|
+
return ACCESSIBILITY_WIDGET_SCRIPT_SOURCES.some( ( src ) => html.includes( src ) );
|
|
7
|
+
}
|