PluginProbe
Slider Block by Sliderberg – WordPress Slider & Carousel Plugin for Gutenberg / 1.2.3
Slider Block by Sliderberg – WordPress Slider & Carousel Plugin for Gutenberg v1.2.3
1.2.3 1.2.2 1.2.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.7 1.0.8 1.0.9
sliderberg / src / blocks / slide / edit.tsx

edit.tsx in Slider Block by Sliderberg – WordPress Slider & Carousel Plugin for Gutenberg 1.2.3, at src/blocks/slide/edit.tsx

301 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Slide block editor component.
3 * Renders a slide with background, overlay, and inner blocks for content.
4 */
5
6 import type { CSSProperties } from "react";
7 import {
8 BlockControls,
9 store as blockEditorStore,
10 useBlockProps,
11 useInnerBlocksProps,
12 } from "@wordpress/block-editor";
13 import {
14 AlignmentMatrixControl,
15 Dropdown,
16 ToolbarButton,
17 ToolbarGroup,
18 } from "@wordpress/components";
19 import { useSelect } from "@wordpress/data";
20 import { __ } from "@wordpress/i18n";
21
22 import SlideInspector from "./inspector";
23 import SlidePlaceholder from "./components/SlidePlaceholder";
24 import type { ContentPosition, SlideAttributes } from "./constants";
25
26 import "./editor.scss";
27
28 interface EditProps {
29 attributes: SlideAttributes;
30 setAttributes: (attrs: Partial<SlideAttributes>) => void;
31 clientId: string;
32 context: Record<string, any>;
33 }
34
35 // AlignmentMatrixControl uses space-separated values ("top left"); our
36 // contentPosition attribute uses hyphens ("top-left") to match the CSS
37 // class / flex-alignment mapping used elsewhere.
38 const toMatrixValue = (position: string) => position.replace("-", " ");
39 const toContentPosition = (value: string) =>
40 value.replace(" ", "-") as ContentPosition;
41
42 export default function Edit({
43 attributes,
44 setAttributes,
45 clientId,
46 context,
47 }: EditProps) {
48 const {
49 backgroundImage,
50 backgroundColor,
51 backgroundGradient,
52 focalPoint,
53 overlayColor,
54 overlayOpacity,
55 contentPosition,
56 isFixed,
57 minHeight: slideMinHeight,
58 border,
59 slideBorderRadius,
60 } = attributes;
61
62 const matrixValue = toMatrixValue(contentPosition || "center-center");
63 const contentPositionControl = (
64 <BlockControls group="block">
65 <ToolbarGroup>
66 <Dropdown
67 popoverProps={{ placement: "bottom-start" }}
68 renderToggle={({ isOpen, onToggle }) => (
69 <ToolbarButton
70 onClick={onToggle}
71 aria-expanded={isOpen}
72 label={__("Change content position", "sliderberg")}
73 icon={<AlignmentMatrixControl.Icon value={matrixValue as any} />}
74 />
75 )}
76 renderContent={() => (
77 <AlignmentMatrixControl
78 label={__("Content Position", "sliderberg")}
79 value={matrixValue as any}
80 onChange={(value) =>
81 setAttributes({
82 contentPosition: toContentPosition(value as string),
83 })
84 }
85 />
86 )}
87 />
88 </ToolbarGroup>
89 </BlockControls>
90 );
91
92 const parentMinHeight = context["sliderberg/minHeight"] || 400;
93 // The slider minimum height is the floor for every slide. A slide-specific
94 // value may make that slide taller, but must not leave it shorter than the
95 // Swiper container and expose empty space below it.
96 const effectiveMinHeight =
97 slideMinHeight !== 400
98 ? Math.max(slideMinHeight, parentMinHeight)
99 : parentMinHeight;
100
101 // In Carousel mode several empty slides can be visible in the canvas at
102 // once — showing each one's full upload/media-library/color placeholder
103 // simultaneously is cluttered (matches the Columns block: only the active
104 // column shows its inserter). Show the full placeholder only for the
105 // slide that's active, or — if nothing in this slider/carousel is active
106 // yet — the first slide, so there's always exactly one full placeholder
107 // rather than none.
108 const hasBackground = !!(
109 backgroundImage?.url ||
110 backgroundGradient ||
111 backgroundColor
112 );
113
114 const showFullPlaceholder = useSelect(
115 (select) => {
116 // Only the placeholder (no background yet) cares about this — skip
117 // the selection lookups entirely once a background is set, since the
118 // result is never used in that case.
119 if (hasBackground) {
120 return false;
121 }
122
123 const { getBlockRootClientId, getBlockOrder, hasSelectedInnerBlock, isBlockSelected } =
124 select(blockEditorStore) as any;
125
126 const isThisSlideActive =
127 isBlockSelected(clientId) || hasSelectedInnerBlock(clientId, true);
128 if (isThisSlideActive) {
129 return true;
130 }
131
132 const parentClientId = getBlockRootClientId(clientId);
133 if (!parentClientId) {
134 return true;
135 }
136
137 const siblingOrder: string[] = getBlockOrder(parentClientId);
138 if (siblingOrder[0] !== clientId) {
139 return false;
140 }
141
142 const anySiblingActive = siblingOrder.some(
143 (siblingId) =>
144 siblingId !== clientId &&
145 (isBlockSelected(siblingId) || hasSelectedInnerBlock(siblingId, true)),
146 );
147 return !anySiblingActive;
148 },
149 [clientId, hasBackground],
150 );
151
152 // Build background styles — infer type from values so backgroundType doesn't need to be kept in sync
153 const backgroundStyle: CSSProperties = {};
154
155 if (backgroundImage?.url) {
156 backgroundStyle.backgroundImage = `url(${backgroundImage.url})`;
157 backgroundStyle.backgroundSize = "cover";
158 backgroundStyle.backgroundPosition = focalPoint
159 ? `${focalPoint.x * 100}% ${focalPoint.y * 100}%`
160 : "50% 50%";
161 backgroundStyle.backgroundAttachment = isFixed ? "fixed" : "scroll";
162 backgroundStyle.backgroundRepeat = "no-repeat";
163 } else if (backgroundGradient) {
164 backgroundStyle.backgroundImage = backgroundGradient;
165 } else if (backgroundColor) {
166 backgroundStyle.backgroundColor = backgroundColor;
167 }
168
169 // Build border styles from new border object
170 const borderStyle: CSSProperties = {};
171 if (border && typeof border === "object") {
172 const sides = ["top", "right", "bottom", "left"] as const;
173 for (const side of sides) {
174 const b = (border as any)[side];
175 if (b) {
176 const key = `border${
177 side.charAt(0).toUpperCase() + side.slice(1)
178 }` as keyof CSSProperties;
179 borderStyle[key as any] = `${b.width || "0px"} ${b.style || "solid"} ${
180 b.color || "transparent"
181 }`;
182 }
183 }
184 }
185
186 // Build border-radius styles
187 const radiusStyle: CSSProperties = {};
188 if (slideBorderRadius && typeof slideBorderRadius === "object") {
189 const r = slideBorderRadius as any;
190 if (r.topLeft) {
191 radiusStyle.borderTopLeftRadius = r.topLeft;
192 }
193 if (r.topRight) {
194 radiusStyle.borderTopRightRadius = r.topRight;
195 }
196 if (r.bottomLeft) {
197 radiusStyle.borderBottomLeftRadius = r.bottomLeft;
198 }
199 if (r.bottomRight) {
200 radiusStyle.borderBottomRightRadius = r.bottomRight;
201 }
202 }
203
204 // Map content position to CSS flexbox alignment. The container below uses
205 // flex-direction: column, so align-items controls the horizontal axis and
206 // justify-content controls the vertical axis.
207 const positionToAlign = (pos: string) => {
208 const [vertical, horizontal] = pos.split("-");
209 const verticalMap: Record<string, string> = {
210 top: "flex-start",
211 bottom: "flex-end",
212 };
213 const horizontalMap: Record<string, string> = {
214 left: "flex-start",
215 right: "flex-end",
216 };
217 return {
218 alignItems: horizontalMap[horizontal] || "center",
219 justifyContent: verticalMap[vertical] || "center",
220 };
221 };
222
223 const { alignItems, justifyContent } = positionToAlign(
224 contentPosition || "center-center",
225 );
226
227 const blockProps = useBlockProps({
228 className: "swiper-slide sliderberg-slide-editor",
229 style: {
230 ...backgroundStyle,
231 ...borderStyle,
232 ...radiusStyle,
233 minHeight: `${effectiveMinHeight}px`,
234 },
235 });
236
237 const innerBlocksProps = useInnerBlocksProps(
238 {
239 className: "sliderberg-slide-content",
240 style: {
241 display: "flex",
242 flexDirection: "column",
243 alignItems,
244 justifyContent,
245 minHeight: `${effectiveMinHeight}px`,
246 },
247 },
248 {
249 template: [
250 [
251 "core/heading",
252 {
253 placeholder: "Slide Title...",
254 level: 2,
255 },
256 ],
257 ["core/paragraph", { placeholder: "Slide content..." }],
258 ],
259 templateLock: false,
260 },
261 );
262
263 const showOverlay = !!overlayColor && !!backgroundImage?.url;
264
265 if (!hasBackground) {
266 return (
267 <div {...blockProps}>
268 {contentPositionControl}
269 <SlideInspector attributes={attributes} setAttributes={setAttributes} />
270 <SlidePlaceholder
271 clientId={clientId}
272 contentPosition={contentPosition || "center-center"}
273 backgroundColor={backgroundColor}
274 border={border || {}}
275 slideBorderRadius={slideBorderRadius || {}}
276 minHeight={effectiveMinHeight}
277 onUpdate={setAttributes}
278 collapsed={!showFullPlaceholder}
279 />
280 </div>
281 );
282 }
283
284 return (
285 <div {...blockProps}>
286 {contentPositionControl}
287 <SlideInspector attributes={attributes} setAttributes={setAttributes} />
288 {showOverlay && (
289 <div
290 className="sliderberg-slide-overlay"
291 style={{
292 backgroundColor: overlayColor,
293 opacity: overlayOpacity || 1,
294 }}
295 />
296 )}
297 <div {...innerBlocksProps} />
298 </div>
299 );
300 }
301