@elementor/editor-audits 4.3.3 → 4.3.5
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 +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +1103 -1146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1075 -1108
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -15
- 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/cookie-policy.ts +1 -0
- package/src/audits/google-consent-mode.ts +4 -7
- package/src/audits/images-alt-text.ts +6 -4
- package/src/audits/images-inefficient-format.ts +66 -0
- package/src/audits/images-too-large.ts +23 -10
- package/src/audits/nested-boxed-containers.ts +5 -0
- package/src/components/audit-feedback.tsx +4 -1
- package/src/components/audit-panel.tsx +13 -13
- package/src/components/audit-toggle-icon.tsx +22 -0
- package/src/components/fix-violation-with-angie.tsx +18 -7
- package/src/components/pages/category-page.tsx +27 -3
- package/src/components/pages/overview-page.tsx +24 -29
- package/src/components/report-shell.tsx +10 -91
- package/src/components/rescan-button.tsx +38 -0
- package/src/components/severity-icons.tsx +5 -1
- package/src/components/severity-summary-cards.tsx +52 -0
- package/src/components/violation-cta-button.tsx +11 -2
- package/src/components/violation-icons.tsx +2 -2
- package/src/components/violation-row.tsx +120 -62
- package/src/hooks/use-audit-report.ts +5 -4
- package/src/hooks/use-audit-toggle-props.ts +2 -2
- package/src/hooks/use-single-expanded-card.ts +11 -0
- package/src/init.ts +2 -0
- package/src/register-audits.ts +6 -2
- package/src/runner.ts +15 -7
- package/src/store/selectors.ts +4 -0
- package/src/store/slice.ts +10 -1
- package/src/sync-report-staleness.ts +14 -0
- package/src/types.ts +11 -1
- package/src/utils/all-image-sources.ts +17 -0
- package/src/utils/alt-text-image-sources.ts +24 -0
- package/src/utils/atomic-background-image-sources.ts +56 -0
- package/src/utils/atomic-image-sources.ts +66 -0
- package/src/utils/audit-status-summary.ts +0 -57
- 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 -14
- package/src/utils/image-size-key.ts +8 -0
- package/src/utils/page-attachments.ts +7 -6
- package/src/utils/scan-accessibility-widgets.ts +7 -0
- package/src/audits/too-many-widgets.ts +0 -53
- package/src/components/audit-statuses-section.tsx +0 -65
- package/src/components/count-summary-circle.tsx +0 -66
- package/src/components/pages/all-audits-page.tsx +0 -65
- package/src/components/pages/issues-page.tsx +0 -65
- package/src/components/score-bar.tsx +0 -56
- package/src/components/score-circle.tsx +0 -60
- package/src/components/severity-filter-chips.tsx +0 -57
- package/src/utils/score-thresholds.ts +0 -23
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useSingleExpandedCard( initialId: string | null = null ) {
|
|
4
|
+
const [ expandedId, setExpandedId ] = useState< string | null >( initialId );
|
|
5
|
+
|
|
6
|
+
const toggle = ( id: string ) => {
|
|
7
|
+
setExpandedId( ( current ) => ( current === id ? null : id ) );
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
return { expandedId, toggle };
|
|
11
|
+
}
|
package/src/init.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { registerAppBarAuditsToggle } from './editor-app-bar';
|
|
|
5
5
|
import { auditPanel } from './editor-panel';
|
|
6
6
|
import { registerAllAudits } from './register-audits';
|
|
7
7
|
import { slice } from './store/slice';
|
|
8
|
+
import { syncReportStaleness } from './sync-report-staleness';
|
|
8
9
|
|
|
9
10
|
export function init(): void {
|
|
10
11
|
registerSlice( slice );
|
|
11
12
|
registerAllAudits();
|
|
12
13
|
registerFloatingPanel( auditPanel.panel );
|
|
13
14
|
registerAppBarAuditsToggle();
|
|
15
|
+
syncReportStaleness();
|
|
14
16
|
}
|
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';
|
|
@@ -7,6 +9,7 @@ import * as googleConsentMode from './audits/google-consent-mode';
|
|
|
7
9
|
import * as headingStructure from './audits/heading-structure';
|
|
8
10
|
import * as hiddenElements from './audits/hidden-elements';
|
|
9
11
|
import * as imagesAltText from './audits/images-alt-text';
|
|
12
|
+
import * as imagesInefficientFormat from './audits/images-inefficient-format';
|
|
10
13
|
import * as imagesTooLarge from './audits/images-too-large';
|
|
11
14
|
import * as nestedBoxedContainers from './audits/nested-boxed-containers';
|
|
12
15
|
import * as pageExcerpt from './audits/page-excerpt';
|
|
@@ -19,7 +22,6 @@ import * as robotsNoindex from './audits/robots-noindex';
|
|
|
19
22
|
import * as scanForCookies from './audits/scan-for-cookies';
|
|
20
23
|
import * as sectionsAndColumns from './audits/sections-and-columns';
|
|
21
24
|
import * as siteIdentity from './audits/site-identity';
|
|
22
|
-
import * as tooManyWidgets from './audits/too-many-widgets';
|
|
23
25
|
import { registerAudit } from './registry';
|
|
24
26
|
import { type Audit } from './types';
|
|
25
27
|
|
|
@@ -30,7 +32,6 @@ const AUDITS: Audit[] = [
|
|
|
30
32
|
|
|
31
33
|
hiddenElements.audit,
|
|
32
34
|
deprecatedWidgets.audit,
|
|
33
|
-
tooManyWidgets.audit,
|
|
34
35
|
sectionsAndColumns.audit,
|
|
35
36
|
deepNesting.audit,
|
|
36
37
|
nestedBoxedContainers.audit,
|
|
@@ -43,11 +44,14 @@ const AUDITS: Audit[] = [
|
|
|
43
44
|
headingStructure.audit,
|
|
44
45
|
imagesAltText.audit,
|
|
45
46
|
imagesTooLarge.audit,
|
|
47
|
+
imagesInefficientFormat.audit,
|
|
46
48
|
|
|
47
49
|
robotsNoindex.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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHostDocumentElements, getHostDocumentId } from '@elementor/editor-elements';
|
|
2
2
|
|
|
3
3
|
import { fetchPageContext } from './api/page-context-client';
|
|
4
4
|
import { getRegisteredAudits } from './registry';
|
|
@@ -12,19 +12,27 @@ 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
|
-
|
|
22
|
-
|
|
21
|
+
if ( documentId !== getHostDocumentId() ) {
|
|
22
|
+
throw new Error( 'runPageAudit: documentId must match the host document.' );
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tree = buildSnapshotTree( getHostDocumentElements() );
|
|
26
|
+
const imageSizeRequests = extractImageSizeRequests( tree );
|
|
27
|
+
const pageContext = await fetchPageContext( documentId, imageSizeRequests );
|
|
23
28
|
|
|
24
29
|
const elements: ElementsModelSnapshot = { documentId, tree };
|
|
25
|
-
const kit: KitSnapshot = await
|
|
30
|
+
const [ kit, renderedHtml ]: [ KitSnapshot, string | null ] = await Promise.all( [
|
|
31
|
+
readKitSnapshot( pageContext.kit_id ),
|
|
32
|
+
fetchRenderedHtml( pageContext.frontend_url ),
|
|
33
|
+
] );
|
|
26
34
|
|
|
27
|
-
const ctx: AuditContext = { documentId, elements, pageContext, kit };
|
|
35
|
+
const ctx: AuditContext = { documentId, elements, pageContext, kit, renderedHtml };
|
|
28
36
|
const registered = getRegisteredAudits();
|
|
29
37
|
|
|
30
38
|
const auditResults: AuditRun[] = await Promise.all(
|
package/src/store/selectors.ts
CHANGED
package/src/store/slice.ts
CHANGED
|
@@ -6,9 +6,10 @@ type SliceState = {
|
|
|
6
6
|
status: 'idle' | 'loading' | 'error' | 'ready';
|
|
7
7
|
report: PageAuditReport | null;
|
|
8
8
|
error: string | null;
|
|
9
|
+
isStale: boolean;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
const initialState: SliceState = { status: 'idle', report: null, error: null };
|
|
12
|
+
const initialState: SliceState = { status: 'idle', report: null, error: null, isStale: false };
|
|
12
13
|
|
|
13
14
|
export const slice = __createSlice( {
|
|
14
15
|
name: 'audits',
|
|
@@ -21,6 +22,7 @@ export const slice = __createSlice( {
|
|
|
21
22
|
runSucceeded( state, action: PayloadAction< PageAuditReport > ) {
|
|
22
23
|
state.status = 'ready';
|
|
23
24
|
state.report = action.payload;
|
|
25
|
+
state.isStale = false;
|
|
24
26
|
},
|
|
25
27
|
runFailed( state, action: PayloadAction< string > ) {
|
|
26
28
|
state.status = 'error';
|
|
@@ -33,11 +35,18 @@ export const slice = __createSlice( {
|
|
|
33
35
|
state.status = 'ready';
|
|
34
36
|
state.report = action.payload;
|
|
35
37
|
state.error = null;
|
|
38
|
+
state.isStale = false;
|
|
36
39
|
},
|
|
37
40
|
reportCleared( state ) {
|
|
38
41
|
state.status = 'idle';
|
|
39
42
|
state.report = null;
|
|
40
43
|
state.error = null;
|
|
44
|
+
state.isStale = false;
|
|
45
|
+
},
|
|
46
|
+
reportStale( state ) {
|
|
47
|
+
if ( state.report ) {
|
|
48
|
+
state.isStale = true;
|
|
49
|
+
}
|
|
41
50
|
},
|
|
42
51
|
},
|
|
43
52
|
} );
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { __privateListenTo as listenTo, commandEndEvent, type CommandEvent } from '@elementor/editor-v1-adapters';
|
|
2
|
+
import { __dispatch as dispatch } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
import { slice } from './store/slice';
|
|
5
|
+
|
|
6
|
+
export function syncReportStaleness() {
|
|
7
|
+
listenTo( commandEndEvent( 'document/save/set-is-modified' ), ( e ) => {
|
|
8
|
+
const event = e as CommandEvent< { status: boolean } >;
|
|
9
|
+
|
|
10
|
+
if ( event.args?.status ) {
|
|
11
|
+
dispatch( slice.actions.reportStale() );
|
|
12
|
+
}
|
|
13
|
+
} );
|
|
14
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type AuditRun = {
|
|
|
23
23
|
export type AuditFailMetadata = {
|
|
24
24
|
missingAltImageCount?: number;
|
|
25
25
|
oversizedImageCount?: number;
|
|
26
|
+
inefficientImageFormatCount?: number;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export type AuditResult =
|
|
@@ -40,14 +41,17 @@ export type AuditViolation = {
|
|
|
40
41
|
angieFix?: boolean;
|
|
41
42
|
angiePrompt?: string;
|
|
42
43
|
ctaLabel?: string;
|
|
44
|
+
secondaryCtaLabel?: string;
|
|
45
|
+
secondaryCtaUrl?: string;
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
export type PageContextResponse = {
|
|
46
49
|
post_title: string | null;
|
|
47
50
|
post_excerpt: string | null;
|
|
48
51
|
featured_image_id: number | null;
|
|
52
|
+
/** Keyed by the composite `buildImageSizeKey` result (`"{attachmentId}:{size}"`), since one attachment can resolve to different files at different sizes. */
|
|
49
53
|
image_sizes: Record<
|
|
50
|
-
|
|
54
|
+
string,
|
|
51
55
|
{
|
|
52
56
|
width: number;
|
|
53
57
|
height: number;
|
|
@@ -65,6 +69,10 @@ export type PageContextResponse = {
|
|
|
65
69
|
privacy_settings_url: string;
|
|
66
70
|
ally_plugin_active: boolean;
|
|
67
71
|
ally_plugin_url: string;
|
|
72
|
+
ally_accessibility_statement_created: boolean;
|
|
73
|
+
ally_accessibility_statement_url: string;
|
|
74
|
+
ally_widget_settings_url: string;
|
|
75
|
+
ally_scan_url: string;
|
|
68
76
|
cookiez_plugin_active: boolean;
|
|
69
77
|
cookiez_plugin_url: string;
|
|
70
78
|
cookiez_plugin_installed: boolean;
|
|
@@ -73,6 +81,7 @@ export type PageContextResponse = {
|
|
|
73
81
|
cookiez_consent_mode_settings_url: string;
|
|
74
82
|
image_optimization_plugin_active: boolean;
|
|
75
83
|
image_optimization_plugin_url: string;
|
|
84
|
+
image_optimization_settings_url: string;
|
|
76
85
|
frontend_url: string | null;
|
|
77
86
|
site_identity: {
|
|
78
87
|
site_name_set: boolean;
|
|
@@ -108,6 +117,7 @@ export type AuditContext = {
|
|
|
108
117
|
elements: ElementsModelSnapshot;
|
|
109
118
|
pageContext: PageContextResponse;
|
|
110
119
|
kit: KitSnapshot;
|
|
120
|
+
renderedHtml: string | null;
|
|
111
121
|
};
|
|
112
122
|
|
|
113
123
|
export type PageAuditReport = {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
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
|
+
|
|
7
|
+
export type ImageSourceVisit = {
|
|
8
|
+
node: ElementSnapshotNode;
|
|
9
|
+
media: ImageLikeMedia;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function walkAllImageSources( tree: ElementSnapshotNode[], visit: ( source: ImageSourceVisit ) => void ): void {
|
|
13
|
+
walkImageLikeSources( tree, visit );
|
|
14
|
+
walkBackgroundImageSources( tree, visit );
|
|
15
|
+
walkAtomicImageSources( tree, visit );
|
|
16
|
+
walkAtomicBackgroundImageSources( tree, visit );
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
import { type ImageSourceVisit } from './all-image-sources';
|
|
3
|
+
import { ATOMIC_CONTENT_IMAGE_PROP_KEYS, walkAtomicImageSources } from './atomic-image-sources';
|
|
4
|
+
import { walkImageLikeSources } from './image-like-sources';
|
|
5
|
+
|
|
6
|
+
export function walkAltTextImageSources(
|
|
7
|
+
tree: ElementSnapshotNode[],
|
|
8
|
+
visit: ( source: ImageSourceVisit ) => void
|
|
9
|
+
): void {
|
|
10
|
+
walkImageLikeSources( tree, visit );
|
|
11
|
+
walkAtomicImageSources( tree, visit, ATOMIC_CONTENT_IMAGE_PROP_KEYS );
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasAltTextImages( tree: ElementSnapshotNode[] ): boolean {
|
|
15
|
+
let found = false;
|
|
16
|
+
|
|
17
|
+
walkAltTextImageSources( tree, ( { media } ) => {
|
|
18
|
+
if ( media.id || media.url ) {
|
|
19
|
+
found = true;
|
|
20
|
+
}
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
@@ -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,66 @@
|
|
|
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
|
+
export const ATOMIC_CONTENT_IMAGE_PROP_KEYS = [ 'image' ] as const;
|
|
14
|
+
|
|
15
|
+
const ATOMIC_IMAGE_PROP_KEYS = [ ...ATOMIC_CONTENT_IMAGE_PROP_KEYS, 'poster' ] as const;
|
|
16
|
+
|
|
17
|
+
export type AtomicImageSourceVisit = {
|
|
18
|
+
node: ElementSnapshotNode;
|
|
19
|
+
media: ImageLikeMedia;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function walkAtomicImageSources(
|
|
23
|
+
tree: ElementSnapshotNode[],
|
|
24
|
+
visit: ( source: AtomicImageSourceVisit ) => void,
|
|
25
|
+
propKeys: readonly string[] = ATOMIC_IMAGE_PROP_KEYS
|
|
26
|
+
): void {
|
|
27
|
+
walkElements( tree, ( node ) => {
|
|
28
|
+
if ( node.elType !== 'widget' ) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for ( const key of propKeys ) {
|
|
33
|
+
const media = extractAtomicImageMedia( node.settings[ key ] );
|
|
34
|
+
|
|
35
|
+
if ( media ) {
|
|
36
|
+
visit( { node, media } );
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} );
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia | null {
|
|
43
|
+
const image = imagePropTypeUtil.extract( imageProp );
|
|
44
|
+
|
|
45
|
+
if ( ! image ) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const src = imageSrcPropTypeUtil.extract( image.src );
|
|
50
|
+
|
|
51
|
+
if ( ! src ) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = imageAttachmentIdPropType.extract( src.id ) ?? undefined;
|
|
56
|
+
const url = urlPropTypeUtil.extract( src.url ) ?? undefined;
|
|
57
|
+
|
|
58
|
+
if ( ! id && ! url ) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const alt = stringPropTypeUtil.extract( src.alt ) ?? undefined;
|
|
63
|
+
const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
|
|
64
|
+
|
|
65
|
+
return { id, url, alt, size };
|
|
66
|
+
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { type ChipProps } from '@elementor/ui';
|
|
2
|
-
import { __ } from '@wordpress/i18n';
|
|
3
|
-
|
|
4
1
|
import { type AuditCategory, type AuditResult, type AuditRun, type PageAuditReport } from '../types';
|
|
5
2
|
import { isScoredAudit } from './is-scored-audit';
|
|
6
3
|
import { sortFailedAuditResults } from './sort-failed-audits';
|
|
7
4
|
|
|
8
|
-
export type AuditStatusGroup = 'fail' | 'pass' | 'skipped';
|
|
9
|
-
|
|
10
5
|
export type PartitionedAuditResults = {
|
|
11
6
|
failed: Array< AuditRun & { result: Extract< AuditResult, { status: 'fail' } > } >;
|
|
12
7
|
passed: Array< AuditRun & { result: Extract< AuditResult, { status: 'pass' } > } >;
|
|
@@ -61,58 +56,6 @@ export function partitionAuditResults(
|
|
|
61
56
|
};
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
export function auditStatusDisplayCounts( report: PageAuditReport ): Record< AuditStatusGroup, number > {
|
|
65
|
-
let pass = 0;
|
|
66
|
-
let skipped = 0;
|
|
67
|
-
let totalViolations = 0;
|
|
68
|
-
|
|
69
|
-
for ( const { audit, result } of report.auditResults ) {
|
|
70
|
-
if ( ! isScoredAudit( audit ) ) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
switch ( result.status ) {
|
|
75
|
-
case 'fail':
|
|
76
|
-
totalViolations += result.violations.length;
|
|
77
|
-
break;
|
|
78
|
-
case 'pass':
|
|
79
|
-
pass++;
|
|
80
|
-
break;
|
|
81
|
-
case 'skipped':
|
|
82
|
-
skipped++;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
fail: totalViolations,
|
|
89
|
-
pass,
|
|
90
|
-
skipped,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function auditStatusColor( status: AuditStatusGroup ): ChipProps[ 'color' ] {
|
|
95
|
-
switch ( status ) {
|
|
96
|
-
case 'fail':
|
|
97
|
-
return 'error';
|
|
98
|
-
case 'pass':
|
|
99
|
-
return 'success';
|
|
100
|
-
case 'skipped':
|
|
101
|
-
return 'default';
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function auditStatusLabel( status: AuditStatusGroup ): string {
|
|
106
|
-
switch ( status ) {
|
|
107
|
-
case 'fail':
|
|
108
|
-
return __( 'Failed audits', 'elementor' );
|
|
109
|
-
case 'pass':
|
|
110
|
-
return __( 'Passed audits', 'elementor' );
|
|
111
|
-
case 'skipped':
|
|
112
|
-
return __( 'Skipped audits', 'elementor' );
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
59
|
export function getPopulatedCategories(
|
|
117
60
|
categoryTotals: PageAuditReport[ 'categories' ],
|
|
118
61
|
categories: readonly AuditCategory[]
|
|
@@ -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 = {
|
|
@@ -14,18 +15,6 @@ export type ImageLikeSourceVisit = {
|
|
|
14
15
|
media: ImageLikeMedia;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export function hasPageImages( tree: ElementSnapshotNode[] ): boolean {
|
|
18
|
-
let found = false;
|
|
19
|
-
|
|
20
|
-
walkImageLikeSources( tree, ( { media } ) => {
|
|
21
|
-
if ( media.id || media.url ) {
|
|
22
|
-
found = true;
|
|
23
|
-
}
|
|
24
|
-
} );
|
|
25
|
-
|
|
26
|
-
return found;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
export function walkImageLikeSources(
|
|
30
19
|
tree: ElementSnapshotNode[],
|
|
31
20
|
visit: ( source: ImageLikeSourceVisit ) => void
|
|
@@ -51,7 +40,7 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
|
|
|
51
40
|
const image = node.settings.image as ImageLikeMedia | undefined;
|
|
52
41
|
|
|
53
42
|
if ( image?.id || image?.url ) {
|
|
54
|
-
sources.push( image );
|
|
43
|
+
sources.push( { ...image, size: node.settings.image_size as string | undefined } );
|
|
55
44
|
}
|
|
56
45
|
|
|
57
46
|
const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
|
|
@@ -59,9 +48,11 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
|
|
|
59
48
|
| undefined;
|
|
60
49
|
|
|
61
50
|
if ( Array.isArray( gallery ) ) {
|
|
51
|
+
const gallerySize = node.settings.thumbnail_size as string | undefined;
|
|
52
|
+
|
|
62
53
|
for ( const item of gallery ) {
|
|
63
54
|
if ( item?.id || item?.url ) {
|
|
64
|
-
sources.push( item );
|
|
55
|
+
sources.push( { ...item, size: gallerySize } );
|
|
65
56
|
}
|
|
66
57
|
}
|
|
67
58
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { type ElementSnapshotNode } from '../types';
|
|
2
|
-
import {
|
|
2
|
+
import { walkAllImageSources } from './all-image-sources';
|
|
3
|
+
import { buildImageSizeKey } from './image-size-key';
|
|
3
4
|
|
|
4
|
-
export function
|
|
5
|
-
const
|
|
5
|
+
export function extractImageSizeRequests( tree: ElementSnapshotNode[] ): string[] {
|
|
6
|
+
const keys = new Set< string >();
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
walkAllImageSources( tree, ( { media } ) => {
|
|
8
9
|
if ( media.id ) {
|
|
9
|
-
|
|
10
|
+
keys.add( buildImageSizeKey( { id: media.id, size: media.size } ) );
|
|
10
11
|
}
|
|
11
12
|
} );
|
|
12
13
|
|
|
13
|
-
return Array.from(
|
|
14
|
+
return Array.from( keys ).sort();
|
|
14
15
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { getWidgetsCache } from '@elementor/editor-elements';
|
|
2
|
-
import { __ } from '@wordpress/i18n';
|
|
3
|
-
|
|
4
|
-
import { type Audit, type ElementSnapshotNode } from '../types';
|
|
5
|
-
import { walkElements } from '../utils/walk';
|
|
6
|
-
|
|
7
|
-
const WIDGET_COUNT_THRESHOLD = 100;
|
|
8
|
-
|
|
9
|
-
function isCountableWidget( node: ElementSnapshotNode, widgetsCache: ReturnType< typeof getWidgetsCache > ): boolean {
|
|
10
|
-
if ( node.elType === 'widget' ) {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return !! widgetsCache?.[ node.elType ]?.meta?.is_compound;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const audit: Audit = {
|
|
18
|
-
id: 'audits/too-many-widgets',
|
|
19
|
-
title: __( 'Too many widgets', 'elementor' ),
|
|
20
|
-
description: __( 'Excessive DOM size caused by too many widgets degrades rendering performance.', 'elementor' ),
|
|
21
|
-
fixHint: __( 'Reduce the number of widgets on the page by removing or combining elements.', 'elementor' ),
|
|
22
|
-
categories: [ 'best-practices', 'performance' ],
|
|
23
|
-
severity: 'warning',
|
|
24
|
-
weight: 5,
|
|
25
|
-
evaluate: ( ctx ) => {
|
|
26
|
-
if ( ctx.elements.tree.length === 0 ) {
|
|
27
|
-
return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const widgetsCache = getWidgetsCache();
|
|
31
|
-
let widgetCount = 0;
|
|
32
|
-
|
|
33
|
-
walkElements( ctx.elements.tree, ( node ) => {
|
|
34
|
-
if ( isCountableWidget( node, widgetsCache ) ) {
|
|
35
|
-
widgetCount++;
|
|
36
|
-
}
|
|
37
|
-
} );
|
|
38
|
-
|
|
39
|
-
if ( widgetCount <= WIDGET_COUNT_THRESHOLD ) {
|
|
40
|
-
return { status: 'pass' };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
status: 'fail',
|
|
45
|
-
violations: [
|
|
46
|
-
{
|
|
47
|
-
auditId: audit.id,
|
|
48
|
-
label: __( 'Page has too many widgets.', 'elementor' ),
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
};
|