PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0
Elementor Website Builder – more than just a page builder v4.3.0
4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 All 454 releases
elementor / assets / js / packages / editor-audits / editor-audits.js

editor-audits.js in Elementor Website Builder – more than just a page builder 4.3.0, at assets/js/packages/editor-audits/editor-audits.js

2,813 lines 96.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function(_elementor_editor_floating_panels, _elementor_store, _elementor_editor_app_bar, _elementor_icons, _wordpress_i18n, react, _elementor_editor_elements, _elementor_ui, _elementor_http_client, _elementor_session, _elementor_editor_notifications, _elementor_editor_v1_adapters, _elementor_events, _elementor_editor_mcp) {
2
3 //#region \0rolldown/runtime.js
4 var __create = Object.create;
5 var __defProp = Object.defineProperty;
6 var __name = (target, value) => __defProp(target, "name", {
7 value,
8 configurable: true
9 });
10 var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11 var __getOwnPropNames = Object.getOwnPropertyNames;
12 var __getProtoOf = Object.getPrototypeOf;
13 var __hasOwnProp = Object.prototype.hasOwnProperty;
14 var __exportAll = (all, no_symbols) => {
15 let target = {};
16 for (var name in all) {
17 __defProp(target, name, {
18 get: all[name],
19 enumerable: true
20 });
21 }
22 if (!no_symbols) {
23 __defProp(target, Symbol.toStringTag, { value: "Module" });
24 }
25 return target;
26 };
27 var __copyProps = (to, from, except, desc) => {
28 if (from && typeof from === "object" || typeof from === "function") {
29 for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
30 key = keys[i];
31 if (!__hasOwnProp.call(to, key) && key !== except) {
32 __defProp(to, key, {
33 get: ((k) => from[k]).bind(null, key),
34 enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
35 });
36 }
37 }
38 }
39 return to;
40 };
41 var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
42 value: mod,
43 enumerable: true
44 }) : target, mod));
45
46 //#endregion
47 react = __toESM(react);
48
49 //#region packages/packages/core/editor-audits/src/utils/window-config.ts
50 function getWindowConfig() {
51 const config = window.elementorAudits;
52 if (!config) return {
53 restNamespace: "elementor/v1",
54 nonce: ""
55 };
56 return config;
57 }
58
59 //#endregion
60 //#region packages/packages/core/editor-audits/src/api/page-context-client.ts
61 async function fetchPageContext(documentId, attachmentIds) {
62 const { restNamespace, nonce } = getWindowConfig();
63 const url = `${restNamespace}/audits/page-context`;
64 return (await (0, _elementor_http_client.httpService)().get(url, {
65 params: {
66 document_id: documentId,
67 attachment_ids: attachmentIds
68 },
69 headers: { "X-WP-Nonce": nonce }
70 })).data;
71 }
72
73 //#endregion
74 //#region packages/packages/core/editor-audits/src/registry.ts
75 var registry = /* @__PURE__ */ new Map();
76 function registerAudit(audit) {
77 registry.set(audit.id, audit);
78 }
79 function getRegisteredAudits() {
80 return Array.from(registry.values());
81 }
82
83 //#endregion
84 //#region packages/packages/core/editor-audits/src/constants.ts
85 var AUDIT_PANEL_ID = "audit-panel";
86 var REPORT_STORAGE_KEY_PREFIX = "elementor/audits/report";
87 var CREATE_WIDGET_EVENT = "elementor/editor/create-widget";
88 var ANGIE_FIX_ENTRY_POINT = "audit_violation";
89 var FEEDBACK_EXPERIMENT_NAME = "in_editor_feedback";
90 var FEEDBACK_ENTRY_POINT = "audit_panel";
91 var FEEDBACK_CLICKED_EVENT = "audit_feedback_clicked";
92 var FEEDBACK_SENT_EVENT = "audit_feedback_sent";
93 var FEEDBACK_CANCELLED_EVENT = "audit_feedback_cancelled";
94 var FEEDBACK_CLOSED_EVENT = "audit_feedback_closed";
95 var ALL_CATEGORIES = [
96 "best-practices",
97 "seo",
98 "accessibility",
99 "performance",
100 "compliance"
101 ];
102 var CATEGORY_LABELS = {
103 "best-practices": (0, _wordpress_i18n.__)("Best Practices", "elementor"),
104 seo: (0, _wordpress_i18n.__)("SEO", "elementor"),
105 accessibility: (0, _wordpress_i18n.__)("Accessibility", "elementor"),
106 performance: (0, _wordpress_i18n.__)("Performance", "elementor"),
107 compliance: (0, _wordpress_i18n.__)("Compliance", "elementor")
108 };
109
110 //#endregion
111 //#region packages/packages/core/editor-audits/src/utils/sort-failed-audits.ts
112 var SEVERITY_SORTING_RANK = {
113 error: 0,
114 warning: 1,
115 info: 2
116 };
117 function sortFailedAuditResults(results) {
118 return [...results].sort((a, b) => {
119 const rankDiff = SEVERITY_SORTING_RANK[a.audit.severity] - SEVERITY_SORTING_RANK[b.audit.severity];
120 if (rankDiff !== 0) return rankDiff;
121 return a.audit.title.localeCompare(b.audit.title);
122 });
123 }
124
125 //#endregion
126 //#region packages/packages/core/editor-audits/src/utils/audit-status-summary.ts
127 function partitionAuditResults(report, options = {}) {
128 const { category, sortFailed = true } = options;
129 const failed = [];
130 const passed = [];
131 const skipped = [];
132 let totalViolations = 0;
133 for (const run of report.auditResults) {
134 if (category && !run.audit.categories.includes(category)) continue;
135 switch (run.result.status) {
136 case "fail":
137 failed.push({
138 ...run,
139 result: run.result
140 });
141 totalViolations += run.result.violations.length;
142 break;
143 case "pass":
144 passed.push({
145 ...run,
146 result: run.result
147 });
148 break;
149 case "skipped":
150 skipped.push({
151 ...run,
152 result: run.result
153 });
154 break;
155 }
156 }
157 return {
158 failed: sortFailed ? sortFailedAuditResults(failed) : failed,
159 passed,
160 skipped,
161 totalViolations
162 };
163 }
164 function auditStatusDisplayCounts(report) {
165 let pass = 0;
166 let skipped = 0;
167 let totalViolations = 0;
168 for (const { result } of report.auditResults) switch (result.status) {
169 case "fail":
170 totalViolations += result.violations.length;
171 break;
172 case "pass":
173 pass++;
174 break;
175 case "skipped":
176 skipped++;
177 break;
178 }
179 return {
180 fail: totalViolations,
181 pass,
182 skipped
183 };
184 }
185 function auditStatusColor(status) {
186 switch (status) {
187 case "fail": return "error";
188 case "pass": return "success";
189 case "skipped": return "default";
190 }
191 }
192 function auditStatusLabel(status) {
193 switch (status) {
194 case "fail": return (0, _wordpress_i18n.__)("Failed audits", "elementor");
195 case "pass": return (0, _wordpress_i18n.__)("Passed audits", "elementor");
196 case "skipped": return (0, _wordpress_i18n.__)("Skipped audits", "elementor");
197 }
198 }
199 function getPopulatedCategories(categoryTotals, categories) {
200 return categories.filter((category) => categoryTotals[category].total > 0);
201 }
202
203 //#endregion
204 //#region packages/packages/core/editor-audits/src/utils/compute-report.ts
205 function computeReport(documentId, results) {
206 const accumulators = Object.fromEntries(ALL_CATEGORIES.map((c) => [c, {
207 totalWeight: 0,
208 passedWeight: 0,
209 total: 0,
210 failed: 0
211 }]));
212 for (const { audit, result } of results) {
213 if (result.status === "skipped") continue;
214 for (const category of audit.categories) {
215 const acc = accumulators[category];
216 acc.total++;
217 acc.totalWeight += audit.weight;
218 if (result.status === "pass") acc.passedWeight += audit.weight;
219 else acc.failed++;
220 }
221 }
222 const categories = Object.fromEntries(ALL_CATEGORIES.map((c) => {
223 const acc = accumulators[c];
224 return [c, {
225 score: acc.totalWeight === 0 ? 0 : Math.round(acc.passedWeight / acc.totalWeight * 100),
226 failed: acc.failed,
227 total: acc.total
228 }];
229 }));
230 const populated = getPopulatedCategories(categories, ALL_CATEGORIES);
231 const overall = populated.length === 0 ? 0 : Math.round(populated.reduce((sum, c) => sum + categories[c].score, 0) / populated.length);
232 return {
233 documentId,
234 runAt: Date.now(),
235 overall,
236 categories,
237 auditResults: results
238 };
239 }
240
241 //#endregion
242 //#region packages/packages/core/editor-audits/src/utils/walk.ts
243 function walkElements(tree, visit) {
244 recurse(tree, [], visit);
245 }
246 function recurse(nodes, parents, visit) {
247 for (const node of nodes) {
248 visit(node, parents);
249 parents.push(node);
250 recurse(node.elements, parents, visit);
251 parents.pop();
252 }
253 }
254
255 //#endregion
256 //#region packages/packages/core/editor-audits/src/utils/image-like-sources.ts
257 var IMAGE_LIKE_WIDGETS = /* @__PURE__ */ new Set([
258 "image",
259 "image-box",
260 "image-carousel",
261 "gallery",
262 "image-gallery"
263 ]);
264 function hasPageImages(tree) {
265 let found = false;
266 walkImageLikeSources(tree, ({ media }) => {
267 if (media.id || media.url) found = true;
268 });
269 return found;
270 }
271 function walkImageLikeSources(tree, visit) {
272 walkElements(tree, (node) => {
273 if (node.elType !== "widget") return;
274 if (!IMAGE_LIKE_WIDGETS.has(node.widgetType ?? "")) return;
275 for (const media of collectMediaFromNode(node)) visit({
276 node,
277 media
278 });
279 });
280 }
281 function collectMediaFromNode(node) {
282 const sources = [];
283 const image = node.settings.image;
284 if (image?.id || image?.url) sources.push(image);
285 const gallery = node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery;
286 if (Array.isArray(gallery)) {
287 for (const item of gallery) if (item?.id || item?.url) sources.push(item);
288 }
289 return sources;
290 }
291
292 //#endregion
293 //#region packages/packages/core/editor-audits/src/utils/page-attachments.ts
294 function extractAttachmentIds(tree) {
295 const ids = /* @__PURE__ */ new Set();
296 walkImageLikeSources(tree, ({ media }) => {
297 if (media.id) ids.add(media.id);
298 });
299 return Array.from(ids).sort((a, b) => a - b);
300 }
301
302 //#endregion
303 //#region packages/packages/core/editor-audits/src/utils/read-kit-snapshot.ts
304 function hasNonEmptyGlobalValue(item) {
305 return item.value.length > 0;
306 }
307 async function readKitSnapshot(kitId) {
308 const [colorsFromApi, fontsFromApi] = await Promise.all([readGlobalsFromApi("globals/colors", mapApiColor, hasNonEmptyGlobalValue), readGlobalsFromApi("globals/typography", mapApiTypography, hasNonEmptyGlobalValue)]);
309 const documentSettings = colorsFromApi.length === 0 || fontsFromApi.length === 0 ? getKitDocumentSettings(kitId) : null;
310 return {
311 id: kitId,
312 globals: {
313 colors: colorsFromApi.length > 0 ? colorsFromApi : readColorsFromKitDocumentSettings(documentSettings),
314 fonts: fontsFromApi.length > 0 ? fontsFromApi : readFontsFromKitDocumentSettings(documentSettings)
315 }
316 };
317 }
318 async function readGlobalsFromApi(command, mapItem, hasValue) {
319 const $e = window.$e;
320 if (!$e?.data?.get) return [];
321 try {
322 const data = (await $e.data.get(command))?.data ?? {};
323 return Object.values(data).map((item) => mapItem(item)).filter(hasValue);
324 } catch {
325 return [];
326 }
327 }
328 function mapApiColor(item) {
329 return {
330 id: item.id,
331 value: item.value ?? "",
332 title: item.title ?? item.id
333 };
334 }
335 function mapApiTypography(item) {
336 return {
337 id: item.id,
338 value: item.value?.typography_font_family ?? "",
339 title: item.title ?? item.id
340 };
341 }
342 function readColorsFromKitDocumentSettings(settings) {
343 if (!settings) return [];
344 return [...settings.system_colors ?? [], ...settings.custom_colors ?? []].map((row) => ({
345 id: row._id,
346 value: row.color,
347 title: row.title ?? row._id
348 })).filter(hasNonEmptyGlobalValue);
349 }
350 function readFontsFromKitDocumentSettings(settings) {
351 if (!settings) return [];
352 return [...settings.system_typography ?? [], ...settings.custom_typography ?? []].map((row) => ({
353 id: row._id,
354 value: row.typography_font_family ?? "",
355 title: row.title ?? row._id
356 })).filter(hasNonEmptyGlobalValue);
357 }
358 function getKitDocumentSettings(kitId) {
359 return (window.elementor?.documents?.get?.(kitId))?.config?.settings?.settings ?? null;
360 }
361
362 //#endregion
363 //#region packages/packages/core/editor-audits/src/utils/v1-snapshot.ts
364 function buildSnapshotTree(elements) {
365 if (elements.length === 0) return [];
366 return (elements[0].children ?? []).map(toSnapshot);
367 }
368 function toSnapshot(element) {
369 const model = element.model;
370 const settings = element.settings?.toJSON?.() ?? {};
371 return {
372 id: element.id,
373 elType: model.get("elType") ?? "",
374 widgetType: model.get("widgetType"),
375 settings,
376 elements: (element.children ?? []).map(toSnapshot)
377 };
378 }
379
380 //#endregion
381 //#region packages/packages/core/editor-audits/src/runner.ts
382 async function runPageAudit(documentId) {
383 const tree = buildSnapshotTree((0, _elementor_editor_elements.getElements)());
384 const pageContext = await fetchPageContext(documentId, extractAttachmentIds(tree));
385 const ctx = {
386 documentId,
387 elements: {
388 documentId,
389 tree
390 },
391 pageContext,
392 kit: await readKitSnapshot(pageContext.kit_id)
393 };
394 const registered = getRegisteredAudits();
395 return computeReport(documentId, await Promise.all(registered.map(async (audit) => {
396 const { evaluate: _evaluate, ...meta } = audit;
397 try {
398 return {
399 audit: meta,
400 result: await audit.evaluate(ctx)
401 };
402 } catch (error) {
403 return {
404 audit: meta,
405 result: {
406 status: "skipped",
407 reason: error instanceof Error ? error.message : "unknown-error"
408 }
409 };
410 }
411 })));
412 }
413
414 //#endregion
415 //#region packages/packages/core/editor-audits/src/store/slice.ts
416 var initialState = {
417 status: "idle",
418 report: null,
419 error: null
420 };
421 var slice = (0, _elementor_store.__createSlice)({
422 name: "audits",
423 initialState,
424 reducers: {
425 runStarted(state) {
426 state.status = "loading";
427 state.error = null;
428 },
429 runSucceeded(state, action) {
430 state.status = "ready";
431 state.report = action.payload;
432 },
433 runFailed(state, action) {
434 state.status = "error";
435 state.error = action.payload;
436 },
437 reportRestored(state, action) {
438 state.status = "ready";
439 state.report = action.payload;
440 state.error = null;
441 },
442 reportCleared(state) {
443 state.status = "idle";
444 state.report = null;
445 state.error = null;
446 }
447 }
448 });
449
450 //#endregion
451 //#region packages/packages/core/editor-audits/src/store/selectors.ts
452 function selectStatus(state) {
453 return state.audits.status;
454 }
455 function selectReport(state) {
456 return state.audits.report;
457 }
458 function selectError(state) {
459 return state.audits.error;
460 }
461
462 //#endregion
463 //#region packages/packages/core/editor-audits/src/utils/report-storage.ts
464 function getStorageKey(documentId) {
465 return `${REPORT_STORAGE_KEY_PREFIX}/${documentId}`;
466 }
467 function getPersistedReport(documentId) {
468 return (0, _elementor_session.getSessionStorageItem)(getStorageKey(documentId)) ?? null;
469 }
470 function persistReport(documentId, report) {
471 (0, _elementor_session.setSessionStorageItem)(getStorageKey(documentId), report);
472 }
473
474 //#endregion
475 //#region packages/packages/core/editor-audits/src/hooks/use-audit-report.ts
476 function useAuditReport() {
477 const status = (0, _elementor_store.__useSelector)((state) => selectStatus(state));
478 const report = (0, _elementor_store.__useSelector)((state) => selectReport(state));
479 const error = (0, _elementor_store.__useSelector)((state) => selectError(state));
480 const dispatch = (0, _elementor_store.__useDispatch)();
481 const documentId = (0, _elementor_editor_elements.getCurrentDocumentId)() ?? 0;
482 (0, react.useEffect)(() => {
483 if (!documentId || report?.documentId === documentId) return;
484 const persisted = getPersistedReport(documentId);
485 if (persisted) dispatch(slice.actions.reportRestored(persisted));
486 else if (report) dispatch(slice.actions.reportCleared());
487 }, [
488 documentId,
489 report,
490 dispatch
491 ]);
492 const run = async (documentIdToRun) => {
493 dispatch(slice.actions.runStarted());
494 try {
495 const nextReport = await runPageAudit(documentIdToRun);
496 dispatch(slice.actions.runSucceeded(nextReport));
497 persistReport(documentIdToRun, nextReport);
498 } catch (e) {
499 dispatch(slice.actions.runFailed(e instanceof Error ? e.message : "Unknown error"));
500 }
501 };
502 return {
503 status,
504 report,
505 error,
506 run
507 };
508 }
509
510 //#endregion
511 //#region packages/packages/core/editor-audits/src/components/audit-feedback.tsx
512 var FEEDBACK_SUBJECT = "Page Audit Tool";
513 var FEEDBACK_SUBJECT_LABEL = (0, _wordpress_i18n.__)("Page Audit Tool", "elementor");
514 var SUCCESS_NOTIFICATION_ID = "audit-feedback-success";
515 var ERROR_NOTIFICATION_ID = "audit-feedback-error";
516 var DIALOG_TITLE_ID = "audit-feedback-dialog-title";
517 var DIALOG_DESCRIPTION_ID = "audit-feedback-dialog-description";
518 var isUserConnected = () => Boolean(window.elementorCommon?.config?.library_connect?.is_connected || window.elementorPro?.config?.isActive);
519 var notifySuccess = () => (0, _elementor_editor_notifications.notify)({
520 id: SUCCESS_NOTIFICATION_ID,
521 type: "success",
522 message: (0, _wordpress_i18n.__)("Feedback sent. Thanks for helping us out.", "elementor")
523 });
524 var notifyError = () => (0, _elementor_editor_notifications.notify)({
525 id: ERROR_NOTIFICATION_ID,
526 type: "error",
527 message: (0, _wordpress_i18n.__)("Something went wrong. Please try sending your feedback again.", "elementor")
528 });
529 function AuditFeedback() {
530 const [isOpen, setIsOpen] = (0, react.useState)(false);
531 const [feedbackText, setFeedbackText] = (0, react.useState)("");
532 const [isSubmitting, setIsSubmitting] = (0, react.useState)(false);
533 const { dispatchEvent: trackEvent = (...args) => void 0 } = (0, _elementor_events.useMixpanel)();
534 if (!(0, _elementor_editor_v1_adapters.isExperimentActive)("in_editor_feedback")) return null;
535 const connected = isUserConnected();
536 const connectUrl = window.elementor?.config?.user?.top_bar?.connect_url;
537 if (!connected && !connectUrl) return null;
538 const triggerLabel = (0, _wordpress_i18n.__)("Give feedback", "elementor");
539 const handleOpen = () => {
540 if (!connected) {
541 window.open(connectUrl, "_blank", "noopener");
542 trackEvent(FEEDBACK_CLICKED_EVENT, {
543 entry_point: FEEDBACK_ENTRY_POINT,
544 connected: false
545 });
546 return;
547 }
548 setIsOpen(true);
549 trackEvent(FEEDBACK_CLICKED_EVENT, {
550 entry_point: FEEDBACK_ENTRY_POINT,
551 connected: true
552 });
553 };
554 const dismiss = (eventName) => {
555 setIsOpen(false);
556 setFeedbackText("");
557 trackEvent(eventName, { entry_point: FEEDBACK_ENTRY_POINT });
558 };
559 const handleCancel = () => dismiss(FEEDBACK_CANCELLED_EVENT);
560 const handleClose = () => dismiss(FEEDBACK_CLOSED_EVENT);
561 const handleSubmit = () => {
562 setIsSubmitting(true);
563 (0, _elementor_http_client.httpService)().post("elementor/v1/feedback/submit", {
564 subject: FEEDBACK_SUBJECT,
565 description: feedbackText.trim()
566 }).then((response) => {
567 if (!response.data.success) {
568 notifyError();
569 return;
570 }
571 setIsOpen(false);
572 setFeedbackText("");
573 notifySuccess();
574 trackEvent(FEEDBACK_SENT_EVENT, { entry_point: FEEDBACK_ENTRY_POINT });
575 }).catch(notifyError).finally(() => setIsSubmitting(false));
576 };
577 return /* @__PURE__ */ react.createElement(react.Fragment, null, /* @__PURE__ */ react.createElement(_elementor_ui.Tooltip, {
578 title: triggerLabel,
579 placement: "top"
580 }, /* @__PURE__ */ react.createElement(_elementor_ui.IconButton, {
581 size: "small",
582 "aria-label": triggerLabel,
583 onClick: handleOpen
584 }, /* @__PURE__ */ react.createElement(_elementor_icons.MessageLinesIcon, { fontSize: "small" }))), /* @__PURE__ */ react.createElement(_elementor_ui.Dialog, {
585 open: isOpen,
586 onClose: handleClose,
587 "aria-labelledby": DIALOG_TITLE_ID,
588 "aria-describedby": DIALOG_DESCRIPTION_ID,
589 PaperProps: { sx: { borderRadius: 1 } }
590 }, /* @__PURE__ */ react.createElement(_elementor_ui.DialogHeader, {
591 logo: false,
592 onClose: handleClose
593 }, /* @__PURE__ */ react.createElement(_elementor_ui.DialogTitle, { id: DIALOG_TITLE_ID }, (0, _wordpress_i18n.__)("Let us know what you think", "elementor"))), /* @__PURE__ */ react.createElement(_elementor_ui.DialogContent, { dividers: true }, /* @__PURE__ */ react.createElement(_elementor_ui.Stack, {
594 direction: "column",
595 gap: 2
596 }, /* @__PURE__ */ react.createElement(_elementor_ui.TextField, {
597 value: FEEDBACK_SUBJECT_LABEL,
598 disabled: true,
599 fullWidth: true,
600 size: "small"
601 }), /* @__PURE__ */ react.createElement(_elementor_ui.TextField, {
602 placeholder: (0, _wordpress_i18n.__)("Tell us what you had in mind, what we can improve or fix to make this feature better.", "elementor"),
603 fullWidth: true,
604 multiline: true,
605 rows: 4,
606 disabled: isSubmitting,
607 onChange: (event) => setFeedbackText(event.target.value),
608 value: feedbackText
609 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
610 id: DIALOG_DESCRIPTION_ID,
611 variant: "caption",
612 color: "text.secondary"
613 }, (0, _wordpress_i18n.__)("We appreciate your feedback! While we review all submissions, we can't guarantee that every suggestion will result in a change or update.", "elementor")))), /* @__PURE__ */ react.createElement(_elementor_ui.DialogActions, null, /* @__PURE__ */ react.createElement(_elementor_ui.Button, {
614 variant: "text",
615 color: "secondary",
616 onClick: handleCancel
617 }, (0, _wordpress_i18n.__)("Cancel", "elementor")), /* @__PURE__ */ react.createElement(_elementor_ui.Button, {
618 variant: "contained",
619 color: "primary",
620 disabled: isSubmitting || feedbackText.trim().length === 0,
621 onClick: handleSubmit
622 }, (0, _wordpress_i18n.__)("Send", "elementor")))));
623 }
624
625 //#endregion
626 //#region packages/packages/core/editor-audits/src/components/pages/error-page.tsx
627 function ErrorPage({ message, onRetry }) {
628 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
629 display: "flex",
630 flexDirection: "column",
631 alignItems: "center",
632 gap: 1.5,
633 p: 4
634 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
635 variant: "body2",
636 color: "error",
637 textAlign: "center"
638 }, message), /* @__PURE__ */ react.createElement(_elementor_ui.Button, {
639 variant: "contained",
640 size: "small",
641 onClick: onRetry
642 }, (0, _wordpress_i18n.__)("Try again", "elementor")));
643 }
644
645 //#endregion
646 //#region packages/packages/core/editor-audits/src/components/pages/loading-page.tsx
647 function LoadingPage() {
648 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
649 display: "flex",
650 flexDirection: "column",
651 alignItems: "center",
652 gap: 1.5,
653 p: 4
654 } }, /* @__PURE__ */ react.createElement(_elementor_ui.CircularProgress, { size: 32 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, { variant: "body2" }, (0, _wordpress_i18n.__)("Audit in progress.", "elementor")));
655 }
656
657 //#endregion
658 //#region packages/packages/core/editor-audits/src/components/pages/welcome-page.tsx
659 var INFORMATION_IMAGE_PATH = "images/information.svg";
660 function WelcomePage() {
661 const assetsUrl = window.elementorCommon?.config?.urls?.assets ?? "";
662 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
663 display: "flex",
664 flexDirection: "column",
665 alignItems: "center",
666 justifyContent: "center",
667 gap: 1,
668 p: 3
669 } }, /* @__PURE__ */ react.createElement("img", {
670 className: "elementor-nerd-box-icon",
671 src: `${assetsUrl}${INFORMATION_IMAGE_PATH}`,
672 loading: "lazy",
673 alt: (0, _wordpress_i18n.__)("Elementor", "elementor")
674 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
675 variant: "subtitle1",
676 component: "h3",
677 sx: { marginBlockStart: 2 }
678 }, (0, _wordpress_i18n.__)("Audit your page!", "elementor")), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
679 variant: "caption",
680 textAlign: "center"
681 }, (0, _wordpress_i18n.__)("Check SEO, accessibility, performance, and best practices. Click \"Run page audit\" to begin.", "elementor")));
682 }
683
684 //#endregion
685 //#region packages/packages/core/editor-audits/src/components/status-section.tsx
686 function StatusSection({ label, count, color = "default", defaultExpanded = false, children }) {
687 const [expanded, setExpanded] = (0, react.useState)(defaultExpanded);
688 if (count === 0) return null;
689 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: { paddingBlock: 1 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
690 sx: {
691 display: "flex",
692 alignItems: "center",
693 gap: .5,
694 cursor: "pointer"
695 },
696 onClick: () => setExpanded((v) => !v)
697 }, /* @__PURE__ */ react.createElement(_elementor_ui.IconButton, {
698 size: "small",
699 "aria-label": expanded ? (0, _wordpress_i18n.__)("Collapse", "elementor") : (0, _wordpress_i18n.__)("Expand", "elementor")
700 }, /* @__PURE__ */ react.createElement(_elementor_icons.ChevronDownIcon, {
701 fontSize: "small",
702 sx: {
703 transform: expanded ? "rotate(180deg)" : void 0,
704 transition: "transform .2s"
705 }
706 })), /* @__PURE__ */ react.createElement(_elementor_ui.Chip, {
707 label: `${label} (${count})`,
708 size: "small",
709 color,
710 variant: "standard"
711 })), /* @__PURE__ */ react.createElement(_elementor_ui.Collapse, { in: expanded }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
712 paddingInlineStart: 4.25,
713 paddingBlock: 2
714 } }, children)));
715 }
716
717 //#endregion
718 //#region packages/packages/core/editor-audits/src/components/subpage-header.tsx
719 function SubpageHeader({ title, onBack, backLabel, icon }) {
720 const isRtl = "rtl" === (0, _elementor_ui.useTheme)().direction;
721 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
722 display: "flex",
723 alignItems: "center",
724 gap: .5,
725 p: 1
726 } }, /* @__PURE__ */ react.createElement(_elementor_ui.IconButton, {
727 size: "small",
728 onClick: onBack,
729 "aria-label": backLabel ?? (0, _wordpress_i18n.__)("Back", "elementor")
730 }, /* @__PURE__ */ react.createElement(_elementor_ui.Rotate, { in: isRtl }, /* @__PURE__ */ react.createElement(_elementor_icons.ArrowLeftIcon, { fontSize: "small" }))), icon, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
731 variant: "subtitle2",
732 fontWeight: "bold"
733 }, title));
734 }
735
736 //#endregion
737 //#region packages/packages/core/editor-audits/src/hooks/focus-violation.ts
738 var SCROLL_TO_VIEW_DELAY_MS = 200;
739 function focusViolation(violation) {
740 if (violation.elementId) {
741 const container = (0, _elementor_editor_elements.getContainer)(violation.elementId);
742 if (container) {
743 (0, _elementor_editor_elements.selectElement)(violation.elementId);
744 const domElement = container.view?.getDomElement?.();
745 if (domElement) window.elementor?.helpers?.scrollToView?.(domElement, SCROLL_TO_VIEW_DELAY_MS);
746 }
747 if (violation.targetHint === "element-settings") (0, _elementor_editor_v1_adapters.__privateRunCommand)("panel/editor/open");
748 return;
749 }
750 if (violation.externalUrl) {
751 window.open(violation.externalUrl, "_blank");
752 return;
753 }
754 if (violation.targetHint === "page-settings") {
755 (0, _elementor_editor_v1_adapters.__privateOpenRoute)("panel/page-settings/settings");
756 return;
757 }
758 if (violation.targetHint === "site-settings") {
759 (0, _elementor_editor_v1_adapters.__privateRunCommand)("panel/global/open");
760 return;
761 }
762 if (violation.targetHint === "site-identity-settings") (0, _elementor_editor_v1_adapters.__privateRunCommand)("panel/global/open").then(() => {
763 (0, _elementor_editor_v1_adapters.__privateOpenRoute)("panel/global/settings-site-identity");
764 });
765 }
766
767 //#endregion
768 //#region packages/packages/core/editor-audits/src/utils/build-angie-prompt.ts
769 function buildAngiePrompt(violationText) {
770 return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("Help me fix: %s", "elementor"), violationText);
771 }
772
773 //#endregion
774 //#region packages/packages/core/editor-audits/src/utils/keyboard-click.ts
775 function onKeyboardClick(callback) {
776 return (event) => {
777 if (event.key === "Enter" || event.key === " ") {
778 event.preventDefault();
779 callback();
780 }
781 };
782 }
783
784 //#endregion
785 //#region packages/packages/core/editor-audits/src/components/fix-violation-with-angie.tsx
786 function FixViolationWithAngie({ prompt }) {
787 const label = (0, _wordpress_i18n.__)("Fix with Angie", "elementor");
788 const href = `#angie-prompt=${encodeURIComponent(prompt)}`;
789 const handleClick = (event) => {
790 event.stopPropagation();
791 event.preventDefault();
792 if ((0, _elementor_editor_mcp.isAngieAvailable)()) {
793 (0, _elementor_editor_mcp.sendPromptToAngie)(prompt);
794 return;
795 }
796 window.dispatchEvent(new CustomEvent(CREATE_WIDGET_EVENT, { detail: {
797 entry_point: ANGIE_FIX_ENTRY_POINT,
798 prompt
799 } }));
800 };
801 return /* @__PURE__ */ react.createElement(_elementor_ui.Tooltip, { title: label }, /* @__PURE__ */ react.createElement(_elementor_ui.IconButton, {
802 className: "violation-hover-icon",
803 component: "a",
804 href,
805 size: "small",
806 "aria-label": label,
807 onClick: handleClick
808 }, /* @__PURE__ */ react.createElement(_elementor_icons.AngieIcon, { fontSize: "tiny" })));
809 }
810
811 //#endregion
812 //#region packages/packages/core/editor-audits/src/components/severity-icons.tsx
813 var SEVERITY_CONFIG = {
814 error: {
815 Icon: _elementor_icons.AlertTriangleFilledIcon,
816 color: "error"
817 },
818 warning: {
819 Icon: _elementor_icons.AlertCircleIcon,
820 color: "warning"
821 },
822 info: {
823 Icon: _elementor_icons.InfoCircleIcon,
824 color: "info"
825 }
826 };
827 function SeverityIcon({ severity }) {
828 const { Icon, color } = SEVERITY_CONFIG[severity];
829 return /* @__PURE__ */ react.createElement(Icon, {
830 fontSize: "small",
831 color
832 });
833 }
834
835 //#endregion
836 //#region packages/packages/core/editor-audits/src/components/violation-icons.tsx
837 function ViolationIcon({ violation, widgetIcon }) {
838 if (widgetIcon) return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
839 component: "i",
840 className: widgetIcon,
841 "aria-hidden": true,
842 sx: {
843 fontSize: "inherit",
844 width: "1em",
845 textAlign: "center"
846 }
847 });
848 if (violation.targetHint === "page-settings") return /* @__PURE__ */ react.createElement(_elementor_icons.FileSettingsIcon, {
849 fontSize: "inherit",
850 "aria-hidden": true
851 });
852 if (violation.targetHint === "site-settings" || violation.targetHint === "site-identity-settings") return /* @__PURE__ */ react.createElement(_elementor_icons.SettingsIcon, {
853 fontSize: "inherit",
854 "aria-hidden": true
855 });
856 return /* @__PURE__ */ react.createElement(_elementor_icons.ShieldCheckIcon, {
857 fontSize: "inherit",
858 "aria-hidden": true
859 });
860 }
861
862 //#endregion
863 //#region packages/packages/core/editor-audits/src/components/violation-row.tsx
864 function SkipReasonTooltip({ reason }) {
865 return /* @__PURE__ */ react.createElement(_elementor_ui.Tooltip, {
866 title: reason,
867 placement: "top"
868 }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
869 "aria-label": reason,
870 component: "span",
871 sx: {
872 display: "inline-flex",
873 alignItems: "center"
874 }
875 }, /* @__PURE__ */ react.createElement(_elementor_icons.HelpIcon, {
876 fontSize: "small",
877 color: "action"
878 })));
879 }
880 function StatusIndicator({ audit, violations }) {
881 if (violations) return /* @__PURE__ */ react.createElement(react.Fragment, null, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
882 variant: "caption",
883 color: "text.secondary",
884 fontWeight: "bold"
885 }, violations.length), /* @__PURE__ */ react.createElement(SeverityIcon, { severity: audit.severity }));
886 return /* @__PURE__ */ react.createElement(_elementor_icons.CheckIcon, {
887 fontSize: "small",
888 color: "success"
889 });
890 }
891 function ViolationRow({ audit, skipReason, violations }) {
892 const [expanded, setExpanded] = (0, react.useState)(false);
893 const toggleExpanded = () => setExpanded((value) => !value);
894 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
895 borderBottom: 1,
896 borderColor: "divider",
897 paddingBlock: .5
898 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
899 display: "flex",
900 alignItems: "center",
901 gap: .5
902 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
903 sx: {
904 alignItems: "center",
905 cursor: "pointer",
906 display: "flex",
907 flex: 1,
908 gap: .5,
909 minWidth: 0
910 },
911 onClick: toggleExpanded
912 }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
913 variant: "body2",
914 sx: { flex: 1 }
915 }, audit.title), !skipReason && /* @__PURE__ */ react.createElement(StatusIndicator, {
916 audit,
917 violations
918 })), skipReason && /* @__PURE__ */ react.createElement(SkipReasonTooltip, { reason: skipReason }), /* @__PURE__ */ react.createElement(_elementor_ui.IconButton, {
919 size: "small",
920 "aria-label": expanded ? (0, _wordpress_i18n.__)("Collapse", "elementor") : (0, _wordpress_i18n.__)("Expand", "elementor"),
921 onClick: toggleExpanded
922 }, /* @__PURE__ */ react.createElement(_elementor_icons.ChevronDownIcon, {
923 fontSize: "small",
924 sx: {
925 transform: expanded ? "rotate(180deg)" : void 0,
926 transition: "transform .2s"
927 }
928 }))), /* @__PURE__ */ react.createElement(_elementor_ui.Collapse, { in: expanded }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
929 display: "flex",
930 flexDirection: "column",
931 gap: 1,
932 paddingBlock: 1
933 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Alert, {
934 severity: "secondary",
935 sx: { p: 1 },
936 icon: /* @__PURE__ */ react.createElement(_elementor_icons.AlertCircleIcon, {
937 fontSize: "small",
938 color: "secondary",
939 "aria-hidden": true
940 })
941 }, /* @__PURE__ */ react.createElement(_elementor_ui.AlertTitle, null, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
942 variant: "caption",
943 component: "p",
944 color: "text.primary",
945 fontWeight: "bold"
946 }, (0, _wordpress_i18n.__)("What's the issue", "elementor"))), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
947 variant: "caption",
948 component: "p",
949 color: "text.secondary"
950 }, audit.description)), /* @__PURE__ */ react.createElement(_elementor_ui.Alert, {
951 severity: "info",
952 sx: { p: 1 },
953 icon: /* @__PURE__ */ react.createElement(_elementor_icons.BulbIcon, {
954 fontSize: "small",
955 color: "info",
956 "aria-hidden": true
957 })
958 }, /* @__PURE__ */ react.createElement(_elementor_ui.AlertTitle, null, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
959 variant: "caption",
960 component: "p",
961 color: "text.primary",
962 fontWeight: "bold"
963 }, (0, _wordpress_i18n.__)("How to resolve", "elementor"))), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
964 variant: "caption",
965 component: "p",
966 color: "text.secondary"
967 }, audit.fixHint))), violations && violations.length > 0 && /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
968 role: "list",
969 sx: {
970 paddingBlockEnd: 1,
971 paddingInlineStart: 2
972 }
973 }, violations.map((violation, idx) => {
974 const widgetIcon = violation.elementId ? (0, _elementor_editor_elements.getElementIcon)(violation.elementId) : null;
975 const elementTitle = violation.elementId ? (0, _elementor_editor_elements.getElementTitle)(violation.elementId) : null;
976 const rowLabel = elementTitle ? `${elementTitle} - ${violation.label}` : violation.label;
977 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
978 key: idx,
979 role: "button",
980 tabIndex: 0,
981 onClick: () => focusViolation(violation),
982 onKeyDown: onKeyboardClick(() => focusViolation(violation)),
983 sx: {
984 display: "flex",
985 alignItems: "center",
986 gap: 1,
987 paddingBlock: .5,
988 paddingInline: 2,
989 borderRadius: 1,
990 cursor: "pointer",
991 "& .violation-hover-icon": {
992 opacity: 0,
993 transition: "opacity .15s"
994 },
995 "&:hover": { bgcolor: "action.hover" },
996 "&:hover .violation-hover-icon, &:focus-visible .violation-hover-icon": { opacity: 1 }
997 }
998 }, /* @__PURE__ */ react.createElement(ViolationIcon, {
999 violation,
1000 widgetIcon
1001 }), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: { flex: 1 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, { variant: "caption" }, rowLabel), violation.detail && /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1002 variant: "caption",
1003 color: "text.secondary"
1004 }, violation.detail)), violation.angieFix && /* @__PURE__ */ react.createElement(FixViolationWithAngie, { prompt: buildAngiePrompt(rowLabel) }), /* @__PURE__ */ react.createElement(_elementor_icons.EyeIcon, {
1005 className: "violation-hover-icon",
1006 fontSize: "tiny",
1007 "aria-hidden": true
1008 }));
1009 }))));
1010 }
1011
1012 //#endregion
1013 //#region packages/packages/core/editor-audits/src/components/pages/all-audits-page.tsx
1014 function AllAuditsPage({ initialExpandedStatus, onBack, report }) {
1015 const { failed, passed, skipped, totalViolations } = partitionAuditResults(report);
1016 const expandFail = !initialExpandedStatus || initialExpandedStatus === "fail";
1017 const expandPass = initialExpandedStatus === "pass";
1018 const expandSkipped = initialExpandedStatus === "skipped";
1019 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { key: initialExpandedStatus ?? "default" }, /* @__PURE__ */ react.createElement(SubpageHeader, {
1020 title: (0, _wordpress_i18n.__)("All audits", "elementor"),
1021 onBack
1022 }), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: { p: 1 } }, /* @__PURE__ */ react.createElement(StatusSection, {
1023 label: auditStatusLabel("fail"),
1024 count: totalViolations,
1025 color: auditStatusColor("fail"),
1026 defaultExpanded: expandFail
1027 }, failed.map((r) => /* @__PURE__ */ react.createElement(ViolationRow, {
1028 key: r.audit.id,
1029 audit: r.audit,
1030 violations: r.result.violations
1031 }))), /* @__PURE__ */ react.createElement(StatusSection, {
1032 label: auditStatusLabel("pass"),
1033 count: passed.length,
1034 color: auditStatusColor("pass"),
1035 defaultExpanded: expandPass
1036 }, passed.map((r) => /* @__PURE__ */ react.createElement(ViolationRow, {
1037 key: r.audit.id,
1038 audit: r.audit
1039 }))), /* @__PURE__ */ react.createElement(StatusSection, {
1040 label: auditStatusLabel("skipped"),
1041 count: skipped.length,
1042 color: auditStatusColor("skipped"),
1043 defaultExpanded: expandSkipped
1044 }, skipped.map((r) => /* @__PURE__ */ react.createElement(ViolationRow, {
1045 key: r.audit.id,
1046 audit: r.audit,
1047 skipReason: r.result.reason
1048 })))));
1049 }
1050
1051 //#endregion
1052 //#region packages/packages/core/editor-audits/src/components/category-icons.ts
1053 var CATEGORY_ICONS = {
1054 "best-practices": _elementor_icons.Settings2Icon,
1055 seo: _elementor_icons.FilterIcon,
1056 accessibility: _elementor_icons.ElementorAccessibilityIcon,
1057 performance: _elementor_icons.ShieldHalfFilledIcon,
1058 compliance: _elementor_icons.HeartHandShakeIcon
1059 };
1060
1061 //#endregion
1062 //#region packages/packages/core/editor-audits/src/components/pages/category-page.tsx
1063 function CategoryPage({ category, report, onBack }) {
1064 const Icon = CATEGORY_ICONS[category];
1065 const { failed, passed, totalViolations } = partitionAuditResults(report, { category });
1066 return /* @__PURE__ */ react.createElement(react.Fragment, null, /* @__PURE__ */ react.createElement(SubpageHeader, {
1067 title: CATEGORY_LABELS[category],
1068 onBack,
1069 backLabel: (0, _wordpress_i18n.__)("Back to all issues", "elementor"),
1070 icon: /* @__PURE__ */ react.createElement(Icon, {
1071 fontSize: "small",
1072 color: "action"
1073 })
1074 }), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: { p: 1 } }, /* @__PURE__ */ react.createElement(StatusSection, {
1075 label: (0, _wordpress_i18n.__)("Failed audits", "elementor"),
1076 count: totalViolations,
1077 color: "error",
1078 defaultExpanded: true
1079 }, failed.map((r) => /* @__PURE__ */ react.createElement(ViolationRow, {
1080 key: r.audit.id,
1081 audit: r.audit,
1082 violations: r.result.violations
1083 }))), /* @__PURE__ */ react.createElement(StatusSection, {
1084 label: (0, _wordpress_i18n.__)("Passed audits", "elementor"),
1085 count: passed.length,
1086 color: "success"
1087 }, passed.map((r) => /* @__PURE__ */ react.createElement(ViolationRow, {
1088 key: r.audit.id,
1089 audit: r.audit
1090 })))));
1091 }
1092
1093 //#endregion
1094 //#region packages/packages/core/editor-audits/src/utils/severity-counts.ts
1095 var ALL_SEVERITIES = [
1096 "error",
1097 "warning",
1098 "info"
1099 ];
1100 function countSeverities(report, category) {
1101 const counts = {
1102 error: 0,
1103 warning: 0,
1104 info: 0
1105 };
1106 for (const { audit, result } of report.auditResults) {
1107 if (result.status !== "fail") continue;
1108 if (category && !audit.categories.includes(category)) continue;
1109 counts[audit.severity] += result.violations.length;
1110 }
1111 return counts;
1112 }
1113 function severityPluralLabel(severity) {
1114 switch (severity) {
1115 case "error": return (0, _wordpress_i18n.__)("Errors", "elementor");
1116 case "warning": return (0, _wordpress_i18n.__)("Warnings", "elementor");
1117 case "info": return (0, _wordpress_i18n.__)("Info", "elementor");
1118 }
1119 }
1120 function severityRemainingCountLabel(severity, count) {
1121 switch (severity) {
1122 case "error": return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%d errors", "elementor"), count);
1123 case "warning": return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%d warnings", "elementor"), count);
1124 case "info": return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%d info", "elementor"), count);
1125 }
1126 }
1127
1128 //#endregion
1129 //#region packages/packages/core/editor-audits/src/components/issues-category-row.tsx
1130 function IssuesCategoryRow({ category, label, counts, onClick }) {
1131 const isRtl = "rtl" === (0, _elementor_ui.useTheme)().direction;
1132 const Icon = CATEGORY_ICONS[category];
1133 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1134 role: "button",
1135 tabIndex: 0,
1136 onClick,
1137 onKeyDown: onKeyboardClick(onClick),
1138 sx: {
1139 display: "flex",
1140 alignItems: "center",
1141 gap: 1,
1142 px: 2,
1143 py: 1.5,
1144 cursor: "pointer",
1145 borderRadius: 1,
1146 border: 1,
1147 borderColor: "divider",
1148 "&:hover": { bgcolor: "action.hover" },
1149 outline: "none",
1150 "&:focus-visible": {
1151 outline: "2px solid",
1152 outlineColor: "primary.main"
1153 }
1154 }
1155 }, /* @__PURE__ */ react.createElement(Icon, {
1156 fontSize: "small",
1157 color: "action"
1158 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1159 variant: "body2",
1160 fontWeight: "bold",
1161 sx: { flex: 1 }
1162 }, label), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1163 display: "flex",
1164 alignItems: "center",
1165 gap: .5
1166 } }, ALL_SEVERITIES.filter((s) => counts[s] > 0).map((severity) => /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1167 key: severity,
1168 sx: {
1169 display: "flex",
1170 alignItems: "center",
1171 gap: .25
1172 }
1173 }, /* @__PURE__ */ react.createElement(SeverityIcon, { severity }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1174 variant: "caption",
1175 color: "text.primary",
1176 fontWeight: "bold"
1177 }, counts[severity])))), /* @__PURE__ */ react.createElement(_elementor_ui.Rotate, { in: isRtl }, /* @__PURE__ */ react.createElement(_elementor_icons.ChevronRightIcon, {
1178 fontSize: "small",
1179 color: "action"
1180 })));
1181 }
1182
1183 //#endregion
1184 //#region packages/packages/core/editor-audits/src/register-promotions.ts
1185 function firstFailExternalUrl(run) {
1186 return run.result.status === "fail" ? run.result.violations[0]?.externalUrl : void 0;
1187 }
1188 var PROMOTIONS = [
1189 {
1190 auditId: "audits/images-alt-text",
1191 icon: _elementor_icons.PhotoIcon,
1192 ctaLabel: (0, _wordpress_i18n.__)("Fix with Ally", "elementor"),
1193 formatSubtitle: (run) => {
1194 if (run.result.status !== "fail") return null;
1195 const count = run.result.metadata?.missingAltImageCount ?? 0;
1196 if (count === 0) return null;
1197 return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n._n)("%d image", "%d images", count, "elementor"), count);
1198 },
1199 getCtaUrl: firstFailExternalUrl
1200 },
1201 {
1202 auditId: "audits/cookie-policy",
1203 icon: _elementor_icons.ElementorCookieIcon,
1204 ctaLabel: (0, _wordpress_i18n.__)("Fix with Cookiez", "elementor"),
1205 formatSubtitle: (run) => run.result.status === "fail" ? (0, _wordpress_i18n.__)("Generate cookie policy", "elementor") : null,
1206 getCtaUrl: firstFailExternalUrl
1207 },
1208 {
1209 auditId: "audits/images-too-large",
1210 icon: _elementor_icons.ShieldHalfFilledIcon,
1211 ctaLabel: (0, _wordpress_i18n.__)("Optimize all", "elementor"),
1212 formatSubtitle: (run) => {
1213 if (run.result.status !== "fail") return null;
1214 const count = run.result.metadata?.oversizedImageCount ?? 0;
1215 if (count === 0) return null;
1216 return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n._n)("%d image", "%d images", count, "elementor"), count);
1217 },
1218 getCtaUrl: firstFailExternalUrl
1219 }
1220 ];
1221
1222 //#endregion
1223 //#region packages/packages/core/editor-audits/src/utils/find-audit-run.ts
1224 function findAuditRun(report, auditId) {
1225 return report.auditResults.find((run) => run.audit.id === auditId);
1226 }
1227
1228 //#endregion
1229 //#region packages/packages/core/editor-audits/src/components/promotion-card.tsx
1230 function PromotionCard({ ctaDisabled, ctaLabel, icon: Icon, onCtaClick, subtitle, title }) {
1231 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1232 alignItems: "center",
1233 border: 1,
1234 borderColor: "divider",
1235 borderRadius: 1,
1236 display: "flex",
1237 gap: 1,
1238 px: 2,
1239 py: 1.5
1240 } }, /* @__PURE__ */ react.createElement(Icon, {
1241 fontSize: "small",
1242 color: "action"
1243 }), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1244 display: "flex",
1245 flex: 1,
1246 flexDirection: "column",
1247 gap: .25,
1248 minWidth: 0
1249 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1250 variant: "body2",
1251 fontWeight: "bold"
1252 }, title), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1253 variant: "caption",
1254 color: "text.secondary"
1255 }, subtitle)), /* @__PURE__ */ react.createElement(_elementor_ui.Button, {
1256 variant: "outlined",
1257 color: "secondary",
1258 size: "small",
1259 disabled: ctaDisabled,
1260 onClick: onCtaClick
1261 }, ctaLabel));
1262 }
1263
1264 //#endregion
1265 //#region packages/packages/core/editor-audits/src/components/promotions.tsx
1266 function Promotions({ report }) {
1267 const cards = PROMOTIONS.flatMap((config) => {
1268 const run = findAuditRun(report, config.auditId);
1269 if (!run || run.result.status !== "fail") return [];
1270 const subtitle = config.formatSubtitle(run);
1271 if (!subtitle) return [];
1272 return [{
1273 config,
1274 ctaUrl: config.getCtaUrl(run),
1275 key: config.auditId,
1276 run,
1277 subtitle
1278 }];
1279 });
1280 if (cards.length === 0) return null;
1281 return /* @__PURE__ */ react.createElement(react.Fragment, null, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1282 variant: "subtitle1",
1283 fontWeight: "bold"
1284 }, (0, _wordpress_i18n.__)("Quick wins", "elementor")), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1285 display: "flex",
1286 flexDirection: "column",
1287 gap: 1
1288 } }, cards.map(({ config, ctaUrl, key, run, subtitle }) => /* @__PURE__ */ react.createElement(PromotionCard, {
1289 key,
1290 icon: config.icon,
1291 title: run.audit.title,
1292 subtitle,
1293 ctaLabel: config.ctaLabel,
1294 ctaDisabled: !ctaUrl,
1295 onCtaClick: () => {
1296 if (ctaUrl) window.open(ctaUrl, "_blank");
1297 }
1298 }))));
1299 }
1300
1301 //#endregion
1302 //#region packages/packages/core/editor-audits/src/components/pages/issues-page.tsx
1303 function IssuesPage({ report, onCategoryClick, onAllAuditsClick }) {
1304 const populatedCategories = getPopulatedCategories(report.categories, ALL_CATEGORIES);
1305 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1306 display: "flex",
1307 flexDirection: "column",
1308 gap: 4,
1309 p: 2
1310 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Link, {
1311 component: "button",
1312 underline: "none",
1313 color: "inherit",
1314 onClick: onAllAuditsClick,
1315 sx: { textAlign: "start" }
1316 }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1317 variant: "subtitle1",
1318 component: "h2"
1319 }, (0, _wordpress_i18n.__)("All issues", "elementor"))), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1320 display: "flex",
1321 flexDirection: "column",
1322 gap: 1
1323 } }, populatedCategories.map((category) => /* @__PURE__ */ react.createElement(IssuesCategoryRow, {
1324 key: category,
1325 category,
1326 label: CATEGORY_LABELS[category],
1327 counts: countSeverities(report, category),
1328 onClick: () => onCategoryClick(category)
1329 }))), /* @__PURE__ */ react.createElement(Promotions, { report }));
1330 }
1331
1332 //#endregion
1333 //#region packages/packages/core/editor-audits/src/utils/score-thresholds.ts
1334 var GOOD_THRESHOLD = 90;
1335 var OK_THRESHOLD = 50;
1336 function getScoreTier(score) {
1337 if (score >= 90) return {
1338 color: "success",
1339 label: (0, _wordpress_i18n.__)("Good", "elementor")
1340 };
1341 if (score >= 50) return {
1342 color: "warning",
1343 label: (0, _wordpress_i18n.__)("Needs work", "elementor")
1344 };
1345 return {
1346 color: "error",
1347 label: (0, _wordpress_i18n.__)("At risk", "elementor")
1348 };
1349 }
1350
1351 //#endregion
1352 //#region packages/packages/core/editor-audits/src/components/count-summary-circle.tsx
1353 var COUNT_SUMMARY_CIRCLE_SIZE = 64;
1354 var COUNT_SUMMARY_BORDER_WIDTH = 4;
1355 function chipBorderColor(theme, color) {
1356 if (color === "default" || !color) return theme.palette.text.secondary;
1357 return theme.palette[color].main;
1358 }
1359 function CountSummaryCircle({ ariaLabel, color, count, label, onClick }) {
1360 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1361 "aria-label": ariaLabel,
1362 component: "button",
1363 onClick,
1364 type: "button",
1365 sx: {
1366 alignItems: "center",
1367 background: "none",
1368 border: "none",
1369 cursor: "pointer",
1370 display: "flex",
1371 flex: 1,
1372 flexDirection: "column",
1373 gap: .5,
1374 padding: 0
1375 }
1376 }, /* @__PURE__ */ react.createElement(_elementor_ui.Chip, {
1377 color,
1378 label: count,
1379 size: "small",
1380 variant: "standard",
1381 sx: (theme) => ({
1382 border: `${COUNT_SUMMARY_BORDER_WIDTH}px solid ${chipBorderColor(theme, color)}`,
1383 borderRadius: "50%",
1384 height: COUNT_SUMMARY_CIRCLE_SIZE,
1385 maxWidth: COUNT_SUMMARY_CIRCLE_SIZE,
1386 width: COUNT_SUMMARY_CIRCLE_SIZE,
1387 "& .MuiChip-label": {
1388 fontSize: theme.typography.body2.fontSize,
1389 fontWeight: 700,
1390 lineHeight: 1,
1391 padding: 0
1392 }
1393 })
1394 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1395 color: "text.secondary",
1396 sx: { textAlign: "center" },
1397 variant: "caption"
1398 }, label));
1399 }
1400
1401 //#endregion
1402 //#region packages/packages/core/editor-audits/src/components/score-bar.tsx
1403 function ScoreBar({ label, score, onClick }) {
1404 const isRtl = "rtl" === (0, _elementor_ui.useTheme)().direction;
1405 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1406 role: onClick ? "button" : void 0,
1407 tabIndex: onClick ? 0 : void 0,
1408 onClick,
1409 onKeyDown: onClick ? onKeyboardClick(onClick) : void 0,
1410 sx: {
1411 display: "flex",
1412 alignItems: "center",
1413 gap: 1,
1414 cursor: onClick ? "pointer" : "default",
1415 outline: "none",
1416 "&:focus-visible": {
1417 outline: "2px solid",
1418 outlineColor: "primary.main",
1419 borderRadius: 1
1420 },
1421 py: .5
1422 }
1423 }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1424 variant: "body2",
1425 sx: { minWidth: 96 }
1426 }, label), /* @__PURE__ */ react.createElement(_elementor_ui.LinearProgress, {
1427 variant: "determinate",
1428 value: score,
1429 color: getScoreTier(score).color,
1430 sx: {
1431 flex: 1,
1432 height: 6,
1433 borderRadius: 4,
1434 bgcolor: "action.disabledBackground"
1435 }
1436 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1437 variant: "body2",
1438 color: "text.primary",
1439 sx: {
1440 minWidth: 24,
1441 textAlign: "right",
1442 fontWeight: 900
1443 }
1444 }, score), onClick && /* @__PURE__ */ react.createElement(_elementor_ui.Rotate, { in: isRtl }, /* @__PURE__ */ react.createElement(_elementor_icons.ChevronRightIcon, {
1445 fontSize: "small",
1446 color: "action"
1447 })));
1448 }
1449
1450 //#endregion
1451 //#region packages/packages/core/editor-audits/src/components/score-circle.tsx
1452 var SCORE_CIRCLE_SIZE = 88;
1453 var SCORE_CIRCLE_THICKNESS = 3;
1454 function ScoreCircle({ color, score }) {
1455 const progressColor = color ?? getScoreTier(score).color;
1456 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1457 role: "progressbar",
1458 "aria-valuenow": score,
1459 "aria-valuemin": 0,
1460 "aria-valuemax": 100,
1461 sx: {
1462 position: "relative",
1463 display: "inline-flex"
1464 }
1465 }, /* @__PURE__ */ react.createElement(_elementor_ui.CircularProgress, {
1466 variant: "determinate",
1467 value: 100,
1468 size: SCORE_CIRCLE_SIZE,
1469 thickness: SCORE_CIRCLE_THICKNESS,
1470 sx: { color: "action.disabledBackground" }
1471 }), /* @__PURE__ */ react.createElement(_elementor_ui.CircularProgress, {
1472 variant: "determinate",
1473 value: score,
1474 size: SCORE_CIRCLE_SIZE,
1475 thickness: SCORE_CIRCLE_THICKNESS,
1476 color: progressColor,
1477 sx: {
1478 position: "absolute",
1479 left: 0
1480 }
1481 }), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1482 position: "absolute",
1483 inset: 0,
1484 display: "flex",
1485 alignItems: "center",
1486 justifyContent: "center"
1487 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1488 variant: "h4",
1489 component: "span",
1490 color: "text.primary",
1491 sx: {
1492 fontWeight: 700,
1493 lineHeight: 1
1494 }
1495 }, score)));
1496 }
1497
1498 //#endregion
1499 //#region packages/packages/core/editor-audits/src/components/pages/overview-page.tsx
1500 var STATUS_ARIA_LABELS = {
1501 pass: (count) => (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%d passed audits, view all", "elementor"), count),
1502 fail: (count) => (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%d failed audits, view all", "elementor"), count),
1503 skipped: (count) => (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%d skipped audits, view all", "elementor"), count)
1504 };
1505 var STATUS_GROUPS = [
1506 "fail",
1507 "pass",
1508 "skipped"
1509 ];
1510 function OverviewPage({ onCategoryClick, onStatusClick, report }) {
1511 const populatedCategories = getPopulatedCategories(report.categories, ALL_CATEGORIES);
1512 const severityCounts = countSeverities(report);
1513 const statusCounts = auditStatusDisplayCounts(report);
1514 const overallScore = getScoreTier(report.overall);
1515 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1516 display: "flex",
1517 flexDirection: "column",
1518 gap: 4,
1519 p: 2
1520 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1521 display: "flex",
1522 alignItems: "center",
1523 gap: 2
1524 } }, /* @__PURE__ */ react.createElement(ScoreCircle, {
1525 color: overallScore.color,
1526 score: report.overall
1527 }), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1528 display: "flex",
1529 flexDirection: "column",
1530 gap: .5
1531 } }, /* @__PURE__ */ react.createElement(_elementor_ui.Chip, {
1532 label: overallScore.label,
1533 color: overallScore.color,
1534 variant: "standard",
1535 size: "small",
1536 sx: {
1537 fontWeight: 600,
1538 alignSelf: "flex-start"
1539 }
1540 }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1541 variant: "body2",
1542 color: "text.secondary"
1543 }, (0, _wordpress_i18n.__)("Overall score", "elementor")))), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1544 display: "flex",
1545 flexDirection: "column",
1546 gap: 2
1547 } }, populatedCategories.map((category) => /* @__PURE__ */ react.createElement(ScoreBar, {
1548 key: category,
1549 label: CATEGORY_LABELS[category],
1550 score: report.categories[category].score,
1551 onClick: () => onCategoryClick(category)
1552 }))), /* @__PURE__ */ react.createElement(_elementor_ui.Divider, null), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1553 variant: "subtitle1",
1554 fontWeight: "bold"
1555 }, (0, _wordpress_i18n.__)("Audit statuses", "elementor")), /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: {
1556 display: "flex",
1557 justifyContent: "space-around",
1558 gap: 2
1559 } }, STATUS_GROUPS.map((status) => /* @__PURE__ */ react.createElement(CountSummaryCircle, {
1560 key: status,
1561 ariaLabel: STATUS_ARIA_LABELS[status](statusCounts[status]),
1562 color: auditStatusColor(status),
1563 count: statusCounts[status],
1564 label: auditStatusLabel(status),
1565 onClick: () => onStatusClick(status)
1566 }))), /* @__PURE__ */ react.createElement(_elementor_ui.Divider, null), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1567 variant: "subtitle1",
1568 fontWeight: "bold"
1569 }, (0, _wordpress_i18n.__)("Remaining issues", "elementor")), /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1570 component: "ul",
1571 sx: {
1572 display: "flex",
1573 flexDirection: "column",
1574 gap: 1,
1575 listStyle: "none",
1576 m: 0,
1577 p: 0
1578 }
1579 }, ALL_SEVERITIES.map((severity) => {
1580 const count = severityCounts[severity];
1581 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, {
1582 "aria-label": severityRemainingCountLabel(severity, count),
1583 component: "li",
1584 key: severity,
1585 sx: {
1586 alignItems: "center",
1587 display: "flex",
1588 gap: .5
1589 }
1590 }, /* @__PURE__ */ react.createElement(SeverityIcon, { severity }), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1591 variant: "body2",
1592 fontWeight: "bold"
1593 }, count), /* @__PURE__ */ react.createElement(_elementor_ui.Typography, { variant: "body2" }, severityPluralLabel(severity)));
1594 })));
1595 }
1596
1597 //#endregion
1598 //#region packages/packages/core/editor-audits/src/components/report-shell.tsx
1599 function activeTab(page) {
1600 if (page === "overview") return "overview";
1601 if (page === "issues") return "issues";
1602 return page.backTo;
1603 }
1604 function isAllAuditsPage(page) {
1605 return typeof page === "object" && "page" in page && page.page === "all-audits";
1606 }
1607 function isCategoryPage(page) {
1608 return typeof page === "object" && "category" in page;
1609 }
1610 function ReportShell({ report }) {
1611 const [activePage, setActivePage] = (0, react.useState)("overview");
1612 const currentTab = activeTab(activePage);
1613 const handleTabChange = (_, value) => {
1614 setActivePage(value);
1615 };
1616 const openCategory = (category, backTo) => {
1617 setActivePage({
1618 category,
1619 backTo
1620 });
1621 };
1622 const openAllAudits = (expand, backTo = "issues") => {
1623 setActivePage({
1624 page: "all-audits",
1625 expand,
1626 backTo
1627 });
1628 };
1629 const backFromSubPage = () => {
1630 if (isAllAuditsPage(activePage) || isCategoryPage(activePage)) setActivePage(activePage.backTo);
1631 };
1632 return /* @__PURE__ */ react.createElement(_elementor_ui.Box, null, /* @__PURE__ */ react.createElement(_elementor_ui.Tabs, {
1633 "aria-label": (0, _wordpress_i18n.__)("Audit navigation", "elementor"),
1634 value: currentTab,
1635 onChange: handleTabChange,
1636 textColor: "secondary",
1637 indicatorColor: "secondary",
1638 size: "small",
1639 centered: true,
1640 variant: "fullWidth"
1641 }, /* @__PURE__ */ react.createElement(_elementor_ui.Tab, {
1642 value: "overview",
1643 label: (0, _wordpress_i18n.__)("Overview", "elementor")
1644 }), /* @__PURE__ */ react.createElement(_elementor_ui.Tab, {
1645 value: "issues",
1646 label: (0, _wordpress_i18n.__)("Issues", "elementor")
1647 })), /* @__PURE__ */ react.createElement(_elementor_ui.Divider, null), activePage === "overview" && /* @__PURE__ */ react.createElement(OverviewPage, {
1648 report,
1649 onCategoryClick: (category) => openCategory(category, "overview"),
1650 onStatusClick: (status) => openAllAudits(status, "overview")
1651 }), activePage === "issues" && /* @__PURE__ */ react.createElement(IssuesPage, {
1652 report,
1653 onCategoryClick: (category) => openCategory(category, "issues"),
1654 onAllAuditsClick: () => openAllAudits()
1655 }), isAllAuditsPage(activePage) && /* @__PURE__ */ react.createElement(AllAuditsPage, {
1656 report,
1657 initialExpandedStatus: activePage.expand,
1658 onBack: backFromSubPage
1659 }), isCategoryPage(activePage) && /* @__PURE__ */ react.createElement(CategoryPage, {
1660 category: activePage.category,
1661 report,
1662 onBack: backFromSubPage
1663 }));
1664 }
1665
1666 //#endregion
1667 //#region packages/packages/core/editor-audits/src/components/audit-panel.tsx
1668 function AuditPanel() {
1669 const { status, report, error, run } = useAuditReport();
1670 const currentDocumentId = (0, _elementor_editor_elements.getCurrentDocumentId)() ?? 0;
1671 const onRun = () => run(currentDocumentId);
1672 const lastScanLabel = report ? new Date(report.runAt).toLocaleTimeString() : null;
1673 return /* @__PURE__ */ react.createElement(react.Fragment, null, /* @__PURE__ */ react.createElement(_elementor_editor_floating_panels.FloatingPanelHeader, {
1674 panelId: "audit-panel",
1675 title: (0, _wordpress_i18n.__)("Page Audit", "elementor"),
1676 badge: (0, _wordpress_i18n.__)("Beta", "elementor"),
1677 titleVariant: "subtitle2"
1678 }), /* @__PURE__ */ react.createElement(_elementor_editor_floating_panels.FloatingPanelBody, null, status === "idle" && /* @__PURE__ */ react.createElement(WelcomePage, null), status === "loading" && /* @__PURE__ */ react.createElement(LoadingPage, null), status === "error" && /* @__PURE__ */ react.createElement(ErrorPage, {
1679 message: error ?? "",
1680 onRetry: onRun
1681 }), status === "ready" && report && /* @__PURE__ */ react.createElement(ReportShell, { report })), /* @__PURE__ */ react.createElement(_elementor_editor_floating_panels.FloatingPanelFooter, null, lastScanLabel ? /* @__PURE__ */ react.createElement(_elementor_ui.Typography, {
1682 variant: "caption",
1683 sx: { flex: 1 }
1684 }, (0, _wordpress_i18n.__)("Last scan:", "elementor"), " ", lastScanLabel) : /* @__PURE__ */ react.createElement(_elementor_ui.Box, { sx: { flex: 1 } }), lastScanLabel && /* @__PURE__ */ react.createElement(AuditFeedback, null), /* @__PURE__ */ react.createElement(_elementor_ui.Button, {
1685 variant: "contained",
1686 size: "small",
1687 onClick: onRun,
1688 disabled: status === "loading" || currentDocumentId === 0
1689 }, lastScanLabel ? (0, _wordpress_i18n.__)("Re-scan", "elementor") : (0, _wordpress_i18n.__)("Run page audit", "elementor"))));
1690 }
1691
1692 //#endregion
1693 //#region packages/packages/core/editor-audits/src/editor-panel.tsx
1694 var auditPanel = (0, _elementor_editor_floating_panels.createFloatingPanel)({
1695 id: AUDIT_PANEL_ID,
1696 title: "Audit",
1697 icon: () => null,
1698 component: AuditPanel,
1699 isDraggable: true,
1700 isResizable: true,
1701 defaults: {
1702 width: 360,
1703 height: 600,
1704 minWidth: 280,
1705 minHeight: 400
1706 }
1707 });
1708
1709 //#endregion
1710 //#region packages/packages/core/editor-audits/src/hooks/use-audit-toggle-props.ts
1711 function useAuditToggleProps() {
1712 const { isOpen } = auditPanel.useFloatingPanelStatus();
1713 const { toggle } = auditPanel.useFloatingPanelActions();
1714 return {
1715 title: (0, _wordpress_i18n.__)("Audit Page", "elementor"),
1716 icon: _elementor_icons.ShieldCheckIcon,
1717 selected: isOpen,
1718 onClick: () => toggle()
1719 };
1720 }
1721
1722 //#endregion
1723 //#region packages/packages/core/editor-audits/src/editor-app-bar.tsx
1724 var AUDIT_TOGGLE_PRIORITY = 24;
1725 function registerAppBarAuditsToggle() {
1726 _elementor_editor_app_bar.utilitiesMenu.registerToggleAction({
1727 id: "toggle-audit-panel",
1728 priority: AUDIT_TOGGLE_PRIORITY,
1729 useProps: useAuditToggleProps
1730 });
1731 }
1732
1733 //#endregion
1734 //#region packages/packages/core/editor-audits/src/audits/accessibility-policy.ts
1735 var audit$19 = {
1736 id: "audits/accessibility-policy",
1737 title: (0, _wordpress_i18n.__)("Accessibility policy", "elementor"),
1738 description: (0, _wordpress_i18n.__)("An accessibility policy demonstrates your commitment to digital inclusion and may be legally required in some regions.", "elementor"),
1739 fixHint: (0, _wordpress_i18n.__)("Install the Ally plugin to add an accessibility policy to your website.", "elementor"),
1740 categories: ["compliance"],
1741 severity: "info",
1742 weight: 1,
1743 evaluate: (ctx) => {
1744 if (ctx.pageContext.ally_plugin_active) return { status: "pass" };
1745 return {
1746 status: "fail",
1747 violations: [{
1748 auditId: audit$19.id,
1749 label: (0, _wordpress_i18n.__)("Ally plugin is not installed or active.", "elementor"),
1750 externalUrl: ctx.pageContext.ally_plugin_url
1751 }]
1752 };
1753 }
1754 };
1755
1756 //#endregion
1757 //#region packages/packages/core/editor-audits/src/audits/cookie-policy.ts
1758 var audit$18 = {
1759 id: "audits/cookie-policy",
1760 title: (0, _wordpress_i18n.__)("Cookie policy", "elementor"),
1761 description: (0, _wordpress_i18n.__)("A cookie policy is required by privacy regulations such as GDPR (EU) or CCPA (US) to inform visitors about tracking and data collection.", "elementor"),
1762 fixHint: (0, _wordpress_i18n.__)("Install the Cookiez plugin to add a cookie policy to your website.", "elementor"),
1763 categories: ["compliance"],
1764 severity: "info",
1765 weight: 1,
1766 evaluate: (ctx) => {
1767 if (ctx.pageContext.cookiez_plugin_active) return { status: "pass" };
1768 return {
1769 status: "fail",
1770 violations: [{
1771 auditId: audit$18.id,
1772 label: (0, _wordpress_i18n.__)("Cookiez plugin is not installed or active.", "elementor"),
1773 externalUrl: ctx.pageContext.cookiez_plugin_url
1774 }]
1775 };
1776 }
1777 };
1778
1779 //#endregion
1780 //#region packages/packages/core/editor-audits/src/audits/deep-nesting.ts
1781 var MAX_NESTING_DEPTH = 6;
1782 var audit$17 = {
1783 id: "audits/deep-nesting",
1784 title: (0, _wordpress_i18n.__)("Deep container nesting", "elementor"),
1785 description: (0, _wordpress_i18n.__)("Container nesting exceeds the recommended depth. Deep DOM trees hurt rendering performance and readability.", "elementor"),
1786 fixHint: (0, _wordpress_i18n.__)("Flatten the layout by removing unnecessary wrapper containers.", "elementor"),
1787 categories: ["performance", "best-practices"],
1788 severity: "warning",
1789 weight: 5,
1790 evaluate: (ctx) => {
1791 if (ctx.elements.tree.length === 0) return {
1792 status: "skipped",
1793 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
1794 };
1795 const violations = [];
1796 walkElements(ctx.elements.tree, (node, parents) => {
1797 if (node.elType !== "container") return;
1798 if (parents.length + 1 > MAX_NESTING_DEPTH) violations.push({
1799 auditId: audit$17.id,
1800 elementId: node.id,
1801 targetHint: "element-settings",
1802 label: (0, _wordpress_i18n.__)("Container nesting is too deep.", "elementor")
1803 });
1804 });
1805 return violations.length === 0 ? { status: "pass" } : {
1806 status: "fail",
1807 violations
1808 };
1809 }
1810 };
1811
1812 //#endregion
1813 //#region packages/packages/core/editor-audits/src/audits/default-design-system.ts
1814 var audit$16 = {
1815 id: "audits/default-design-system",
1816 title: (0, _wordpress_i18n.__)("Default website kit", "elementor"),
1817 description: (0, _wordpress_i18n.__)("Your website is using the default design system colors and fonts. Custom branding makes the website feel uniquely yours.", "elementor"),
1818 fixHint: (0, _wordpress_i18n.__)("Open Site Settings and customize your kit (colors, fonts, layout).", "elementor"),
1819 categories: ["best-practices"],
1820 severity: "info",
1821 weight: 1,
1822 evaluate: (ctx) => {
1823 if (!ctx.pageContext.kit_is_default_unchanged) return { status: "pass" };
1824 return {
1825 status: "fail",
1826 violations: [{
1827 auditId: audit$16.id,
1828 label: (0, _wordpress_i18n.__)("Kit appears unchanged from default.", "elementor"),
1829 targetHint: "site-settings"
1830 }]
1831 };
1832 }
1833 };
1834
1835 //#endregion
1836 //#region packages/packages/core/editor-audits/src/audits/deprecated-widgets.ts
1837 var DEPRECATION_CONTROL_NAME = "deprecation_message";
1838 var audit$15 = {
1839 id: "audits/deprecated-widgets",
1840 title: (0, _wordpress_i18n.__)("Deprecated widgets", "elementor"),
1841 description: (0, _wordpress_i18n.__)("Deprecated widgets should not be used. For better capabilities use the recommended replacement.", "elementor"),
1842 fixHint: (0, _wordpress_i18n.__)("Replace each deprecated widget with the recommended new widget shown in its panel.", "elementor"),
1843 categories: ["best-practices", "performance"],
1844 severity: "warning",
1845 weight: 7,
1846 evaluate: (ctx) => {
1847 const widgetsCache = (0, _elementor_editor_elements.getWidgetsCache)();
1848 if (!widgetsCache) return {
1849 status: "skipped",
1850 reason: "widgets-cache-unavailable"
1851 };
1852 const violations = [];
1853 walkElements(ctx.elements.tree, (node) => {
1854 if (node.elType !== "widget") return;
1855 const config = widgetsCache[node.widgetType ?? ""];
1856 if (!config?.controls) return;
1857 const deprecationControl = config.controls[DEPRECATION_CONTROL_NAME];
1858 if (!deprecationControl || typeof deprecationControl !== "object") return;
1859 violations.push({
1860 auditId: audit$15.id,
1861 elementId: node.id,
1862 targetHint: "element-settings",
1863 label: (0, _wordpress_i18n.__)("Using deprecated widget.", "elementor")
1864 });
1865 });
1866 return violations.length === 0 ? { status: "pass" } : {
1867 status: "fail",
1868 violations
1869 };
1870 }
1871 };
1872
1873 //#endregion
1874 //#region packages/packages/core/editor-audits/src/audits/heading-structure.ts
1875 var HEADING_LEVELS = [
1876 "h1",
1877 "h2",
1878 "h3",
1879 "h4",
1880 "h5",
1881 "h6"
1882 ];
1883 var HEADING_CONTROLS_REGISTRY = {
1884 heading: [{
1885 control: "header_size",
1886 requiresNonEmpty: "title"
1887 }],
1888 "e-heading": [{
1889 control: "tag",
1890 requiresNonEmpty: "title"
1891 }],
1892 "icon-box": [{
1893 control: "title_size",
1894 requiresNonEmpty: "title_text"
1895 }],
1896 "image-box": [{
1897 control: "title_size",
1898 requiresNonEmpty: "title_text"
1899 }],
1900 counter: [{
1901 control: "title_tag",
1902 requiresNonEmpty: "title"
1903 }],
1904 progress: [{
1905 control: "title_tag",
1906 when: { title_display: "yes" },
1907 requiresNonEmpty: "title"
1908 }],
1909 accordion: [{
1910 control: "title_html_tag",
1911 repeat: {
1912 items: "tabs",
1913 textField: "tab_title"
1914 }
1915 }],
1916 toggle: [{
1917 control: "title_html_tag",
1918 repeat: {
1919 items: "tabs",
1920 textField: "tab_title"
1921 }
1922 }],
1923 divider: [{
1924 control: "html_tag",
1925 when: { look: "line_text" },
1926 requiresNonEmpty: "text"
1927 }],
1928 "nested-accordion": [{
1929 control: "title_tag",
1930 repeat: {
1931 items: "items",
1932 textField: "item_title"
1933 }
1934 }],
1935 "link-in-bio": [
1936 {
1937 control: "bio_heading_tag",
1938 requiresNonEmpty: "bio_heading"
1939 },
1940 {
1941 control: "bio_title_tag",
1942 requiresNonEmpty: "bio_title"
1943 },
1944 {
1945 control: "bio_about_tag",
1946 requiresNonEmpty: "bio_about"
1947 }
1948 ],
1949 "animated-headline": [{
1950 control: "tag",
1951 requiresAnyNonEmpty: [
1952 "before_text",
1953 "highlighted_text",
1954 "rotating_text",
1955 "after_text"
1956 ]
1957 }],
1958 "author-box": [{
1959 control: "author_name_tag",
1960 requiresNonEmpty: "author_name"
1961 }],
1962 "call-to-action": [{
1963 control: "title_tag",
1964 requiresNonEmpty: "title"
1965 }, {
1966 control: "description_tag",
1967 requiresNonEmpty: "description"
1968 }],
1969 "flip-box": [
1970 {
1971 control: "title_tag",
1972 requiresNonEmpty: "title_text_a"
1973 },
1974 {
1975 control: "description_tag",
1976 requiresNonEmpty: "description_text_a"
1977 },
1978 {
1979 control: "title_tag",
1980 requiresNonEmpty: "title_text_b"
1981 },
1982 {
1983 control: "description_tag",
1984 requiresNonEmpty: "description_text_b"
1985 }
1986 ],
1987 "price-list": [{
1988 control: "title_tag",
1989 repeat: {
1990 items: "price_list",
1991 textField: "title"
1992 }
1993 }, {
1994 control: "description_tag",
1995 repeat: {
1996 items: "price_list",
1997 textField: "item_description"
1998 }
1999 }],
2000 "price-table": [{
2001 control: "heading_tag",
2002 requiresNonEmpty: "heading"
2003 }],
2004 slides: [{
2005 control: "slides_title_tag",
2006 repeat: {
2007 items: "slides",
2008 textField: "heading"
2009 }
2010 }, {
2011 control: "slides_description_tag",
2012 repeat: {
2013 items: "slides",
2014 textField: "description"
2015 }
2016 }],
2017 "table-of-contents": [{
2018 control: "html_tag",
2019 requiresNonEmpty: "title"
2020 }],
2021 "video-playlist": [
2022 {
2023 control: "playlist_title_tag",
2024 requiresNonEmpty: "playlist_title"
2025 },
2026 {
2027 control: "section_html_tag",
2028 repeat: {
2029 items: "tabs",
2030 tagSource: "item",
2031 itemWhen: { type: "section" },
2032 textField: "title"
2033 }
2034 },
2035 {
2036 control: "video_html_tag",
2037 repeat: {
2038 items: "tabs",
2039 tagSource: "item",
2040 itemWhen: { type: [
2041 "youtube",
2042 "vimeo",
2043 "hosted"
2044 ] },
2045 textField: "title"
2046 }
2047 }
2048 ],
2049 sitemap: [{
2050 control: "sitemap_title_tag",
2051 repeat: {
2052 items: "sitemap_items",
2053 textField: "sitemap_title"
2054 }
2055 }],
2056 "loop-grid": [{
2057 control: "nothing_found_message_html_tag",
2058 when: { enable_nothing_found_message: "yes" },
2059 requiresNonEmpty: "nothing_found_message_text"
2060 }],
2061 search: [{
2062 control: "nothing_found_message_html_tag",
2063 when: { enable_nothing_found_message: "yes" },
2064 requiresNonEmpty: "nothing_found_message_text"
2065 }],
2066 posts: [{
2067 control: "title_tag",
2068 when: { show_title: "yes" }
2069 }],
2070 "archive-posts": [{
2071 control: "title_tag",
2072 when: { show_title: "yes" }
2073 }],
2074 portfolio: [{
2075 control: "title_tag",
2076 when: { show_title: "yes" }
2077 }],
2078 "theme-post-title": [{ control: "header_size" }],
2079 "theme-page-title": [{ control: "header_size" }],
2080 "theme-archive-title": [{ control: "header_size" }],
2081 "theme-site-title": [{ control: "header_size" }],
2082 "woocommerce-product-title": [{ control: "header_size" }]
2083 };
2084 function isNonEmptyString(value) {
2085 return typeof value === "string" && value.trim() !== "";
2086 }
2087 function matchesWhen(settings, when) {
2088 if (!when) return true;
2089 return Object.entries(when).every(([key, expected]) => {
2090 const actual = settings[key];
2091 if (Array.isArray(expected)) return expected.includes(String(actual));
2092 return String(actual) === expected;
2093 });
2094 }
2095 function hasRequiredText(settings, key) {
2096 if (!key) return true;
2097 return isNonEmptyString(settings[key]);
2098 }
2099 function hasAnyRequiredText(settings, keys) {
2100 if (!keys?.length) return true;
2101 return keys.some((key) => isNonEmptyString(settings[key]));
2102 }
2103 function parseHeadingLevel(raw) {
2104 if (typeof raw !== "string") return null;
2105 const idx = HEADING_LEVELS.indexOf(raw.toLowerCase());
2106 return idx >= 0 ? idx + 1 : null;
2107 }
2108 function getRepeaterRows(settings, key) {
2109 const rows = settings[key];
2110 return Array.isArray(rows) ? rows : [];
2111 }
2112 function pushHeading(headings, elementId, raw) {
2113 const level = parseHeadingLevel(raw);
2114 if (level !== null) headings.push({
2115 elementId,
2116 level
2117 });
2118 }
2119 function extractHeadingsFromWidget(elementId, widgetType, settings) {
2120 const descriptors = HEADING_CONTROLS_REGISTRY[widgetType];
2121 if (!descriptors) return [];
2122 const headings = [];
2123 for (const descriptor of descriptors) {
2124 if (!matchesWhen(settings, descriptor.when)) continue;
2125 if (!hasRequiredText(settings, descriptor.requiresNonEmpty)) continue;
2126 if (!hasAnyRequiredText(settings, descriptor.requiresAnyNonEmpty)) continue;
2127 if (descriptor.repeat) {
2128 const { items, textField, itemWhen, tagSource = "widget" } = descriptor.repeat;
2129 for (const row of getRepeaterRows(settings, items)) {
2130 if (!matchesWhen(row, itemWhen)) continue;
2131 if (textField && !hasRequiredText(row, textField)) continue;
2132 pushHeading(headings, elementId, tagSource === "item" ? row[descriptor.control] : settings[descriptor.control]);
2133 }
2134 } else pushHeading(headings, elementId, settings[descriptor.control]);
2135 }
2136 return headings;
2137 }
2138 function extractPageHeadings(tree) {
2139 const headings = [];
2140 walkElements(tree, (node) => {
2141 if (node.elType !== "widget" || !node.widgetType) return;
2142 headings.push(...extractHeadingsFromWidget(node.id, node.widgetType, node.settings));
2143 });
2144 return headings;
2145 }
2146 var audit$14 = {
2147 id: "audits/heading-structure",
2148 title: (0, _wordpress_i18n.__)("Heading structure", "elementor"),
2149 description: (0, _wordpress_i18n.__)("Pages should have exactly one H1 and a non-skipping heading order.", "elementor"),
2150 fixHint: (0, _wordpress_i18n.__)("Ensure your page has one H1 and that heading levels do not skip (no H2 → H4).", "elementor"),
2151 categories: ["seo", "accessibility"],
2152 severity: "error",
2153 weight: 10,
2154 evaluate: (ctx) => {
2155 if (ctx.elements.tree.length === 0) return {
2156 status: "skipped",
2157 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2158 };
2159 const headings = extractPageHeadings(ctx.elements.tree);
2160 const violations = [];
2161 if (headings.length === 0) violations.push({
2162 auditId: audit$14.id,
2163 label: (0, _wordpress_i18n.__)("No headings found on the page.", "elementor")
2164 });
2165 else {
2166 const h1Count = headings.filter((h) => h.level === 1).length;
2167 if (h1Count === 0) violations.push({
2168 auditId: audit$14.id,
2169 label: (0, _wordpress_i18n.__)("No H1 on the page.", "elementor")
2170 });
2171 if (h1Count > 1) headings.filter((h) => h.level === 1).slice(1).forEach((h) => violations.push({
2172 auditId: audit$14.id,
2173 elementId: h.elementId,
2174 targetHint: "element-settings",
2175 label: (0, _wordpress_i18n.__)("More than one H1 on the page.", "elementor")
2176 }));
2177 for (let i = 1; i < headings.length; i++) if (headings[i].level - headings[i - 1].level > 1) violations.push({
2178 auditId: audit$14.id,
2179 elementId: headings[i].elementId,
2180 targetHint: "element-settings",
2181 label: (0, _wordpress_i18n.__)("Heading level skipped.", "elementor")
2182 });
2183 }
2184 return violations.length === 0 ? { status: "pass" } : {
2185 status: "fail",
2186 violations
2187 };
2188 }
2189 };
2190
2191 //#endregion
2192 //#region packages/packages/core/editor-audits/src/audits/hidden-elements.ts
2193 function isHiddenOnAllDevices(settings) {
2194 return Boolean(settings.hide_desktop) && Boolean(settings.hide_tablet) && Boolean(settings.hide_mobile);
2195 }
2196 var audit$13 = {
2197 id: "audits/hidden-elements",
2198 title: (0, _wordpress_i18n.__)("Hidden elements", "elementor"),
2199 description: (0, _wordpress_i18n.__)("Elements hidden on all devices simultaneously are effectively dead code in the page tree.", "elementor"),
2200 fixHint: (0, _wordpress_i18n.__)("Remove the element or make it visible on at least one device breakpoint.", "elementor"),
2201 categories: ["best-practices", "performance"],
2202 severity: "info",
2203 weight: 3,
2204 evaluate: (ctx) => {
2205 if (ctx.elements.tree.length === 0) return {
2206 status: "skipped",
2207 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2208 };
2209 const violations = [];
2210 walkElements(ctx.elements.tree, (node) => {
2211 if (isHiddenOnAllDevices(node.settings)) violations.push({
2212 auditId: audit$13.id,
2213 elementId: node.id,
2214 targetHint: "element-settings",
2215 label: (0, _wordpress_i18n.__)("Element is hidden on all devices.", "elementor")
2216 });
2217 });
2218 return violations.length === 0 ? { status: "pass" } : {
2219 status: "fail",
2220 violations
2221 };
2222 }
2223 };
2224
2225 //#endregion
2226 //#region packages/packages/core/editor-audits/src/utils/image-alt.ts
2227 function hasMeaningfulAlt(media, pageContext) {
2228 if (!media.id && !media.url) return true;
2229 if (media.id) return (pageContext.image_sizes[media.id]?.alt ?? "").trim().length > 0;
2230 return (media.alt ?? "").trim().length > 0;
2231 }
2232 function isImageSourcePresent(media) {
2233 return Boolean(media.id || media.url);
2234 }
2235
2236 //#endregion
2237 //#region packages/packages/core/editor-audits/src/audits/images-alt-text.ts
2238 var audit$12 = {
2239 id: "audits/images-alt-text",
2240 title: (0, _wordpress_i18n.__)("Images alt text", "elementor"),
2241 description: (0, _wordpress_i18n.__)("Every image needs a meaningful alt attribute for screen readers and image-search.", "elementor"),
2242 fixHint: (0, _wordpress_i18n.__)("Open the image's settings and add an Alt Text describing the image.", "elementor"),
2243 categories: ["seo", "accessibility"],
2244 severity: "error",
2245 weight: 10,
2246 evaluate: (ctx) => {
2247 if (!hasPageImages(ctx.elements.tree)) return {
2248 status: "skipped",
2249 reason: (0, _wordpress_i18n.__)("No images", "elementor")
2250 };
2251 const violations = [];
2252 const failedWidgetIds = /* @__PURE__ */ new Set();
2253 let missingAltImageCount = 0;
2254 walkImageLikeSources(ctx.elements.tree, ({ node, media }) => {
2255 if (!isImageSourcePresent(media) || hasMeaningfulAlt(media, ctx.pageContext)) return;
2256 missingAltImageCount++;
2257 if (!failedWidgetIds.has(node.id)) {
2258 failedWidgetIds.add(node.id);
2259 violations.push({
2260 auditId: audit$12.id,
2261 elementId: node.id,
2262 targetHint: "element-settings",
2263 label: (0, _wordpress_i18n.__)("Image is missing alt text.", "elementor"),
2264 externalUrl: ctx.pageContext.ally_plugin_url
2265 });
2266 }
2267 });
2268 return violations.length === 0 ? { status: "pass" } : {
2269 status: "fail",
2270 violations,
2271 metadata: { missingAltImageCount }
2272 };
2273 }
2274 };
2275
2276 //#endregion
2277 //#region packages/packages/core/editor-audits/src/audits/images-too-large.ts
2278 var SIZE_THRESHOLD_BYTES = 500 * 1024;
2279 var BYTES_PER_KB = 1024;
2280 var audit$11 = {
2281 id: "audits/images-too-large",
2282 title: (0, _wordpress_i18n.__)("Oversized images", "elementor"),
2283 description: (0, _wordpress_i18n.__)("Large image files slow down the page.", "elementor"),
2284 fixHint: (0, _wordpress_i18n.__)("Replace the image with a smaller version or enable image optimization.", "elementor"),
2285 categories: ["performance"],
2286 severity: "warning",
2287 weight: 7,
2288 evaluate: (ctx) => {
2289 if (!hasPageImages(ctx.elements.tree)) return {
2290 status: "skipped",
2291 reason: (0, _wordpress_i18n.__)("No images", "elementor")
2292 };
2293 const widgetMaxKb = /* @__PURE__ */ new Map();
2294 let oversizedImageCount = 0;
2295 walkImageLikeSources(ctx.elements.tree, ({ node, media }) => {
2296 const id = media.id;
2297 if (!id) return;
2298 const size = ctx.pageContext.image_sizes[id];
2299 if (!size || size.filesize_bytes <= SIZE_THRESHOLD_BYTES) return;
2300 oversizedImageCount++;
2301 const kb = Math.round(size.filesize_bytes / BYTES_PER_KB);
2302 const currentMax = widgetMaxKb.get(node.id) ?? 0;
2303 widgetMaxKb.set(node.id, Math.max(currentMax, kb));
2304 });
2305 const violations = Array.from(widgetMaxKb.entries()).map(([elementId, kb]) => ({
2306 auditId: audit$11.id,
2307 elementId,
2308 targetHint: "element-settings",
2309 label: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("Image is %d KB (over 500 KB).", "elementor"), kb),
2310 externalUrl: ctx.pageContext.image_optimization_plugin_url
2311 }));
2312 return violations.length === 0 ? { status: "pass" } : {
2313 status: "fail",
2314 violations,
2315 metadata: { oversizedImageCount }
2316 };
2317 }
2318 };
2319
2320 //#endregion
2321 //#region packages/packages/core/editor-audits/src/audits/nested-boxed-containers.ts
2322 function isBoxed(settings) {
2323 return settings.content_width === "boxed";
2324 }
2325 var audit$10 = {
2326 id: "audits/nested-boxed-containers",
2327 title: (0, _wordpress_i18n.__)("Boxed container nested inside a boxed parent", "elementor"),
2328 description: (0, _wordpress_i18n.__)("An inner container does not need to be boxed when its parent already is.", "elementor"),
2329 fixHint: (0, _wordpress_i18n.__)("Change the inner container's content width to Full Width.", "elementor"),
2330 categories: ["best-practices", "performance"],
2331 severity: "info",
2332 weight: 5,
2333 evaluate: (ctx) => {
2334 if (ctx.elements.tree.length === 0) return {
2335 status: "skipped",
2336 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2337 };
2338 const violations = [];
2339 walkElements(ctx.elements.tree, (node, parents) => {
2340 if (node.elType !== "container" || !isBoxed(node.settings)) return;
2341 const nearestContainerAncestor = [...parents].reverse().find((p) => p.elType === "container");
2342 if (nearestContainerAncestor && isBoxed(nearestContainerAncestor.settings)) violations.push({
2343 auditId: audit$10.id,
2344 elementId: node.id,
2345 targetHint: "element-settings",
2346 label: (0, _wordpress_i18n.__)("Nested boxed container.", "elementor")
2347 });
2348 });
2349 return violations.length === 0 ? { status: "pass" } : {
2350 status: "fail",
2351 violations
2352 };
2353 }
2354 };
2355
2356 //#endregion
2357 //#region packages/packages/core/editor-audits/src/audits/page-excerpt.ts
2358 var audit$9 = {
2359 id: "audits/page-excerpt",
2360 title: (0, _wordpress_i18n.__)("Page excerpt", "elementor"),
2361 description: (0, _wordpress_i18n.__)("A descriptive excerpt helps search engines and previews summarize the page.", "elementor"),
2362 fixHint: (0, _wordpress_i18n.__)("Open Page Settings and write a short excerpt.", "elementor"),
2363 categories: ["seo"],
2364 severity: "warning",
2365 weight: 5,
2366 evaluate: (ctx) => {
2367 if (ctx.pageContext.post_excerpt) return { status: "pass" };
2368 return {
2369 status: "fail",
2370 violations: [{
2371 auditId: audit$9.id,
2372 label: (0, _wordpress_i18n.__)("Page has no excerpt.", "elementor"),
2373 targetHint: "page-settings",
2374 angieFix: true
2375 }]
2376 };
2377 }
2378 };
2379
2380 //#endregion
2381 //#region packages/packages/core/editor-audits/src/audits/page-featured-image.ts
2382 var audit$8 = {
2383 id: "audits/page-featured-image",
2384 title: (0, _wordpress_i18n.__)("Page featured image", "elementor"),
2385 description: (0, _wordpress_i18n.__)("Featured images are used by social shares and many themes for hero visuals.", "elementor"),
2386 fixHint: (0, _wordpress_i18n.__)("Open Page Settings and set a featured image.", "elementor"),
2387 categories: ["seo"],
2388 severity: "info",
2389 weight: 5,
2390 evaluate: (ctx) => {
2391 if (ctx.pageContext.featured_image_id) return { status: "pass" };
2392 return {
2393 status: "fail",
2394 violations: [{
2395 auditId: audit$8.id,
2396 label: (0, _wordpress_i18n.__)("No featured image set.", "elementor"),
2397 targetHint: "page-settings"
2398 }]
2399 };
2400 }
2401 };
2402
2403 //#endregion
2404 //#region packages/packages/core/editor-audits/src/audits/page-title.ts
2405 var MAX_TITLE_LENGTH = 60;
2406 var audit$7 = {
2407 id: "audits/page-title",
2408 title: (0, _wordpress_i18n.__)("Page title", "elementor"),
2409 description: (0, _wordpress_i18n.__)("Pages need a clear title for SEO and screen-reader navigation.", "elementor"),
2410 fixHint: (0, _wordpress_i18n.__)("Open Page Settings and add a title.", "elementor"),
2411 categories: ["seo"],
2412 severity: "error",
2413 weight: 10,
2414 evaluate: (ctx) => {
2415 const title = ctx.pageContext.post_title;
2416 if (!title) return {
2417 status: "fail",
2418 violations: [{
2419 auditId: audit$7.id,
2420 label: (0, _wordpress_i18n.__)("Page has no title.", "elementor"),
2421 targetHint: "page-settings",
2422 angieFix: true
2423 }]
2424 };
2425 if (title.length > MAX_TITLE_LENGTH) return {
2426 status: "fail",
2427 violations: [{
2428 auditId: audit$7.id,
2429 label: (0, _wordpress_i18n.__)("Page title is too long.", "elementor"),
2430 targetHint: "page-settings",
2431 angieFix: true
2432 }]
2433 };
2434 return { status: "pass" };
2435 }
2436 };
2437
2438 //#endregion
2439 //#region packages/packages/core/editor-audits/src/utils/collect-hardcoded-colors.ts
2440 var RESERVED_SETTING_KEYS$1 = /* @__PURE__ */ new Set(["__globals__", "__dynamic__"]);
2441 var REPEATER_ROW_ID_KEY$1 = "_id";
2442 var HEX_COLOR_PATTERN = /^#[0-9a-f]{3,8}$/i;
2443 var RGB_COLOR_PATTERN = /^rgba?\(/i;
2444 function isGlobalColorReference(value) {
2445 return value.startsWith("globals/");
2446 }
2447 function isHardcodedColorCandidate(value) {
2448 const trimmed = value.trim();
2449 return HEX_COLOR_PATTERN.test(trimmed) || RGB_COLOR_PATTERN.test(trimmed);
2450 }
2451 function formatPath$1(prefix, key) {
2452 return prefix ? `${prefix}.${key}` : key;
2453 }
2454 __name(formatPath$1, "formatPath");
2455 function formatArrayPath$1(prefix, index) {
2456 return `${prefix}[${index}]`;
2457 }
2458 __name(formatArrayPath$1, "formatArrayPath");
2459 function collectHardcodedColors(settings, pathPrefix = "") {
2460 const linkedGlobals = settings.__globals__ ?? {};
2461 const results = [];
2462 for (const [key, value] of Object.entries(settings)) {
2463 if (RESERVED_SETTING_KEYS$1.has(key) || key === REPEATER_ROW_ID_KEY$1) continue;
2464 if (linkedGlobals[key]) continue;
2465 if (typeof value === "string") {
2466 if (!isGlobalColorReference(value) && isHardcodedColorCandidate(value)) results.push({
2467 path: formatPath$1(pathPrefix, key),
2468 value
2469 });
2470 continue;
2471 }
2472 if (Array.isArray(value)) {
2473 const arrayPath = formatPath$1(pathPrefix, key);
2474 value.forEach((item, index) => {
2475 if (item && typeof item === "object" && !Array.isArray(item)) results.push(...collectHardcodedColors(item, formatArrayPath$1(arrayPath, index)));
2476 });
2477 continue;
2478 }
2479 if (value && typeof value === "object") results.push(...collectHardcodedColors(value, formatPath$1(pathPrefix, key)));
2480 }
2481 return results;
2482 }
2483
2484 //#endregion
2485 //#region packages/packages/core/editor-audits/src/utils/match-global-by-value.ts
2486 function findMatchingGlobalByValue(value, globals) {
2487 const trimmed = value.trim();
2488 return globals.find((global) => global.value === trimmed) ?? null;
2489 }
2490
2491 //#endregion
2492 //#region packages/packages/core/editor-audits/src/audits/prefer-global-colors.ts
2493 var audit$6 = {
2494 id: "audits/prefer-global-colors",
2495 title: (0, _wordpress_i18n.__)("Prefer global colors", "elementor"),
2496 description: (0, _wordpress_i18n.__)("Global colors make the design consistent and easy to update site-wide.", "elementor"),
2497 fixHint: (0, _wordpress_i18n.__)("Replace the hard-coded value with the matching global color.", "elementor"),
2498 categories: ["best-practices"],
2499 severity: "info",
2500 weight: 3,
2501 evaluate: (ctx) => {
2502 if (ctx.elements.tree.length === 0) return {
2503 status: "skipped",
2504 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2505 };
2506 if (ctx.kit.globals.colors.length === 0) return {
2507 status: "skipped",
2508 reason: (0, _wordpress_i18n.__)("No global colors", "elementor")
2509 };
2510 const violations = [];
2511 walkElements(ctx.elements.tree, (node) => {
2512 for (const { value } of collectHardcodedColors(node.settings)) {
2513 const global = findMatchingGlobalByValue(value, ctx.kit.globals.colors);
2514 if (!global) continue;
2515 violations.push({
2516 auditId: audit$6.id,
2517 elementId: node.id,
2518 targetHint: "element-settings",
2519 angieFix: true,
2520 label: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("replace \"%1$s\" with \"%2$s\" global color", "elementor"), value, global.title)
2521 });
2522 }
2523 });
2524 return violations.length === 0 ? { status: "pass" } : {
2525 status: "fail",
2526 violations
2527 };
2528 }
2529 };
2530
2531 //#endregion
2532 //#region packages/packages/core/editor-audits/src/utils/collect-hardcoded-fonts.ts
2533 var RESERVED_SETTING_KEYS = /* @__PURE__ */ new Set(["__globals__", "__dynamic__"]);
2534 var REPEATER_ROW_ID_KEY = "_id";
2535 var FONT_FAMILY_KEY_PATTERN = /font_family/;
2536 function isGlobalFontReference(value) {
2537 return value.startsWith("globals/");
2538 }
2539 function isHardcodedFontCandidate(key, value) {
2540 return FONT_FAMILY_KEY_PATTERN.test(key) && value.trim().length > 0;
2541 }
2542 function formatPath(prefix, key) {
2543 return prefix ? `${prefix}.${key}` : key;
2544 }
2545 function formatArrayPath(prefix, index) {
2546 return `${prefix}[${index}]`;
2547 }
2548 function collectHardcodedFonts(settings, pathPrefix = "") {
2549 const linkedGlobals = settings.__globals__ ?? {};
2550 const results = [];
2551 for (const [key, value] of Object.entries(settings)) {
2552 if (RESERVED_SETTING_KEYS.has(key) || key === REPEATER_ROW_ID_KEY) continue;
2553 if (linkedGlobals[key]) continue;
2554 if (typeof value === "string") {
2555 if (!isGlobalFontReference(value) && isHardcodedFontCandidate(key, value)) results.push({
2556 path: formatPath(pathPrefix, key),
2557 value
2558 });
2559 continue;
2560 }
2561 if (Array.isArray(value)) {
2562 const arrayPath = formatPath(pathPrefix, key);
2563 value.forEach((item, index) => {
2564 if (item && typeof item === "object" && !Array.isArray(item)) results.push(...collectHardcodedFonts(item, formatArrayPath(arrayPath, index)));
2565 });
2566 continue;
2567 }
2568 if (value && typeof value === "object") results.push(...collectHardcodedFonts(value, formatPath(pathPrefix, key)));
2569 }
2570 return results;
2571 }
2572
2573 //#endregion
2574 //#region packages/packages/core/editor-audits/src/audits/prefer-global-fonts.ts
2575 var audit$5 = {
2576 id: "audits/prefer-global-fonts",
2577 title: (0, _wordpress_i18n.__)("Prefer global fonts", "elementor"),
2578 description: (0, _wordpress_i18n.__)("Global fonts make the design consistent and easy to update site-wide.", "elementor"),
2579 fixHint: (0, _wordpress_i18n.__)("Replace the hard-coded value with the matching global font.", "elementor"),
2580 categories: ["best-practices", "performance"],
2581 severity: "info",
2582 weight: 3,
2583 evaluate: (ctx) => {
2584 if (ctx.elements.tree.length === 0) return {
2585 status: "skipped",
2586 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2587 };
2588 if (ctx.kit.globals.fonts.length === 0) return {
2589 status: "skipped",
2590 reason: (0, _wordpress_i18n.__)("No global fonts", "elementor")
2591 };
2592 const violations = [];
2593 walkElements(ctx.elements.tree, (node) => {
2594 for (const { value } of collectHardcodedFonts(node.settings)) {
2595 const global = findMatchingGlobalByValue(value, ctx.kit.globals.fonts);
2596 if (!global) continue;
2597 violations.push({
2598 auditId: audit$5.id,
2599 elementId: node.id,
2600 targetHint: "element-settings",
2601 label: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("replace hardcoded typography with \"%s\" global font", "elementor"), global.title)
2602 });
2603 }
2604 });
2605 return violations.length === 0 ? { status: "pass" } : {
2606 status: "fail",
2607 violations
2608 };
2609 }
2610 };
2611
2612 //#endregion
2613 //#region packages/packages/core/editor-audits/src/audits/privacy-policy.ts
2614 var audit$4 = {
2615 id: "audits/privacy-policy",
2616 title: (0, _wordpress_i18n.__)("Privacy policy", "elementor"),
2617 description: (0, _wordpress_i18n.__)("A privacy policy page is required by privacy regulations such as GDPR (EU) and CCPA (US).", "elementor"),
2618 fixHint: (0, _wordpress_i18n.__)("Go to Settings > Privacy in the WordPress admin and assign a published privacy policy page.", "elementor"),
2619 categories: ["compliance"],
2620 severity: "info",
2621 weight: 1,
2622 evaluate: (ctx) => {
2623 if (ctx.pageContext.privacy_policy_url) return { status: "pass" };
2624 return {
2625 status: "fail",
2626 violations: [{
2627 auditId: audit$4.id,
2628 label: (0, _wordpress_i18n.__)("No privacy policy page is set.", "elementor"),
2629 externalUrl: ctx.pageContext.privacy_settings_url
2630 }]
2631 };
2632 }
2633 };
2634
2635 //#endregion
2636 //#region packages/packages/core/editor-audits/src/audits/robots-noindex.ts
2637 var audit$3 = {
2638 id: "audits/robots-noindex",
2639 title: (0, _wordpress_i18n.__)("Search engine visibility", "elementor"),
2640 description: (0, _wordpress_i18n.__)("When search engine visibility is checked in WordPress Settings -> Reading -> Search Engine Visibility, search engines are discouraged from indexing the website.", "elementor"),
2641 fixHint: (0, _wordpress_i18n.__)("Go to Settings → Reading in the WordPress admin and uncheck \"Discourage search engines from indexing this site\" when the website should be indexed.", "elementor"),
2642 categories: ["seo"],
2643 severity: "error",
2644 weight: 7,
2645 evaluate: (ctx) => {
2646 if (!ctx.pageContext.is_noindex) return { status: "pass" };
2647 return {
2648 status: "fail",
2649 violations: [{
2650 auditId: audit$3.id,
2651 label: (0, _wordpress_i18n.__)("Search engines are discouraged from indexing this website.", "elementor"),
2652 externalUrl: ctx.pageContext.reading_settings_url
2653 }]
2654 };
2655 }
2656 };
2657
2658 //#endregion
2659 //#region packages/packages/core/editor-audits/src/audits/sections-and-columns.ts
2660 var audit$2 = {
2661 id: "audits/sections-and-columns",
2662 title: (0, _wordpress_i18n.__)("Sections and columns", "elementor"),
2663 description: (0, _wordpress_i18n.__)("Sections and columns are legacy elements. Containers render fewer DOM nodes and are more flexible.", "elementor"),
2664 fixHint: (0, _wordpress_i18n.__)("Use the Container Converter to replace each section/column with a container.", "elementor"),
2665 categories: ["best-practices", "performance"],
2666 severity: "warning",
2667 weight: 7,
2668 evaluate: (ctx) => {
2669 if (ctx.elements.tree.length === 0) return {
2670 status: "skipped",
2671 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2672 };
2673 const violations = [];
2674 walkElements(ctx.elements.tree, (node) => {
2675 if (node.elType === "section" || node.elType === "column") violations.push({
2676 auditId: audit$2.id,
2677 elementId: node.id,
2678 label: node.elType === "section" ? (0, _wordpress_i18n.__)("Section element", "elementor") : (0, _wordpress_i18n.__)("Column element", "elementor")
2679 });
2680 });
2681 return violations.length === 0 ? { status: "pass" } : {
2682 status: "fail",
2683 violations
2684 };
2685 }
2686 };
2687
2688 //#endregion
2689 //#region packages/packages/core/editor-audits/src/audits/site-identity.ts
2690 var audit$1 = {
2691 id: "audits/site-identity",
2692 title: (0, _wordpress_i18n.__)("Site identity", "elementor"),
2693 description: (0, _wordpress_i18n.__)("Site name, description, logo, and favicon establish your brand and appear in search results and browser tabs.", "elementor"),
2694 fixHint: (0, _wordpress_i18n.__)("Open Site Settings → Site Identity and complete all the missing fields.", "elementor"),
2695 categories: ["best-practices", "seo"],
2696 severity: "info",
2697 weight: 7,
2698 evaluate: (ctx) => {
2699 const { site_identity: identity } = ctx.pageContext;
2700 const violations = [];
2701 if (!identity.site_name_set) violations.push({
2702 auditId: audit$1.id,
2703 label: (0, _wordpress_i18n.__)("Site name is missing or still uses the default.", "elementor"),
2704 targetHint: "site-identity-settings"
2705 });
2706 if (!identity.site_description_set) violations.push({
2707 auditId: audit$1.id,
2708 label: (0, _wordpress_i18n.__)("Site description is missing or still uses the default.", "elementor"),
2709 targetHint: "site-identity-settings",
2710 angieFix: true
2711 });
2712 if (!identity.site_logo_set) violations.push({
2713 auditId: audit$1.id,
2714 label: (0, _wordpress_i18n.__)("Site logo is not set.", "elementor"),
2715 targetHint: "site-identity-settings"
2716 });
2717 if (!identity.site_favicon_set) violations.push({
2718 auditId: audit$1.id,
2719 label: (0, _wordpress_i18n.__)("Site favicon is not set.", "elementor"),
2720 targetHint: "site-identity-settings"
2721 });
2722 if (violations.length === 0) return { status: "pass" };
2723 return {
2724 status: "fail",
2725 violations
2726 };
2727 }
2728 };
2729
2730 //#endregion
2731 //#region packages/packages/core/editor-audits/src/audits/too-many-widgets.ts
2732 var WIDGET_COUNT_THRESHOLD = 100;
2733 var audit = {
2734 id: "audits/too-many-widgets",
2735 title: (0, _wordpress_i18n.__)("Too many widgets", "elementor"),
2736 description: (0, _wordpress_i18n.__)("Excessive DOM size caused by too many widgets degrades rendering performance.", "elementor"),
2737 fixHint: (0, _wordpress_i18n.__)("Reduce the number of widgets on the page by removing or combining elements.", "elementor"),
2738 categories: ["best-practices", "performance"],
2739 severity: "warning",
2740 weight: 5,
2741 evaluate: (ctx) => {
2742 if (ctx.elements.tree.length === 0) return {
2743 status: "skipped",
2744 reason: (0, _wordpress_i18n.__)("No elements", "elementor")
2745 };
2746 let widgetCount = 0;
2747 walkElements(ctx.elements.tree, (node) => {
2748 if (node.elType === "widget") widgetCount++;
2749 });
2750 if (widgetCount <= WIDGET_COUNT_THRESHOLD) return { status: "pass" };
2751 return {
2752 status: "fail",
2753 violations: [{
2754 auditId: audit.id,
2755 label: (0, _wordpress_i18n.__)("Page has too many widgets.", "elementor")
2756 }]
2757 };
2758 }
2759 };
2760
2761 //#endregion
2762 //#region packages/packages/core/editor-audits/src/register-audits.ts
2763 var AUDITS = [
2764 audit$7,
2765 audit$9,
2766 audit$8,
2767 audit$13,
2768 audit$15,
2769 audit,
2770 audit$2,
2771 audit$17,
2772 audit$10,
2773 audit$16,
2774 audit$1,
2775 audit$6,
2776 audit$5,
2777 audit$14,
2778 audit$12,
2779 audit$11,
2780 audit$3,
2781 audit$4,
2782 audit$19,
2783 audit$18
2784 ];
2785 function registerAllAudits() {
2786 for (const audit of AUDITS) registerAudit(audit);
2787 }
2788
2789 //#endregion
2790 //#region packages/packages/core/editor-audits/src/init.ts
2791 function init() {
2792 (0, _elementor_store.__registerSlice)(slice);
2793 registerAllAudits();
2794 (0, _elementor_editor_floating_panels.registerFloatingPanel)(auditPanel.panel);
2795 registerAppBarAuditsToggle();
2796 }
2797
2798 //#endregion
2799 //#region packages/packages/core/editor-audits/src/index.ts
2800 var src_exports = /* @__PURE__ */ __exportAll({
2801 init: () => init,
2802 registerAudit: () => registerAudit,
2803 runPageAudit: () => runPageAudit
2804 });
2805
2806 //#endregion
2807 //#region \0elementor-package-library-entry
2808 (window.elementorV2 = window.elementorV2 || {}).editorAudits = src_exports;
2809
2810 //#endregion
2811 })(elementorV2.editorFloatingPanels, elementorV2.store, elementorV2.editorAppBar, elementorV2.icons, wp.i18n, React, elementorV2.editorElements, elementorV2.ui, elementorV2.httpClient, elementorV2.session, elementorV2.editorNotifications, elementorV2.editorV1Adapters, elementorV2.events, elementorV2.editorMcp);
2812 window.elementorV2.editorAudits?.init?.();
2813 //# sourceMappingURL=editor-audits.js.map