| 1 |
import { useState, useMemo } from "react"; |
| 2 |
import { |
| 3 |
InspectorControls, |
| 4 |
BlockControls, |
| 5 |
MediaUpload, |
| 6 |
MediaUploadCheck, |
| 7 |
__experimentalLinkControl as LinkControl, |
| 8 |
} from "@wordpress/block-editor"; |
| 9 |
import { |
| 10 |
Button, |
| 11 |
SelectControl, |
| 12 |
TextareaControl, |
| 13 |
ToolbarButton, |
| 14 |
ToolbarGroup, |
| 15 |
Popover, |
| 16 |
__experimentalToolsPanel as ToolsPanel, |
| 17 |
__experimentalToolsPanelItem as ToolsPanelItem, |
| 18 |
__experimentalUnitControl as UnitControl, |
| 19 |
__experimentalToggleGroupControl as ToggleGroupControl, |
| 20 |
__experimentalToggleGroupControlOption as ToggleGroupControlOption, |
| 21 |
} from "@wordpress/components"; |
| 22 |
import { __ } from "@wordpress/i18n"; |
| 23 |
import { |
| 24 |
alignNone, |
| 25 |
caption as captionIcon, |
| 26 |
fullscreen, |
| 27 |
Icon, |
| 28 |
link, |
| 29 |
linkOff, |
| 30 |
replace, |
| 31 |
} from "@wordpress/icons"; |
| 32 |
import { prependHTTP } from "@wordpress/url"; |
| 33 |
import { |
| 34 |
BorderControl, |
| 35 |
BorderRadiusControl, |
| 36 |
ToolbarWithDropdown, |
| 37 |
} from "@tableberg/components"; |
| 38 |
import { CellKey } from "../../attributes"; |
| 39 |
import { useTableStore } from "../../store"; |
| 40 |
import { ImageElementAttributes, imageAttrDefaults } from "./element"; |
| 41 |
import { ElementBindings } from "../../dynamic-data/types"; |
| 42 |
import { DynamicDataPanel } from "../../components/DynamicDataPanel"; |
| 43 |
import { ElementDeleteButton } from "../../components/ElementDeleteButton"; |
| 44 |
import { ElementOptionsBlockControls } from "../../components/ElementOptionsButton"; |
| 45 |
|
| 46 |
const ASPECT_RATIO_OPTIONS = [ |
| 47 |
{ label: __("Original", "tableberg"), value: "" }, |
| 48 |
{ label: __("Square - 1:1", "tableberg"), value: "1" }, |
| 49 |
{ label: __("Standard - 4:3", "tableberg"), value: "4/3" }, |
| 50 |
{ label: __("Portrait - 3:4", "tableberg"), value: "3/4" }, |
| 51 |
{ label: __("Classic - 3:2", "tableberg"), value: "3/2" }, |
| 52 |
{ label: __("Classic Portrait - 2:3", "tableberg"), value: "2/3" }, |
| 53 |
{ label: __("Wide - 16:9", "tableberg"), value: "16/9" }, |
| 54 |
{ label: __("Tall - 9:16", "tableberg"), value: "9/16" }, |
| 55 |
]; |
| 56 |
|
| 57 |
const SCALE_OPTIONS = [ |
| 58 |
{ |
| 59 |
value: "cover", |
| 60 |
label: __("Cover", "tableberg"), |
| 61 |
help: __("Fill the space by clipping what doesn't fit.", "tableberg"), |
| 62 |
}, |
| 63 |
{ |
| 64 |
value: "contain", |
| 65 |
label: __("Contain", "tableberg"), |
| 66 |
help: __("Fit the content to the space without clipping.", "tableberg"), |
| 67 |
}, |
| 68 |
]; |
| 69 |
|
| 70 |
const SIZE_SLUG_OPTIONS = [ |
| 71 |
{ label: __("Thumbnail", "tableberg"), value: "thumbnail" }, |
| 72 |
{ label: __("Medium", "tableberg"), value: "medium" }, |
| 73 |
{ label: __("Large", "tableberg"), value: "large" }, |
| 74 |
{ label: __("Full Size", "tableberg"), value: "full" }, |
| 75 |
]; |
| 76 |
|
| 77 |
export function ImageElementControls({ |
| 78 |
attributes, |
| 79 |
bindings, |
| 80 |
showCaption, |
| 81 |
setShowCaption, |
| 82 |
cellCoords, |
| 83 |
elementIndex, |
| 84 |
updateAttrs: updateAttrsProp, |
| 85 |
}: { |
| 86 |
attributes: ImageElementAttributes; |
| 87 |
bindings?: ElementBindings; |
| 88 |
showCaption: boolean; |
| 89 |
setShowCaption: (show: boolean) => void; |
| 90 |
cellCoords: CellKey; |
| 91 |
elementIndex: number; |
| 92 |
// Native block edits inject a setAttributes-based updater; store-backed |
| 93 |
// element renderers use the table-store fallback. |
| 94 |
updateAttrs?: (attrs: Partial<ImageElementAttributes>) => void; |
| 95 |
}) { |
| 96 |
const { |
| 97 |
media, |
| 98 |
alt, |
| 99 |
aspectRatio, |
| 100 |
height, |
| 101 |
width, |
| 102 |
scale, |
| 103 |
sizeSlug, |
| 104 |
href, |
| 105 |
linkTarget, |
| 106 |
lightbox, |
| 107 |
align, |
| 108 |
border, |
| 109 |
borderRadius, |
| 110 |
caption, |
| 111 |
} = attributes; |
| 112 |
|
| 113 |
const storeUpdateAttrs = useTableStore( |
| 114 |
state => state.updateSelectedElementAttrs |
| 115 |
); |
| 116 |
const updateAttrs = updateAttrsProp ?? storeUpdateAttrs; |
| 117 |
|
| 118 |
const [isEditingURL, setIsEditingURL] = useState(false); |
| 119 |
|
| 120 |
const isURLSet = !!href; |
| 121 |
const isLightboxEnabled = !!lightbox?.enabled; |
| 122 |
const hasLinkAction = isURLSet || isLightboxEnabled; |
| 123 |
const opensInNewTab = linkTarget === "_blank"; |
| 124 |
|
| 125 |
const linkValue = useMemo( |
| 126 |
() => ({ url: href, opensInNewTab }), |
| 127 |
[href, opensInNewTab] |
| 128 |
); |
| 129 |
|
| 130 |
const unlink = () => { |
| 131 |
updateAttrs({ |
| 132 |
href: "", |
| 133 |
linkTarget: "_self", |
| 134 |
lightbox: imageAttrDefaults.lightbox, |
| 135 |
}); |
| 136 |
setIsEditingURL(false); |
| 137 |
}; |
| 138 |
|
| 139 |
const toggleLightbox = () => { |
| 140 |
updateAttrs({ |
| 141 |
lightbox: { |
| 142 |
enabled: !isLightboxEnabled, |
| 143 |
}, |
| 144 |
...(!isLightboxEnabled |
| 145 |
? { |
| 146 |
href: "", |
| 147 |
linkTarget: "_self", |
| 148 |
} |
| 149 |
: {}), |
| 150 |
}); |
| 151 |
setIsEditingURL(false); |
| 152 |
}; |
| 153 |
|
| 154 |
const onSelectMedia = (newMedia: any) => { |
| 155 |
if (!newMedia || !newMedia.url) { |
| 156 |
updateAttrs({ media: {} }); |
| 157 |
return; |
| 158 |
} |
| 159 |
|
| 160 |
updateAttrs({ |
| 161 |
media: { |
| 162 |
id: newMedia.id, |
| 163 |
url: newMedia.url, |
| 164 |
alt: newMedia.alt || "", |
| 165 |
title: newMedia.title || "", |
| 166 |
sizes: newMedia.sizes || {}, |
| 167 |
}, |
| 168 |
alt: newMedia.alt || "", |
| 169 |
}); |
| 170 |
}; |
| 171 |
|
| 172 |
const hasImage = !!media.url || !!media.id; |
| 173 |
const scaleHelp = useMemo(() => { |
| 174 |
return SCALE_OPTIONS.reduce( |
| 175 |
(acc: { [key: string]: string }, option) => { |
| 176 |
acc[option.value] = option.help; |
| 177 |
return acc; |
| 178 |
}, |
| 179 |
{} |
| 180 |
); |
| 181 |
}, []); |
| 182 |
|
| 183 |
const resetAll = () => { |
| 184 |
updateAttrs({ |
| 185 |
alt: "", |
| 186 |
aspectRatio: "", |
| 187 |
height: "", |
| 188 |
scale: "cover", |
| 189 |
width: "150px", |
| 190 |
sizeSlug: "large", |
| 191 |
lightbox: imageAttrDefaults.lightbox, |
| 192 |
}); |
| 193 |
}; |
| 194 |
|
| 195 |
return ( |
| 196 |
<> |
| 197 |
<BlockControls group="block"> |
| 198 |
<ToolbarWithDropdown |
| 199 |
icon={alignNone} |
| 200 |
title={__("Align image", "tableberg")} |
| 201 |
value={align} |
| 202 |
onChange={(newVal: string | undefined) => { |
| 203 |
if (newVal) { |
| 204 |
updateAttrs({ |
| 205 |
align: newVal as "left" | "center" | "right", |
| 206 |
}); |
| 207 |
} |
| 208 |
}} |
| 209 |
controlset="alignment" |
| 210 |
/> |
| 211 |
<ToolbarButton |
| 212 |
onClick={() => { |
| 213 |
setShowCaption(!showCaption); |
| 214 |
if (showCaption && caption) { |
| 215 |
updateAttrs({ caption: "" }); |
| 216 |
} |
| 217 |
}} |
| 218 |
icon={captionIcon} |
| 219 |
isPressed={showCaption} |
| 220 |
label={ |
| 221 |
showCaption |
| 222 |
? __("Remove caption", "tableberg") |
| 223 |
: __("Add caption", "tableberg") |
| 224 |
} |
| 225 |
/> |
| 226 |
<ToolbarButton |
| 227 |
icon={link} |
| 228 |
title={ |
| 229 |
hasLinkAction |
| 230 |
? __("Edit link", "tableberg") |
| 231 |
: __("Link", "tableberg") |
| 232 |
} |
| 233 |
onClick={() => setIsEditingURL(true)} |
| 234 |
isActive={hasLinkAction} |
| 235 |
/> |
| 236 |
<ElementDeleteButton |
| 237 |
cellCoords={cellCoords} |
| 238 |
elementIndex={elementIndex} |
| 239 |
/> |
| 240 |
{isEditingURL && ( |
| 241 |
<Popover |
| 242 |
placement="bottom" |
| 243 |
onClose={() => setIsEditingURL(false)} |
| 244 |
shift |
| 245 |
> |
| 246 |
{isLightboxEnabled ? ( |
| 247 |
<div |
| 248 |
style={{ |
| 249 |
alignItems: "center", |
| 250 |
border: "1px solid #1e1e1e", |
| 251 |
borderRadius: "2px", |
| 252 |
display: "flex", |
| 253 |
gap: "16px", |
| 254 |
justifyContent: "space-between", |
| 255 |
minWidth: "560px", |
| 256 |
padding: "12px 14px", |
| 257 |
}} |
| 258 |
> |
| 259 |
<div |
| 260 |
style={{ |
| 261 |
alignItems: "center", |
| 262 |
display: "flex", |
| 263 |
gap: "16px", |
| 264 |
}} |
| 265 |
> |
| 266 |
<Icon icon={fullscreen} /> |
| 267 |
<span |
| 268 |
style={{ |
| 269 |
display: "flex", |
| 270 |
flexDirection: "column", |
| 271 |
}} |
| 272 |
> |
| 273 |
<span> |
| 274 |
{__( |
| 275 |
"Enlarge on click", |
| 276 |
"tableberg" |
| 277 |
)} |
| 278 |
</span> |
| 279 |
<span |
| 280 |
style={{ |
| 281 |
color: "#757575", |
| 282 |
}} |
| 283 |
> |
| 284 |
{__( |
| 285 |
"Scales the image with a lightbox effect", |
| 286 |
"tableberg" |
| 287 |
)} |
| 288 |
</span> |
| 289 |
</span> |
| 290 |
</div> |
| 291 |
<Button |
| 292 |
icon={linkOff} |
| 293 |
label={__( |
| 294 |
"Disable Enlarge on click", |
| 295 |
"tableberg" |
| 296 |
)} |
| 297 |
onClick={toggleLightbox} |
| 298 |
showTooltip |
| 299 |
variant="secondary" |
| 300 |
/> |
| 301 |
</div> |
| 302 |
) : ( |
| 303 |
<> |
| 304 |
<LinkControl |
| 305 |
value={linkValue} |
| 306 |
onChange={({ |
| 307 |
url: newURL = "", |
| 308 |
opensInNewTab: newOpensInNewTab, |
| 309 |
}: { |
| 310 |
url?: string; |
| 311 |
opensInNewTab?: boolean; |
| 312 |
}) => { |
| 313 |
updateAttrs({ |
| 314 |
href: prependHTTP(newURL), |
| 315 |
linkTarget: newOpensInNewTab |
| 316 |
? "_blank" |
| 317 |
: "_self", |
| 318 |
lightbox: |
| 319 |
imageAttrDefaults.lightbox, |
| 320 |
}); |
| 321 |
}} |
| 322 |
onRemove={unlink} |
| 323 |
/> |
| 324 |
{hasImage && ( |
| 325 |
<div |
| 326 |
style={{ |
| 327 |
borderTop: "1px solid #ddd", |
| 328 |
padding: "8px 0", |
| 329 |
}} |
| 330 |
> |
| 331 |
<Button |
| 332 |
icon={fullscreen} |
| 333 |
isPressed={isLightboxEnabled} |
| 334 |
onClick={toggleLightbox} |
| 335 |
style={{ |
| 336 |
alignItems: "flex-start", |
| 337 |
display: "flex", |
| 338 |
gap: "16px", |
| 339 |
height: "auto", |
| 340 |
justifyContent: "flex-start", |
| 341 |
padding: "12px 16px", |
| 342 |
textAlign: "left", |
| 343 |
width: "100%", |
| 344 |
}} |
| 345 |
> |
| 346 |
<span |
| 347 |
style={{ |
| 348 |
display: "flex", |
| 349 |
flexDirection: "column", |
| 350 |
gap: "4px", |
| 351 |
}} |
| 352 |
> |
| 353 |
<span> |
| 354 |
{__( |
| 355 |
"Enlarge on click", |
| 356 |
"tableberg" |
| 357 |
)} |
| 358 |
</span> |
| 359 |
<span |
| 360 |
style={{ |
| 361 |
color: "#757575", |
| 362 |
fontSize: "12px", |
| 363 |
}} |
| 364 |
> |
| 365 |
{__( |
| 366 |
"Scale the image with a lightbox effect.", |
| 367 |
"tableberg" |
| 368 |
)} |
| 369 |
</span> |
| 370 |
</span> |
| 371 |
</Button> |
| 372 |
</div> |
| 373 |
)} |
| 374 |
</> |
| 375 |
)} |
| 376 |
</Popover> |
| 377 |
)} |
| 378 |
</BlockControls> |
| 379 |
<ElementOptionsBlockControls |
| 380 |
cellCoords={cellCoords} |
| 381 |
elementIndex={elementIndex} |
| 382 |
/> |
| 383 |
{hasImage && ( |
| 384 |
<BlockControls> |
| 385 |
<ToolbarGroup> |
| 386 |
<MediaUploadCheck> |
| 387 |
<MediaUpload |
| 388 |
onSelect={onSelectMedia} |
| 389 |
allowedTypes={["image"]} |
| 390 |
value={media.id} |
| 391 |
render={({ open }) => ( |
| 392 |
<ToolbarButton |
| 393 |
onClick={open} |
| 394 |
icon={replace} |
| 395 |
label={__("Replace", "tableberg")} |
| 396 |
/> |
| 397 |
)} |
| 398 |
/> |
| 399 |
</MediaUploadCheck> |
| 400 |
</ToolbarGroup> |
| 401 |
</BlockControls> |
| 402 |
)} |
| 403 |
<InspectorControls> |
| 404 |
<ToolsPanel |
| 405 |
label={__("Settings", "tableberg")} |
| 406 |
resetAll={resetAll} |
| 407 |
> |
| 408 |
<ToolsPanelItem |
| 409 |
isShownByDefault |
| 410 |
hasValue={() => !!alt} |
| 411 |
label={__("Alternative Text", "tableberg")} |
| 412 |
onDeselect={() => updateAttrs({ alt: "" })} |
| 413 |
> |
| 414 |
<TextareaControl |
| 415 |
__nextHasNoMarginBottom |
| 416 |
value={alt} |
| 417 |
label={__("Alternative Text", "tableberg")} |
| 418 |
onChange={(newValue: string) => |
| 419 |
updateAttrs({ alt: newValue }) |
| 420 |
} |
| 421 |
help={__( |
| 422 |
"Describe the image for screen readers.", |
| 423 |
"tableberg" |
| 424 |
)} |
| 425 |
/> |
| 426 |
</ToolsPanelItem> |
| 427 |
<ToolsPanelItem |
| 428 |
isShownByDefault |
| 429 |
label={__("Aspect ratio", "tableberg")} |
| 430 |
onDeselect={() => updateAttrs({ aspectRatio: "" })} |
| 431 |
hasValue={() => aspectRatio !== ""} |
| 432 |
> |
| 433 |
<SelectControl |
| 434 |
value={aspectRatio || ""} |
| 435 |
__nextHasNoMarginBottom |
| 436 |
options={ASPECT_RATIO_OPTIONS} |
| 437 |
label={__("Aspect ratio", "tableberg")} |
| 438 |
onChange={newValue => |
| 439 |
updateAttrs({ aspectRatio: newValue }) |
| 440 |
} |
| 441 |
/> |
| 442 |
</ToolsPanelItem> |
| 443 |
{aspectRatio && ( |
| 444 |
<ToolsPanelItem |
| 445 |
label={__("Scale", "tableberg")} |
| 446 |
isShownByDefault |
| 447 |
hasValue={() => scale !== "cover"} |
| 448 |
onDeselect={() => updateAttrs({ scale: "cover" })} |
| 449 |
> |
| 450 |
<ToggleGroupControl |
| 451 |
label={__("Scale", "tableberg")} |
| 452 |
isBlock |
| 453 |
help={scaleHelp[scale || "cover"]} |
| 454 |
value={scale || "cover"} |
| 455 |
onChange={newScale => |
| 456 |
updateAttrs({ scale: newScale as string }) |
| 457 |
} |
| 458 |
__nextHasNoMarginBottom |
| 459 |
> |
| 460 |
{SCALE_OPTIONS.map(option => ( |
| 461 |
<ToggleGroupControlOption |
| 462 |
key={option.value} |
| 463 |
value={option.value} |
| 464 |
label={option.label} |
| 465 |
/> |
| 466 |
))} |
| 467 |
</ToggleGroupControl> |
| 468 |
</ToolsPanelItem> |
| 469 |
)} |
| 470 |
<div |
| 471 |
style={{ |
| 472 |
display: "flex", |
| 473 |
gap: "10px", |
| 474 |
gridColumn: "1 / -1", |
| 475 |
}} |
| 476 |
> |
| 477 |
<ToolsPanelItem |
| 478 |
isShownByDefault |
| 479 |
label={__("Width", "tableberg")} |
| 480 |
hasValue={() => width !== "" && width !== "150px"} |
| 481 |
onDeselect={() => updateAttrs({ width: "150px" })} |
| 482 |
> |
| 483 |
<UnitControl |
| 484 |
label={__("Width", "tableberg")} |
| 485 |
placeholder={__("Auto", "tableberg")} |
| 486 |
labelPosition="top" |
| 487 |
units={[ |
| 488 |
{ value: "px", label: "px", default: 0 }, |
| 489 |
]} |
| 490 |
min={0} |
| 491 |
value={width} |
| 492 |
onChange={(newWidth: string | undefined) => |
| 493 |
updateAttrs({ width: newWidth || "" }) |
| 494 |
} |
| 495 |
/> |
| 496 |
</ToolsPanelItem> |
| 497 |
<ToolsPanelItem |
| 498 |
isShownByDefault |
| 499 |
label={__("Height", "tableberg")} |
| 500 |
hasValue={() => height !== ""} |
| 501 |
onDeselect={() => updateAttrs({ height: "" })} |
| 502 |
> |
| 503 |
<UnitControl |
| 504 |
label={__("Height", "tableberg")} |
| 505 |
placeholder={__("Auto", "tableberg")} |
| 506 |
labelPosition="top" |
| 507 |
units={[ |
| 508 |
{ value: "px", label: "px", default: 0 }, |
| 509 |
]} |
| 510 |
min={0} |
| 511 |
value={height} |
| 512 |
onChange={(newHeight: string | undefined) => |
| 513 |
updateAttrs({ height: newHeight || "" }) |
| 514 |
} |
| 515 |
/> |
| 516 |
</ToolsPanelItem> |
| 517 |
</div> |
| 518 |
{hasImage && |
| 519 |
media.sizes && |
| 520 |
Object.keys(media.sizes).length > 0 && ( |
| 521 |
<ToolsPanelItem |
| 522 |
isShownByDefault |
| 523 |
label={__("Resolution", "tableberg")} |
| 524 |
hasValue={() => sizeSlug !== "large"} |
| 525 |
onDeselect={() => |
| 526 |
updateAttrs({ sizeSlug: "large" }) |
| 527 |
} |
| 528 |
> |
| 529 |
<SelectControl |
| 530 |
label={__("Resolution", "tableberg")} |
| 531 |
value={sizeSlug} |
| 532 |
options={SIZE_SLUG_OPTIONS.filter( |
| 533 |
opt => media.sizes?.[opt.value] |
| 534 |
)} |
| 535 |
onChange={(newSlug: string) => |
| 536 |
updateAttrs({ sizeSlug: newSlug }) |
| 537 |
} |
| 538 |
help={__( |
| 539 |
"Select the size of the source image.", |
| 540 |
"tableberg" |
| 541 |
)} |
| 542 |
/> |
| 543 |
</ToolsPanelItem> |
| 544 |
)} |
| 545 |
</ToolsPanel> |
| 546 |
</InspectorControls> |
| 547 |
<InspectorControls group="border"> |
| 548 |
<BorderControl |
| 549 |
value={border} |
| 550 |
label={__("Border", "tableberg")} |
| 551 |
onChange={(newBorder: any) => |
| 552 |
updateAttrs({ border: newBorder }) |
| 553 |
} |
| 554 |
onDeselect={() => |
| 555 |
updateAttrs({ border: imageAttrDefaults.border }) |
| 556 |
} |
| 557 |
hasValue={() => |
| 558 |
!!border.top || |
| 559 |
!!border.right || |
| 560 |
!!border.bottom || |
| 561 |
!!border.left |
| 562 |
} |
| 563 |
/> |
| 564 |
<BorderRadiusControl |
| 565 |
label={__("Border Radius", "tableberg")} |
| 566 |
value={borderRadius} |
| 567 |
onChange={(newRadius: any) => |
| 568 |
updateAttrs({ borderRadius: newRadius }) |
| 569 |
} |
| 570 |
onDeselect={() => |
| 571 |
updateAttrs({ |
| 572 |
borderRadius: imageAttrDefaults.borderRadius, |
| 573 |
}) |
| 574 |
} |
| 575 |
hasValue={() => |
| 576 |
!!borderRadius.topLeft || |
| 577 |
!!borderRadius.topRight || |
| 578 |
!!borderRadius.bottomLeft || |
| 579 |
!!borderRadius.bottomRight |
| 580 |
} |
| 581 |
/> |
| 582 |
</InspectorControls> |
| 583 |
<InspectorControls> |
| 584 |
<DynamicDataPanel elementType="image" bindings={bindings} /> |
| 585 |
</InspectorControls> |
| 586 |
</> |
| 587 |
); |
| 588 |
} |
| 589 |
|