PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / gutenberg / blocks / reading-time / src / aiActionsPreview.js

aiActionsPreview.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/gutenberg/blocks/reading-time/src/aiActionsPreview.js

167 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * The AI Actions split button, drawn in the editor canvas.
3 *
4 * Same markup and classes as views/templates/parts/ai-actions.php, so the
5 * frontend stylesheet dresses it without a line of editor-only CSS, and so the
6 * block's style controls — which select on `.betterdocs-ai-actions-primary` and
7 * friends — hit the preview exactly as they hit the real thing.
8 *
9 * It is deliberately inert. The frontend delegates clicks from `body` in
10 * public/betterdocs.js, which never runs in the editor, so the disclosure is
11 * driven by React state here instead. Nothing navigates and nothing touches the
12 * clipboard: the point is to show the author what the toggles do, not to be the
13 * button.
14 */
15
16 import { __ } from "@wordpress/i18n";
17 import { Fragment, useEffect, useRef, useState } from "@wordpress/element";
18 import { caretMarkup, enabledActions } from "./aiActions";
19 import { copyIcon, caretIcon } from "./aiIcons";
20
21 /**
22 * Registry icons arrive as SVG markup rather than components, because they come
23 * from Core\AIActions::icon() — one source of truth for both surfaces, including
24 * whatever a third-party registry entry supplies.
25 */
26 const Icon = ({ markup, className = "" }) => (
27 <span
28 className={`betterdocs-ai-actions-icon${className ? ` ${className}` : ""}`}
29 aria-hidden="true"
30 dangerouslySetInnerHTML={{ __html: markup }}
31 />
32 );
33
34 /**
35 * The static button, for when the localized registry is missing — a screen that
36 * loaded the editor bundle without going through our enqueue, or a test. Better
37 * an unopenable button than an empty block.
38 */
39 const Placeholder = ({ label }) => (
40 <div className="betterdocs-ai-actions betterdocs-ai-actions-placeholder">
41 <span className="betterdocs-ai-actions-primary">
42 <span className="betterdocs-ai-actions-icon">{copyIcon()}</span>
43 <span className="betterdocs-ai-actions-label">{label}</span>
44 </span>
45 <span className="betterdocs-ai-actions-toggle">
46 <span className="betterdocs-ai-actions-caret">{caretIcon()}</span>
47 </span>
48 </div>
49 );
50
51 const AIActionsPreview = ({ attributes }) => {
52 const { aiButtonLabel } = attributes;
53 const [isOpen, setIsOpen] = useState(false);
54 const wrapper = useRef(null);
55
56 const actions = enabledActions(attributes);
57 const primary = actions.find((action) => action.primary);
58 const label = aiButtonLabel || (primary ? primary.label : __("AI Actions", "betterdocs"));
59
60 // The canvas is an iframe in WP 6.3+, so these have to be bound to the
61 // element's *own* document — `document` here is the admin frame's, which never
62 // sees a click inside the canvas.
63 useEffect(() => {
64 if (!isOpen || !wrapper.current) {
65 return;
66 }
67
68 const doc = wrapper.current.ownerDocument;
69 const onDocumentClick = (event) => {
70 if (wrapper.current && !wrapper.current.contains(event.target)) {
71 setIsOpen(false);
72 }
73 };
74 const onKeyDown = (event) => {
75 if (event.key === "Escape" || event.key === "Esc") {
76 setIsOpen(false);
77 }
78 };
79
80 doc.addEventListener("click", onDocumentClick);
81 doc.addEventListener("keydown", onKeyDown);
82
83 return () => {
84 doc.removeEventListener("click", onDocumentClick);
85 doc.removeEventListener("keydown", onKeyDown);
86 };
87 }, [isOpen]);
88
89 if (!actions.length) {
90 return <Placeholder label={label} />;
91 }
92
93 const caret = caretMarkup();
94
95 return (
96 <div
97 className={`betterdocs-ai-actions${primary ? "" : " betterdocs-ai-actions-no-primary"}`}
98 ref={wrapper}
99 >
100 {primary && (
101 <button
102 type="button"
103 className="betterdocs-ai-actions-primary"
104 onClick={(event) => event.preventDefault()}
105 >
106 <Icon markup={primary.icon} className="betterdocs-ai-actions-icon-idle" />
107 <span className="betterdocs-ai-actions-label">{label}</span>
108 </button>
109 )}
110
111 <button
112 type="button"
113 className="betterdocs-ai-actions-toggle"
114 aria-expanded={isOpen}
115 aria-label={__("AI actions", "betterdocs")}
116 // Not stopPropagation: the click also has to reach Gutenberg, or
117 // opening the menu on an unselected block would fail to select it.
118 // The outside-click listener already ignores clicks inside the
119 // wrapper, so nothing here closes what it just opened.
120 onClick={(event) => {
121 event.preventDefault();
122 setIsOpen(!isOpen);
123 }}
124 >
125 {caret ? (
126 <Icon markup={caret} />
127 ) : (
128 <span className="betterdocs-ai-actions-caret">{caretIcon()}</span>
129 )}
130 </button>
131
132 <ul className="betterdocs-ai-actions-menu" hidden={!isOpen}>
133 {actions.map((action, index) => (
134 <Fragment key={action.id}>
135 {/* Same rule the frontend view draws: wherever the group
136 changes between two consecutive rows. */}
137 {index > 0 && action.group !== actions[index - 1].group && (
138 <li className="betterdocs-ai-actions-separator" role="presentation" />
139 )}
140 <li className="betterdocs-ai-actions-row">
141 <button
142 type="button"
143 className="betterdocs-ai-actions-item"
144 onClick={(event) => event.preventDefault()}
145 >
146 <Icon markup={action.icon} />
147 <span className="betterdocs-ai-actions-text">
148 <span className="betterdocs-ai-actions-title">
149 {action.label}
150 </span>
151 {action.description && (
152 <span className="betterdocs-ai-actions-desc">
153 {action.description}
154 </span>
155 )}
156 </span>
157 </button>
158 </li>
159 </Fragment>
160 ))}
161 </ul>
162 </div>
163 );
164 };
165
166 export default AIActionsPreview;
167