@elementor/editor-audits 4.3.0-1036
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/CHANGELOG.md +5 -0
- package/dist/index.d.mts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2822 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2801 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
- package/src/api/page-context-client.ts +19 -0
- package/src/audits/accessibility-policy.ts +32 -0
- package/src/audits/cookie-policy.ts +32 -0
- package/src/audits/deep-nesting.ts +45 -0
- package/src/audits/default-design-system.ts +32 -0
- package/src/audits/deprecated-widgets.ts +57 -0
- package/src/audits/heading-structure.ts +294 -0
- package/src/audits/hidden-elements.ts +41 -0
- package/src/audits/images-alt-text.ts +47 -0
- package/src/audits/images-too-large.ts +61 -0
- package/src/audits/nested-boxed-containers.ts +44 -0
- package/src/audits/page-excerpt.ts +30 -0
- package/src/audits/page-featured-image.ts +29 -0
- package/src/audits/page-title.ts +48 -0
- package/src/audits/prefer-global-colors.ts +52 -0
- package/src/audits/prefer-global-fonts.ts +50 -0
- package/src/audits/privacy-policy.ts +35 -0
- package/src/audits/robots-noindex.ts +35 -0
- package/src/audits/sections-and-columns.ts +39 -0
- package/src/audits/site-identity.ts +62 -0
- package/src/audits/too-many-widgets.ts +43 -0
- package/src/components/audit-feedback.tsx +178 -0
- package/src/components/audit-panel.tsx +55 -0
- package/src/components/category-icons.ts +17 -0
- package/src/components/count-summary-circle.tsx +66 -0
- package/src/components/fix-violation-with-angie.tsx +50 -0
- package/src/components/issues-category-row.tsx +62 -0
- package/src/components/pages/all-audits-page.tsx +65 -0
- package/src/components/pages/category-page.tsx +50 -0
- package/src/components/pages/error-page.tsx +21 -0
- package/src/components/pages/issues-page.tsx +48 -0
- package/src/components/pages/loading-page.tsx +12 -0
- package/src/components/pages/overview-page.tsx +140 -0
- package/src/components/pages/welcome-page.tsx +48 -0
- package/src/components/promotion-card.tsx +43 -0
- package/src/components/promotions.tsx +69 -0
- package/src/components/report-shell.tsx +110 -0
- package/src/components/score-bar.tsx +56 -0
- package/src/components/score-circle.tsx +60 -0
- package/src/components/severity-icons.tsx +26 -0
- package/src/components/status-section.tsx +47 -0
- package/src/components/subpage-header.tsx +29 -0
- package/src/components/violation-icons.tsx +33 -0
- package/src/components/violation-row.tsx +169 -0
- package/src/constants.ts +35 -0
- package/src/editor-app-bar.tsx +13 -0
- package/src/editor-panel.tsx +19 -0
- package/src/hooks/focus-violation.ts +53 -0
- package/src/hooks/use-audit-report.ts +43 -0
- package/src/hooks/use-audit-toggle-props.ts +17 -0
- package/src/index.ts +14 -0
- package/src/init.ts +14 -0
- package/src/register-audits.ts +56 -0
- package/src/register-promotions.ts +75 -0
- package/src/registry.ts +19 -0
- package/src/runner.ts +44 -0
- package/src/store/index.ts +2 -0
- package/src/store/selectors.ts +15 -0
- package/src/store/slice.ts +42 -0
- package/src/types.ts +112 -0
- package/src/utils/audit-status-summary.ts +112 -0
- package/src/utils/build-angie-prompt.ts +9 -0
- package/src/utils/collect-hardcoded-colors.ts +74 -0
- package/src/utils/collect-hardcoded-fonts.ts +70 -0
- package/src/utils/compute-report.ts +51 -0
- package/src/utils/find-audit-run.ts +5 -0
- package/src/utils/image-alt.ts +19 -0
- package/src/utils/image-like-sources.ts +70 -0
- package/src/utils/keyboard-click.ts +10 -0
- package/src/utils/match-global-by-value.ts +5 -0
- package/src/utils/page-attachments.ts +14 -0
- package/src/utils/read-kit-snapshot.ts +149 -0
- package/src/utils/report-storage.ts +16 -0
- package/src/utils/score-thresholds.ts +23 -0
- package/src/utils/severity-counts.ts +59 -0
- package/src/utils/sort-failed-audits.ts +19 -0
- package/src/utils/v1-snapshot.ts +27 -0
- package/src/utils/walk.ts +17 -0
- package/src/utils/window-config.ts +20 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/page-featured-image',
|
|
7
|
+
title: __( 'Page featured image', 'elementor' ),
|
|
8
|
+
description: __( 'Featured images are used by social shares and many themes for hero visuals.', 'elementor' ),
|
|
9
|
+
fixHint: __( 'Open Page Settings and set a featured image.', 'elementor' ),
|
|
10
|
+
categories: [ 'seo' ],
|
|
11
|
+
severity: 'info',
|
|
12
|
+
weight: 5,
|
|
13
|
+
evaluate: ( ctx ) => {
|
|
14
|
+
if ( ctx.pageContext.featured_image_id ) {
|
|
15
|
+
return { status: 'pass' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
status: 'fail',
|
|
20
|
+
violations: [
|
|
21
|
+
{
|
|
22
|
+
auditId: audit.id,
|
|
23
|
+
label: __( 'No featured image set.', 'elementor' ),
|
|
24
|
+
targetHint: 'page-settings',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
const MAX_TITLE_LENGTH = 60;
|
|
6
|
+
|
|
7
|
+
export const audit: Audit = {
|
|
8
|
+
id: 'audits/page-title',
|
|
9
|
+
title: __( 'Page title', 'elementor' ),
|
|
10
|
+
description: __( 'Pages need a clear title for SEO and screen-reader navigation.', 'elementor' ),
|
|
11
|
+
fixHint: __( 'Open Page Settings and add a title.', 'elementor' ),
|
|
12
|
+
categories: [ 'seo' ],
|
|
13
|
+
severity: 'error',
|
|
14
|
+
weight: 10,
|
|
15
|
+
evaluate: ( ctx ) => {
|
|
16
|
+
const title = ctx.pageContext.post_title;
|
|
17
|
+
|
|
18
|
+
if ( ! title ) {
|
|
19
|
+
return {
|
|
20
|
+
status: 'fail',
|
|
21
|
+
violations: [
|
|
22
|
+
{
|
|
23
|
+
auditId: audit.id,
|
|
24
|
+
label: __( 'Page has no title.', 'elementor' ),
|
|
25
|
+
targetHint: 'page-settings',
|
|
26
|
+
angieFix: true,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ( title.length > MAX_TITLE_LENGTH ) {
|
|
33
|
+
return {
|
|
34
|
+
status: 'fail',
|
|
35
|
+
violations: [
|
|
36
|
+
{
|
|
37
|
+
auditId: audit.id,
|
|
38
|
+
label: __( 'Page title is too long.', 'elementor' ),
|
|
39
|
+
targetHint: 'page-settings',
|
|
40
|
+
angieFix: true,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { status: 'pass' };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit, type AuditViolation } from '../types';
|
|
4
|
+
import { collectHardcodedColors } from '../utils/collect-hardcoded-colors';
|
|
5
|
+
import { findMatchingGlobalByValue } from '../utils/match-global-by-value';
|
|
6
|
+
import { walkElements } from '../utils/walk';
|
|
7
|
+
|
|
8
|
+
export const audit: Audit = {
|
|
9
|
+
id: 'audits/prefer-global-colors',
|
|
10
|
+
title: __( 'Prefer global colors', 'elementor' ),
|
|
11
|
+
description: __( 'Global colors make the design consistent and easy to update site-wide.', 'elementor' ),
|
|
12
|
+
fixHint: __( 'Replace the hard-coded value with the matching global color.', 'elementor' ),
|
|
13
|
+
categories: [ 'best-practices' ],
|
|
14
|
+
severity: 'info',
|
|
15
|
+
weight: 3,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
if ( ctx.elements.tree.length === 0 ) {
|
|
18
|
+
return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ( ctx.kit.globals.colors.length === 0 ) {
|
|
22
|
+
return { status: 'skipped', reason: __( 'No global colors', 'elementor' ) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const violations: AuditViolation[] = [];
|
|
26
|
+
|
|
27
|
+
walkElements( ctx.elements.tree, ( node ) => {
|
|
28
|
+
for ( const { value } of collectHardcodedColors( node.settings ) ) {
|
|
29
|
+
const global = findMatchingGlobalByValue( value, ctx.kit.globals.colors );
|
|
30
|
+
|
|
31
|
+
if ( ! global ) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
violations.push( {
|
|
36
|
+
auditId: audit.id,
|
|
37
|
+
elementId: node.id,
|
|
38
|
+
targetHint: 'element-settings',
|
|
39
|
+
angieFix: true,
|
|
40
|
+
label: sprintf(
|
|
41
|
+
/* translators: 1: hard-coded color value, 2: global color title. */
|
|
42
|
+
__( 'replace "%1$s" with "%2$s" global color', 'elementor' ),
|
|
43
|
+
value,
|
|
44
|
+
global.title
|
|
45
|
+
),
|
|
46
|
+
} );
|
|
47
|
+
}
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
return violations.length === 0 ? { status: 'pass' } : { status: 'fail', violations };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit, type AuditViolation } from '../types';
|
|
4
|
+
import { collectHardcodedFonts } from '../utils/collect-hardcoded-fonts';
|
|
5
|
+
import { findMatchingGlobalByValue } from '../utils/match-global-by-value';
|
|
6
|
+
import { walkElements } from '../utils/walk';
|
|
7
|
+
|
|
8
|
+
export const audit: Audit = {
|
|
9
|
+
id: 'audits/prefer-global-fonts',
|
|
10
|
+
title: __( 'Prefer global fonts', 'elementor' ),
|
|
11
|
+
description: __( 'Global fonts make the design consistent and easy to update site-wide.', 'elementor' ),
|
|
12
|
+
fixHint: __( 'Replace the hard-coded value with the matching global font.', 'elementor' ),
|
|
13
|
+
categories: [ 'best-practices', 'performance' ],
|
|
14
|
+
severity: 'info',
|
|
15
|
+
weight: 3,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
if ( ctx.elements.tree.length === 0 ) {
|
|
18
|
+
return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ( ctx.kit.globals.fonts.length === 0 ) {
|
|
22
|
+
return { status: 'skipped', reason: __( 'No global fonts', 'elementor' ) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const violations: AuditViolation[] = [];
|
|
26
|
+
|
|
27
|
+
walkElements( ctx.elements.tree, ( node ) => {
|
|
28
|
+
for ( const { value } of collectHardcodedFonts( node.settings ) ) {
|
|
29
|
+
const global = findMatchingGlobalByValue( value, ctx.kit.globals.fonts );
|
|
30
|
+
|
|
31
|
+
if ( ! global ) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
violations.push( {
|
|
36
|
+
auditId: audit.id,
|
|
37
|
+
elementId: node.id,
|
|
38
|
+
targetHint: 'element-settings',
|
|
39
|
+
label: sprintf(
|
|
40
|
+
/* translators: %s: global font title. */
|
|
41
|
+
__( 'replace hardcoded typography with "%s" global font', 'elementor' ),
|
|
42
|
+
global.title
|
|
43
|
+
),
|
|
44
|
+
} );
|
|
45
|
+
}
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
return violations.length === 0 ? { status: 'pass' } : { status: 'fail', violations };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/privacy-policy',
|
|
7
|
+
title: __( 'Privacy policy', 'elementor' ),
|
|
8
|
+
description: __(
|
|
9
|
+
'A privacy policy page is required by privacy regulations such as GDPR (EU) and CCPA (US).',
|
|
10
|
+
'elementor'
|
|
11
|
+
),
|
|
12
|
+
fixHint: __(
|
|
13
|
+
'Go to Settings > Privacy in the WordPress admin and assign a published privacy policy page.',
|
|
14
|
+
'elementor'
|
|
15
|
+
),
|
|
16
|
+
categories: [ 'compliance' ],
|
|
17
|
+
severity: 'info',
|
|
18
|
+
weight: 1,
|
|
19
|
+
evaluate: ( ctx ) => {
|
|
20
|
+
if ( ctx.pageContext.privacy_policy_url ) {
|
|
21
|
+
return { status: 'pass' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
status: 'fail',
|
|
26
|
+
violations: [
|
|
27
|
+
{
|
|
28
|
+
auditId: audit.id,
|
|
29
|
+
label: __( 'No privacy policy page is set.', 'elementor' ),
|
|
30
|
+
externalUrl: ctx.pageContext.privacy_settings_url,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/robots-noindex',
|
|
7
|
+
title: __( 'Search engine visibility', 'elementor' ),
|
|
8
|
+
description: __(
|
|
9
|
+
'When search engine visibility is checked in WordPress Settings -> Reading -> Search Engine Visibility, search engines are discouraged from indexing the website.',
|
|
10
|
+
'elementor'
|
|
11
|
+
),
|
|
12
|
+
fixHint: __(
|
|
13
|
+
'Go to Settings → Reading in the WordPress admin and uncheck "Discourage search engines from indexing this site" when the website should be indexed.',
|
|
14
|
+
'elementor'
|
|
15
|
+
),
|
|
16
|
+
categories: [ 'seo' ],
|
|
17
|
+
severity: 'error',
|
|
18
|
+
weight: 7,
|
|
19
|
+
evaluate: ( ctx ) => {
|
|
20
|
+
if ( ! ctx.pageContext.is_noindex ) {
|
|
21
|
+
return { status: 'pass' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
status: 'fail',
|
|
26
|
+
violations: [
|
|
27
|
+
{
|
|
28
|
+
auditId: audit.id,
|
|
29
|
+
label: __( 'Search engines are discouraged from indexing this website.', 'elementor' ),
|
|
30
|
+
externalUrl: ctx.pageContext.reading_settings_url,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit, type AuditViolation } from '../types';
|
|
4
|
+
import { walkElements } from '../utils/walk';
|
|
5
|
+
|
|
6
|
+
export const audit: Audit = {
|
|
7
|
+
id: 'audits/sections-and-columns',
|
|
8
|
+
title: __( 'Sections and columns', 'elementor' ),
|
|
9
|
+
description: __(
|
|
10
|
+
'Sections and columns are legacy elements. Containers render fewer DOM nodes and are more flexible.',
|
|
11
|
+
'elementor'
|
|
12
|
+
),
|
|
13
|
+
fixHint: __( 'Use the Container Converter to replace each section/column with a container.', 'elementor' ),
|
|
14
|
+
categories: [ 'best-practices', 'performance' ],
|
|
15
|
+
severity: 'warning',
|
|
16
|
+
weight: 7,
|
|
17
|
+
evaluate: ( ctx ) => {
|
|
18
|
+
if ( ctx.elements.tree.length === 0 ) {
|
|
19
|
+
return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const violations: AuditViolation[] = [];
|
|
23
|
+
|
|
24
|
+
walkElements( ctx.elements.tree, ( node ) => {
|
|
25
|
+
if ( node.elType === 'section' || node.elType === 'column' ) {
|
|
26
|
+
violations.push( {
|
|
27
|
+
auditId: audit.id,
|
|
28
|
+
elementId: node.id,
|
|
29
|
+
label:
|
|
30
|
+
node.elType === 'section'
|
|
31
|
+
? __( 'Section element', 'elementor' )
|
|
32
|
+
: __( 'Column element', 'elementor' ),
|
|
33
|
+
} );
|
|
34
|
+
}
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
return violations.length === 0 ? { status: 'pass' } : { status: 'fail', violations };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit, type AuditViolation } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/site-identity',
|
|
7
|
+
title: __( 'Site identity', 'elementor' ),
|
|
8
|
+
description: __(
|
|
9
|
+
'Site name, description, logo, and favicon establish your brand and appear in search results and browser tabs.',
|
|
10
|
+
'elementor'
|
|
11
|
+
),
|
|
12
|
+
fixHint: __( 'Open Site Settings → Site Identity and complete all the missing fields.', 'elementor' ),
|
|
13
|
+
categories: [ 'best-practices', 'seo' ],
|
|
14
|
+
severity: 'info',
|
|
15
|
+
weight: 7,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
const { site_identity: identity } = ctx.pageContext;
|
|
18
|
+
const violations: AuditViolation[] = [];
|
|
19
|
+
|
|
20
|
+
if ( ! identity.site_name_set ) {
|
|
21
|
+
violations.push( {
|
|
22
|
+
auditId: audit.id,
|
|
23
|
+
label: __( 'Site name is missing or still uses the default.', 'elementor' ),
|
|
24
|
+
targetHint: 'site-identity-settings',
|
|
25
|
+
} );
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if ( ! identity.site_description_set ) {
|
|
29
|
+
violations.push( {
|
|
30
|
+
auditId: audit.id,
|
|
31
|
+
label: __( 'Site description is missing or still uses the default.', 'elementor' ),
|
|
32
|
+
targetHint: 'site-identity-settings',
|
|
33
|
+
angieFix: true,
|
|
34
|
+
} );
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ( ! identity.site_logo_set ) {
|
|
38
|
+
violations.push( {
|
|
39
|
+
auditId: audit.id,
|
|
40
|
+
label: __( 'Site logo is not set.', 'elementor' ),
|
|
41
|
+
targetHint: 'site-identity-settings',
|
|
42
|
+
} );
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if ( ! identity.site_favicon_set ) {
|
|
46
|
+
violations.push( {
|
|
47
|
+
auditId: audit.id,
|
|
48
|
+
label: __( 'Site favicon is not set.', 'elementor' ),
|
|
49
|
+
targetHint: 'site-identity-settings',
|
|
50
|
+
} );
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if ( violations.length === 0 ) {
|
|
54
|
+
return { status: 'pass' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
status: 'fail',
|
|
59
|
+
violations,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
import { walkElements } from '../utils/walk';
|
|
5
|
+
|
|
6
|
+
const WIDGET_COUNT_THRESHOLD = 100;
|
|
7
|
+
|
|
8
|
+
export const audit: Audit = {
|
|
9
|
+
id: 'audits/too-many-widgets',
|
|
10
|
+
title: __( 'Too many widgets', 'elementor' ),
|
|
11
|
+
description: __( 'Excessive DOM size caused by too many widgets degrades rendering performance.', 'elementor' ),
|
|
12
|
+
fixHint: __( 'Reduce the number of widgets on the page by removing or combining elements.', 'elementor' ),
|
|
13
|
+
categories: [ 'best-practices', 'performance' ],
|
|
14
|
+
severity: 'warning',
|
|
15
|
+
weight: 5,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
if ( ctx.elements.tree.length === 0 ) {
|
|
18
|
+
return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let widgetCount = 0;
|
|
22
|
+
|
|
23
|
+
walkElements( ctx.elements.tree, ( node ) => {
|
|
24
|
+
if ( node.elType === 'widget' ) {
|
|
25
|
+
widgetCount++;
|
|
26
|
+
}
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
if ( widgetCount <= WIDGET_COUNT_THRESHOLD ) {
|
|
30
|
+
return { status: 'pass' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
status: 'fail',
|
|
35
|
+
violations: [
|
|
36
|
+
{
|
|
37
|
+
auditId: audit.id,
|
|
38
|
+
label: __( 'Page has too many widgets.', 'elementor' ),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { notify } from '@elementor/editor-notifications';
|
|
4
|
+
import { isExperimentActive } from '@elementor/editor-v1-adapters';
|
|
5
|
+
import { useMixpanel } from '@elementor/events';
|
|
6
|
+
import { httpService } from '@elementor/http-client';
|
|
7
|
+
import { MessageLinesIcon } from '@elementor/icons';
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogActions,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
IconButton,
|
|
16
|
+
Stack,
|
|
17
|
+
TextField,
|
|
18
|
+
Tooltip,
|
|
19
|
+
Typography,
|
|
20
|
+
} from '@elementor/ui';
|
|
21
|
+
import { __ } from '@wordpress/i18n';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
FEEDBACK_CANCELLED_EVENT,
|
|
25
|
+
FEEDBACK_CLICKED_EVENT,
|
|
26
|
+
FEEDBACK_CLOSED_EVENT,
|
|
27
|
+
FEEDBACK_ENTRY_POINT,
|
|
28
|
+
FEEDBACK_EXPERIMENT_NAME,
|
|
29
|
+
FEEDBACK_SENT_EVENT,
|
|
30
|
+
} from '../constants';
|
|
31
|
+
|
|
32
|
+
const FEEDBACK_SUBJECT = 'Page Audit Tool';
|
|
33
|
+
const FEEDBACK_SUBJECT_LABEL = __( 'Page Audit Tool', 'elementor' );
|
|
34
|
+
const SUCCESS_NOTIFICATION_ID = 'audit-feedback-success';
|
|
35
|
+
const ERROR_NOTIFICATION_ID = 'audit-feedback-error';
|
|
36
|
+
const DIALOG_TITLE_ID = 'audit-feedback-dialog-title';
|
|
37
|
+
const DIALOG_DESCRIPTION_ID = 'audit-feedback-dialog-description';
|
|
38
|
+
|
|
39
|
+
const isUserConnected = () =>
|
|
40
|
+
Boolean( window.elementorCommon?.config?.library_connect?.is_connected || window.elementorPro?.config?.isActive );
|
|
41
|
+
|
|
42
|
+
const notifySuccess = () =>
|
|
43
|
+
notify( {
|
|
44
|
+
id: SUCCESS_NOTIFICATION_ID,
|
|
45
|
+
type: 'success',
|
|
46
|
+
message: __( 'Feedback sent. Thanks for helping us out.', 'elementor' ),
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
const notifyError = () =>
|
|
50
|
+
notify( {
|
|
51
|
+
id: ERROR_NOTIFICATION_ID,
|
|
52
|
+
type: 'error',
|
|
53
|
+
message: __( 'Something went wrong. Please try sending your feedback again.', 'elementor' ),
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
export default function AuditFeedback() {
|
|
57
|
+
const [ isOpen, setIsOpen ] = useState( false );
|
|
58
|
+
const [ feedbackText, setFeedbackText ] = useState( '' );
|
|
59
|
+
const [ isSubmitting, setIsSubmitting ] = useState( false );
|
|
60
|
+
const { dispatchEvent: trackEvent = ( ...args: unknown[] ) => void args } = useMixpanel();
|
|
61
|
+
|
|
62
|
+
if ( ! isExperimentActive( FEEDBACK_EXPERIMENT_NAME ) ) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const connected = isUserConnected();
|
|
67
|
+
const connectUrl = window.elementor?.config?.user?.top_bar?.connect_url;
|
|
68
|
+
|
|
69
|
+
if ( ! connected && ! connectUrl ) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const triggerLabel = __( 'Give feedback', 'elementor' );
|
|
74
|
+
|
|
75
|
+
const handleOpen = () => {
|
|
76
|
+
if ( ! connected ) {
|
|
77
|
+
window.open( connectUrl, '_blank', 'noopener' );
|
|
78
|
+
trackEvent( FEEDBACK_CLICKED_EVENT, { entry_point: FEEDBACK_ENTRY_POINT, connected: false } );
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setIsOpen( true );
|
|
83
|
+
trackEvent( FEEDBACK_CLICKED_EVENT, { entry_point: FEEDBACK_ENTRY_POINT, connected: true } );
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const dismiss = ( eventName: string ) => {
|
|
87
|
+
setIsOpen( false );
|
|
88
|
+
setFeedbackText( '' );
|
|
89
|
+
trackEvent( eventName, { entry_point: FEEDBACK_ENTRY_POINT } );
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleCancel = () => dismiss( FEEDBACK_CANCELLED_EVENT );
|
|
93
|
+
const handleClose = () => dismiss( FEEDBACK_CLOSED_EVENT );
|
|
94
|
+
|
|
95
|
+
const handleSubmit = () => {
|
|
96
|
+
setIsSubmitting( true );
|
|
97
|
+
|
|
98
|
+
httpService()
|
|
99
|
+
.post( 'elementor/v1/feedback/submit', {
|
|
100
|
+
subject: FEEDBACK_SUBJECT,
|
|
101
|
+
description: feedbackText.trim(),
|
|
102
|
+
} )
|
|
103
|
+
.then( ( response ) => {
|
|
104
|
+
if ( ! response.data.success ) {
|
|
105
|
+
notifyError();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setIsOpen( false );
|
|
110
|
+
setFeedbackText( '' );
|
|
111
|
+
notifySuccess();
|
|
112
|
+
trackEvent( FEEDBACK_SENT_EVENT, { entry_point: FEEDBACK_ENTRY_POINT } );
|
|
113
|
+
} )
|
|
114
|
+
.catch( notifyError )
|
|
115
|
+
.finally( () => setIsSubmitting( false ) );
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<Tooltip title={ triggerLabel } placement="top">
|
|
121
|
+
<IconButton size="small" aria-label={ triggerLabel } onClick={ handleOpen }>
|
|
122
|
+
<MessageLinesIcon fontSize="small" />
|
|
123
|
+
</IconButton>
|
|
124
|
+
</Tooltip>
|
|
125
|
+
<Dialog
|
|
126
|
+
open={ isOpen }
|
|
127
|
+
onClose={ handleClose }
|
|
128
|
+
aria-labelledby={ DIALOG_TITLE_ID }
|
|
129
|
+
aria-describedby={ DIALOG_DESCRIPTION_ID }
|
|
130
|
+
PaperProps={ { sx: { borderRadius: 1 } } }
|
|
131
|
+
>
|
|
132
|
+
<DialogHeader logo={ false } onClose={ handleClose }>
|
|
133
|
+
<DialogTitle id={ DIALOG_TITLE_ID }>
|
|
134
|
+
{ __( 'Let us know what you think', 'elementor' ) }
|
|
135
|
+
</DialogTitle>
|
|
136
|
+
</DialogHeader>
|
|
137
|
+
<DialogContent dividers>
|
|
138
|
+
<Stack direction="column" gap={ 2 }>
|
|
139
|
+
<TextField value={ FEEDBACK_SUBJECT_LABEL } disabled fullWidth size="small" />
|
|
140
|
+
<TextField
|
|
141
|
+
placeholder={ __(
|
|
142
|
+
'Tell us what you had in mind, what we can improve or fix to make this feature better.',
|
|
143
|
+
'elementor'
|
|
144
|
+
) }
|
|
145
|
+
fullWidth
|
|
146
|
+
multiline
|
|
147
|
+
rows={ 4 }
|
|
148
|
+
disabled={ isSubmitting }
|
|
149
|
+
onChange={ ( event: React.ChangeEvent< HTMLInputElement > ) =>
|
|
150
|
+
setFeedbackText( event.target.value )
|
|
151
|
+
}
|
|
152
|
+
value={ feedbackText }
|
|
153
|
+
/>
|
|
154
|
+
<Typography id={ DIALOG_DESCRIPTION_ID } variant="caption" color="text.secondary">
|
|
155
|
+
{ __(
|
|
156
|
+
"We appreciate your feedback! While we review all submissions, we can't guarantee that every suggestion will result in a change or update.",
|
|
157
|
+
'elementor'
|
|
158
|
+
) }
|
|
159
|
+
</Typography>
|
|
160
|
+
</Stack>
|
|
161
|
+
</DialogContent>
|
|
162
|
+
<DialogActions>
|
|
163
|
+
<Button variant="text" color="secondary" onClick={ handleCancel }>
|
|
164
|
+
{ __( 'Cancel', 'elementor' ) }
|
|
165
|
+
</Button>
|
|
166
|
+
<Button
|
|
167
|
+
variant="contained"
|
|
168
|
+
color="primary"
|
|
169
|
+
disabled={ isSubmitting || feedbackText.trim().length === 0 }
|
|
170
|
+
onClick={ handleSubmit }
|
|
171
|
+
>
|
|
172
|
+
{ __( 'Send', 'elementor' ) }
|
|
173
|
+
</Button>
|
|
174
|
+
</DialogActions>
|
|
175
|
+
</Dialog>
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { getCurrentDocumentId } from '@elementor/editor-elements';
|
|
3
|
+
import { FloatingPanelBody, FloatingPanelFooter, FloatingPanelHeader } from '@elementor/editor-floating-panels';
|
|
4
|
+
import { Box, Button, Typography } from '@elementor/ui';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
import { useAuditReport } from '../hooks/use-audit-report';
|
|
8
|
+
import AuditFeedback from './audit-feedback';
|
|
9
|
+
import ErrorPage from './pages/error-page';
|
|
10
|
+
import LoadingPage from './pages/loading-page';
|
|
11
|
+
import WelcomePage from './pages/welcome-page';
|
|
12
|
+
import ReportShell from './report-shell';
|
|
13
|
+
|
|
14
|
+
export default function AuditPanel() {
|
|
15
|
+
const { status, report, error, run } = useAuditReport();
|
|
16
|
+
|
|
17
|
+
const currentDocumentId = getCurrentDocumentId() ?? 0;
|
|
18
|
+
const onRun = () => run( currentDocumentId );
|
|
19
|
+
const lastScanLabel = report ? new Date( report.runAt ).toLocaleTimeString() : null;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<FloatingPanelHeader
|
|
24
|
+
panelId="audit-panel"
|
|
25
|
+
title={ __( 'Page Audit', 'elementor' ) }
|
|
26
|
+
badge={ __( 'Beta', 'elementor' ) }
|
|
27
|
+
titleVariant="subtitle2"
|
|
28
|
+
/>
|
|
29
|
+
<FloatingPanelBody>
|
|
30
|
+
{ status === 'idle' && <WelcomePage /> }
|
|
31
|
+
{ status === 'loading' && <LoadingPage /> }
|
|
32
|
+
{ status === 'error' && <ErrorPage message={ error ?? '' } onRetry={ onRun } /> }
|
|
33
|
+
{ status === 'ready' && report && <ReportShell report={ report } /> }
|
|
34
|
+
</FloatingPanelBody>
|
|
35
|
+
<FloatingPanelFooter>
|
|
36
|
+
{ lastScanLabel ? (
|
|
37
|
+
<Typography variant="caption" sx={ { flex: 1 } }>
|
|
38
|
+
{ __( 'Last scan:', 'elementor' ) } { lastScanLabel }
|
|
39
|
+
</Typography>
|
|
40
|
+
) : (
|
|
41
|
+
<Box sx={ { flex: 1 } } />
|
|
42
|
+
) }
|
|
43
|
+
{ lastScanLabel && <AuditFeedback /> }
|
|
44
|
+
<Button
|
|
45
|
+
variant="contained"
|
|
46
|
+
size="small"
|
|
47
|
+
onClick={ onRun }
|
|
48
|
+
disabled={ status === 'loading' || currentDocumentId === 0 }
|
|
49
|
+
>
|
|
50
|
+
{ lastScanLabel ? __( 'Re-scan', 'elementor' ) : __( 'Run page audit', 'elementor' ) }
|
|
51
|
+
</Button>
|
|
52
|
+
</FloatingPanelFooter>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ElementorAccessibilityIcon,
|
|
3
|
+
FilterIcon,
|
|
4
|
+
HeartHandShakeIcon,
|
|
5
|
+
Settings2Icon,
|
|
6
|
+
ShieldHalfFilledIcon,
|
|
7
|
+
} from '@elementor/icons';
|
|
8
|
+
|
|
9
|
+
import type { AuditCategory } from '../types';
|
|
10
|
+
|
|
11
|
+
export const CATEGORY_ICONS: Record< AuditCategory, typeof Settings2Icon > = {
|
|
12
|
+
'best-practices': Settings2Icon,
|
|
13
|
+
seo: FilterIcon,
|
|
14
|
+
accessibility: ElementorAccessibilityIcon,
|
|
15
|
+
performance: ShieldHalfFilledIcon,
|
|
16
|
+
compliance: HeartHandShakeIcon,
|
|
17
|
+
};
|