PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / src / blocks / image / element.tsx

element.tsx in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/blocks/image/element.tsx

440 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useRef, useState, useEffect } from "react";
2 import {
3 RichText,
4 MediaUpload,
5 MediaUploadCheck,
6 } from "@wordpress/block-editor";
7 import { Button, Placeholder, ResizableBox } from "@wordpress/components";
8 import { __ } from "@wordpress/i18n";
9 import { image as imageIcon } from "@wordpress/icons";
10 import { mergeAttrsWithDefaultsAndApplyBindings } from "@tableberg/shared/utils/merge-attrs-with-defaults-and-apply-bindings";
11
12 import { useTableStore } from "../../store";
13 import { useClickOutside } from "../../hooks/useClickOutside";
14 import { ElementRendererProps } from "../../elements";
15 import { ImageElementControls } from "./controls";
16 import { BindableAttribute, ElementBindings } from "../../dynamic-data/types";
17 import { useDynamicDataBindings } from "../../dynamic-data/hooks/useDynamicData";
18 import { useBlockCardUpdateShim } from "../../hooks/block-editor-compat";
19
20 interface MediaSize {
21 height: number;
22 width: number;
23 url: string;
24 orientation?: string;
25 }
26
27 export interface MediaSizes {
28 thumbnail?: MediaSize;
29 medium?: MediaSize;
30 large?: MediaSize;
31 full?: MediaSize;
32 [key: string]: MediaSize | undefined;
33 }
34
35 export interface MediaObject {
36 id?: number;
37 url?: string;
38 alt?: string;
39 title?: string;
40 sizes?: MediaSizes;
41 }
42
43 export interface ImageElementAttributes {
44 media: MediaObject;
45 height: string;
46 width: string;
47 alt: string;
48 align: "left" | "center" | "right";
49 aspectRatio: string;
50 scale: string;
51 sizeSlug: string;
52 caption: string;
53 href: string;
54 linkTarget: string;
55 lightbox: {
56 enabled: boolean;
57 };
58 border: {
59 top: string;
60 right: string;
61 bottom: string;
62 left: string;
63 };
64 borderRadius: {
65 topLeft: string;
66 topRight: string;
67 bottomLeft: string;
68 bottomRight: string;
69 };
70 }
71
72 export const imageAttrDefaults: ImageElementAttributes = {
73 media: {},
74 height: "",
75 width: "150px",
76 alt: "",
77 align: "left",
78 aspectRatio: "",
79 scale: "cover",
80 sizeSlug: "large",
81 caption: "",
82 href: "",
83 linkTarget: "_self",
84 lightbox: {
85 enabled: false,
86 },
87 border: {
88 top: "",
89 right: "",
90 bottom: "",
91 left: "",
92 },
93 borderRadius: {
94 topLeft: "",
95 topRight: "",
96 bottomLeft: "",
97 bottomRight: "",
98 },
99 };
100
101 export interface ImageElementType {
102 name: "image";
103 attributes: ImageElementAttributes;
104 bindings?: ElementBindings;
105 }
106
107 export function ImageElement({
108 attributes,
109 bindings,
110 cellCoords,
111 elementIndex,
112 }: ElementRendererProps<ImageElementAttributes>) {
113 const wrapperRef = useRef<HTMLDivElement>(null);
114 const imageRef = useRef<HTMLImageElement>(null);
115
116 const sortPreviewMode = useTableStore(state => state.sortPreviewMode);
117 const isElementSelected = useTableStore(state => state.isElementSelected);
118 const setSelectedElement = useTableStore(state => state.setSelectedElement);
119 const clearSelectedElement = useTableStore(
120 state => state.clearSelectedElement
121 );
122 const updateCellElement = useTableStore(state => state.updateCellElement);
123 const updateBlockCard = useBlockCardUpdateShim();
124
125 const isSelected = isElementSelected(cellCoords, elementIndex);
126
127 const [showCaption, setShowCaption] = useState(!!attributes.caption);
128
129 useClickOutside({
130 ref: wrapperRef,
131 onClickOutside: () => {
132 if (isSelected) {
133 clearSelectedElement();
134 }
135 },
136 });
137
138 const { values: previewValues } = useDynamicDataBindings(bindings);
139
140 const mergedAttrs = mergeAttrsWithDefaultsAndApplyBindings(
141 attributes,
142 imageAttrDefaults,
143 bindings,
144 previewValues,
145 __("(No data)", "tableberg")
146 ) as ImageElementAttributes;
147
148 const {
149 media,
150 height,
151 width,
152 alt,
153 aspectRatio,
154 scale,
155 sizeSlug,
156 caption,
157 href,
158 linkTarget,
159 border,
160 borderRadius,
161 } = mergedAttrs;
162
163 const hasImage = !!media.url || !!media.id;
164
165 // Get image URL based on size slug
166 const getImageUrl = () => {
167 if (media.sizes && media.sizes[sizeSlug]) {
168 return media.sizes[sizeSlug]!.url;
169 }
170 return media.url || "";
171 };
172
173 const imageSrc = getImageUrl();
174
175 const onSelectMedia = (newMedia: any) => {
176 if (!newMedia || !newMedia.url) {
177 updateCellElement(cellCoords, elementIndex, {
178 attributes: { media: {} },
179 });
180 return;
181 }
182
183 updateCellElement(cellCoords, elementIndex, {
184 attributes: {
185 media: {
186 id: newMedia.id,
187 url: newMedia.url,
188 alt: newMedia.alt || "",
189 title: newMedia.title || "",
190 sizes: newMedia.sizes || {},
191 },
192 alt: newMedia.alt || "",
193 },
194 });
195 };
196
197 const [naturalDimensions, setNaturalDimensions] = useState<{
198 width: number;
199 height: number;
200 } | null>(null);
201
202 useEffect(() => {
203 if (imageRef.current?.complete && imageRef.current.naturalWidth) {
204 setNaturalDimensions({
205 width: imageRef.current.naturalWidth,
206 height: imageRef.current.naturalHeight,
207 });
208 }
209 }, [imageSrc]);
210
211 const handleImageLoad = () => {
212 if (imageRef.current) {
213 setNaturalDimensions({
214 width: imageRef.current.naturalWidth,
215 height: imageRef.current.naturalHeight,
216 });
217 }
218 };
219
220 const getAspectRatio = () => {
221 if (aspectRatio) {
222 return aspectRatio;
223 }
224 if (naturalDimensions) {
225 return `${naturalDimensions.width}/${naturalDimensions.height}`;
226 }
227 return undefined;
228 };
229
230 const handleResize = (_event: any, direction: string, elt: HTMLElement) => {
231 let ratio = 1;
232 const currentAspectRatio =
233 aspectRatio ||
234 (naturalDimensions
235 ? `${naturalDimensions.width}/${naturalDimensions.height}`
236 : "1/1");
237
238 const parts = currentAspectRatio.split("/");
239 if (parts.length === 2) {
240 ratio = parseInt(parts[0]) / parseInt(parts[1]);
241 } else if (currentAspectRatio === "1") {
242 ratio = 1;
243 }
244
245 let w = elt.offsetWidth;
246 let h = elt.offsetHeight;
247
248 if (direction === "bottom") {
249 w = h * ratio;
250 } else {
251 h = w / ratio;
252 }
253
254 updateCellElement(cellCoords, elementIndex, {
255 attributes: {
256 width: `${Math.round(w)}px`,
257 height: `${Math.round(h)}px`,
258 },
259 });
260 };
261
262 const imageStyle: React.CSSProperties = {
263 aspectRatio: getAspectRatio(),
264 objectFit: scale as React.CSSProperties["objectFit"],
265 width: width || "100%",
266 height: height || "auto",
267 borderTopLeftRadius: borderRadius.topLeft,
268 borderTopRightRadius: borderRadius.topRight,
269 borderBottomLeftRadius: borderRadius.bottomLeft,
270 borderBottomRightRadius: borderRadius.bottomRight,
271 borderTop: border.top,
272 borderRight: border.right,
273 borderBottom: border.bottom,
274 borderLeft: border.left,
275 display: "block",
276 };
277
278 const renderImage = () => {
279 const img = (
280 <img
281 ref={imageRef}
282 src={imageSrc}
283 alt={alt || media.alt || ""}
284 style={imageStyle}
285 onLoad={handleImageLoad}
286 />
287 );
288
289 if (href && !isSelected) {
290 return (
291 <a
292 href={href}
293 target={linkTarget}
294 rel={
295 linkTarget === "_blank"
296 ? "noopener noreferrer"
297 : undefined
298 }
299 onClick={e => e.preventDefault()}
300 >
301 {img}
302 </a>
303 );
304 }
305
306 return img;
307 };
308
309 return (
310 <>
311 {isSelected && (
312 <ImageElementControls
313 attributes={mergedAttrs}
314 bindings={bindings}
315 showCaption={showCaption}
316 setShowCaption={setShowCaption}
317 cellCoords={cellCoords}
318 elementIndex={elementIndex}
319 />
320 )}
321 <figure
322 ref={wrapperRef}
323 className="tableberg-image-element"
324 style={{ margin: 0, lineHeight: 1 }}
325 onClick={e => {
326 e.stopPropagation();
327 if (!sortPreviewMode) {
328 setSelectedElement(cellCoords, elementIndex);
329 if (wrapperRef.current) {
330 updateBlockCard(
331 wrapperRef.current,
332 "Image",
333 "An image element within a Tableberg cell"
334 );
335 }
336 }
337 }}
338 >
339 {hasImage ? (
340 <>
341 {isSelected ? (
342 <ResizableBox
343 size={{
344 width: width || "auto",
345 height: height || "auto",
346 }}
347 showHandle={isSelected}
348 minWidth={50}
349 minHeight={50}
350 maxWidth={720}
351 enable={{
352 top: false,
353 right: true,
354 bottom: true,
355 left: false,
356 }}
357 onResize={handleResize}
358 >
359 {renderImage()}
360 </ResizableBox>
361 ) : (
362 renderImage()
363 )}
364 {showCaption && (isSelected || caption) && (
365 <RichText
366 tagName="figcaption"
367 className="tableberg-image-caption"
368 aria-label={__(
369 "Image caption text",
370 "tableberg"
371 )}
372 placeholder={__("Add caption", "tableberg")}
373 value={caption}
374 onChange={(value: string) =>
375 updateCellElement(
376 cellCoords,
377 elementIndex,
378 {
379 attributes: { caption: value },
380 }
381 )
382 }
383 style={{
384 marginTop: "0.5em",
385 marginBottom: "1em",
386 textAlign: "center",
387 fontSize: "0.875em",
388 color: "#666",
389 }}
390 />
391 )}
392 </>
393 ) : (
394 <MediaUploadCheck>
395 <MediaUpload
396 onSelect={onSelectMedia}
397 allowedTypes={["image"]}
398 value={media.id}
399 render={({ open }) => (
400 <Placeholder
401 className="tableberg-image-placeholder"
402 icon={imageIcon}
403 label={__("Image", "tableberg")}
404 instructions={__(
405 "Upload an image or pick one from your media library.",
406 "tableberg"
407 )}
408 >
409 <Button variant="secondary" onClick={open}>
410 {__("Media Library", "tableberg")}
411 </Button>
412 </Placeholder>
413 )}
414 />
415 </MediaUploadCheck>
416 )}
417 </figure>
418 </>
419 );
420 }
421
422 export const imageBindableAttributes: BindableAttribute[] = [
423 {
424 path: "media.url",
425 label: __("Image URL", "tableberg"),
426 },
427 {
428 path: "alt",
429 label: __("Alt Text", "tableberg"),
430 },
431 {
432 path: "href",
433 label: __("Link URL", "tableberg"),
434 },
435 {
436 path: "caption",
437 label: __("Caption", "tableberg"),
438 },
439 ];
440