| 1 |
import { GiphyFetch } from '@giphy/js-fetch-api'; |
| 2 |
import classnames from 'classnames/dedupe'; |
| 3 |
|
| 4 |
import { |
| 5 |
InspectorControls, |
| 6 |
RichText, |
| 7 |
useBlockProps, |
| 8 |
} from '@wordpress/block-editor'; |
| 9 |
import { |
| 10 |
Button, |
| 11 |
ExternalLink, |
| 12 |
PanelBody, |
| 13 |
Placeholder, |
| 14 |
Spinner, |
| 15 |
TextareaControl, |
| 16 |
TextControl, |
| 17 |
} from '@wordpress/components'; |
| 18 |
import { useState } from '@wordpress/element'; |
| 19 |
import { __ } from '@wordpress/i18n'; |
| 20 |
|
| 21 |
import getIcon from '../../utils/get-icon'; |
| 22 |
|
| 23 |
const GIPHY_API_KEY = 'Qm6AupESs2abqy7fSeHNQ892EWSS28r8'; |
| 24 |
|
| 25 |
// use @giphy/js-fetch-api to fetch gifs, instantiate with your api key |
| 26 |
const gf = new GiphyFetch(GIPHY_API_KEY); |
| 27 |
|
| 28 |
function splitAndLast(array, delimiter) { |
| 29 |
const split = array.split(delimiter); |
| 30 |
return split[split.length - 1]; |
| 31 |
} |
| 32 |
|
| 33 |
/** |
| 34 |
* Block Edit Class. |
| 35 |
* |
| 36 |
* @param props |
| 37 |
*/ |
| 38 |
export default function BlockEdit(props) { |
| 39 |
const { attributes, isSelected, setAttributes } = props; |
| 40 |
|
| 41 |
let { className } = props; |
| 42 |
|
| 43 |
const { caption, url, srcset, alt, width, height, searchText } = attributes; |
| 44 |
|
| 45 |
const [results, setResults] = useState(null); |
| 46 |
const [loading, setLoading] = useState(false); |
| 47 |
|
| 48 |
function parseSearch() { |
| 49 |
let giphyID = null; |
| 50 |
|
| 51 |
// If search is hardcoded Giphy URL following this pattern: https://giphy.com/embed/4ZFekt94LMhNK |
| 52 |
if (searchText.indexOf('//giphy.com/gifs') !== -1) { |
| 53 |
giphyID = splitAndLast(splitAndLast(searchText, '/'), '-'); |
| 54 |
} |
| 55 |
|
| 56 |
// If search is hardcoded Giphy URL following this pattern: http://i.giphy.com/4ZFekt94LMhNK.gif |
| 57 |
if (searchText.indexOf('//i.giphy.com') !== -1) { |
| 58 |
giphyID = splitAndLast(searchText, '/').replace('.gif', ''); |
| 59 |
} |
| 60 |
|
| 61 |
// https://media.giphy.com/media/gt0hYzKlMpfOg/giphy.gif |
| 62 |
const match = searchText.match( |
| 63 |
/http[s]?:\/\/media.giphy.com\/media\/([A-Za-z0-9\-.]+)\/giphy.gif/ |
| 64 |
); |
| 65 |
|
| 66 |
if (match) { |
| 67 |
// eslint-disable-next-line prefer-destructuring |
| 68 |
giphyID = match[1]; |
| 69 |
} |
| 70 |
|
| 71 |
if (giphyID) { |
| 72 |
return { |
| 73 |
id: giphyID, |
| 74 |
}; |
| 75 |
} |
| 76 |
|
| 77 |
return { |
| 78 |
search: searchText, |
| 79 |
}; |
| 80 |
} |
| 81 |
|
| 82 |
function fetch() { |
| 83 |
setLoading(true); |
| 84 |
|
| 85 |
const searchData = parseSearch(); |
| 86 |
let request = false; |
| 87 |
|
| 88 |
if (searchData.id) { |
| 89 |
request = gf.gifs([searchData.id]); |
| 90 |
} else { |
| 91 |
request = gf.search(searchData.search || '', { |
| 92 |
sort: 'relevant', |
| 93 |
limit: 10, |
| 94 |
}); |
| 95 |
} |
| 96 |
|
| 97 |
request.then((result) => { |
| 98 |
setLoading(false); |
| 99 |
|
| 100 |
if ( |
| 101 |
result.meta && |
| 102 |
result.meta.status && |
| 103 |
result.meta.status === 200 |
| 104 |
) { |
| 105 |
// If there is only one result, Giphy's API does not return an array. |
| 106 |
// The following statement normalizes the data into an array with one member in this case. |
| 107 |
const newResults = |
| 108 |
typeof result.data.images !== 'undefined' |
| 109 |
? [result.data] |
| 110 |
: result.data; |
| 111 |
const giphyData = newResults[0]; |
| 112 |
|
| 113 |
// No results |
| 114 |
if (!giphyData.images) { |
| 115 |
return; |
| 116 |
} |
| 117 |
|
| 118 |
setResults(newResults); |
| 119 |
} else { |
| 120 |
// Error |
| 121 |
} |
| 122 |
}); |
| 123 |
} |
| 124 |
|
| 125 |
function onSubmit() { |
| 126 |
fetch(); |
| 127 |
} |
| 128 |
|
| 129 |
function onFormSubmit(event) { |
| 130 |
event.preventDefault(); |
| 131 |
onSubmit(); |
| 132 |
} |
| 133 |
|
| 134 |
function selectGiphy(giphy) { |
| 135 |
const { images } = giphy; |
| 136 |
|
| 137 |
const sizes = [ |
| 138 |
'preview_gif', |
| 139 |
'downsized_small', |
| 140 |
'downsized_medium', |
| 141 |
'downsized_large', |
| 142 |
'downsized', |
| 143 |
]; |
| 144 |
|
| 145 |
let newSrcset = ''; |
| 146 |
|
| 147 |
sizes.forEach((sizeName) => { |
| 148 |
if ( |
| 149 |
images[sizeName] && |
| 150 |
images[sizeName].url && |
| 151 |
!new RegExp(` ${images[sizeName].width}w`).test(newSrcset) |
| 152 |
) { |
| 153 |
newSrcset += `${newSrcset ? ', ' : ''}${images[sizeName].url} ${ |
| 154 |
images[sizeName].width |
| 155 |
}w`; |
| 156 |
} |
| 157 |
}); |
| 158 |
|
| 159 |
setAttributes({ |
| 160 |
url: images.original.url, |
| 161 |
srcset: newSrcset, |
| 162 |
caption: giphy.title, |
| 163 |
alt: giphy.title, |
| 164 |
width: parseInt(images.original.width, 10), |
| 165 |
height: parseInt(images.original.height, 10), |
| 166 |
}); |
| 167 |
} |
| 168 |
|
| 169 |
const inputFields = ( |
| 170 |
<> |
| 171 |
<form |
| 172 |
className="ghostkit-gif-input-container" |
| 173 |
onSubmit={onFormSubmit} |
| 174 |
> |
| 175 |
<TextControl |
| 176 |
className="ghostkit-gif-input" |
| 177 |
placeholder={__( |
| 178 |
'Enter search terms, e.g. cat…', |
| 179 |
'ghostkit' |
| 180 |
)} |
| 181 |
onChange={(val) => setAttributes({ searchText: val })} |
| 182 |
value={searchText} |
| 183 |
__next40pxDefaultSize |
| 184 |
__nextHasNoMarginBottom |
| 185 |
/> |
| 186 |
<Button |
| 187 |
variant="secondary" |
| 188 |
onClick={(e) => { |
| 189 |
onFormSubmit(e); |
| 190 |
}} |
| 191 |
> |
| 192 |
{__('Search', 'ghostkit')} |
| 193 |
</Button> |
| 194 |
</form> |
| 195 |
{loading && <Spinner />} |
| 196 |
{!loading && results && results.length > 1 && ( |
| 197 |
<div className="ghostkit-gif-thumbnails-container"> |
| 198 |
{results.map((thumbnail) => { |
| 199 |
const thumbnailStyle = { |
| 200 |
backgroundImage: `url(${thumbnail.images.downsized_still.url})`, |
| 201 |
}; |
| 202 |
|
| 203 |
return ( |
| 204 |
// eslint-disable-next-line react/button-has-type |
| 205 |
<button |
| 206 |
className="ghostkit-gif-thumbnail-container" |
| 207 |
key={thumbnail.id} |
| 208 |
onClick={() => { |
| 209 |
selectGiphy(thumbnail); |
| 210 |
}} |
| 211 |
style={thumbnailStyle} |
| 212 |
> |
| 213 |
{' '} |
| 214 |
</button> |
| 215 |
); |
| 216 |
})} |
| 217 |
</div> |
| 218 |
)} |
| 219 |
</> |
| 220 |
); |
| 221 |
|
| 222 |
className = classnames('ghostkit-gif', className); |
| 223 |
|
| 224 |
const blockProps = useBlockProps({ className }); |
| 225 |
|
| 226 |
return ( |
| 227 |
<> |
| 228 |
<InspectorControls> |
| 229 |
<PanelBody className="components-panel__body-gif-branding"> |
| 230 |
<svg |
| 231 |
xmlns="http://www.w3.org/2000/svg" |
| 232 |
viewBox="0 0 202 22" |
| 233 |
> |
| 234 |
<path d="M4.6 5.9H0v10h1.6v-3.1h3c4.8 0 4.8-6.9 0-6.9zm0 5.4h-3v-4h3c2.6.1 2.6 4 0 4zM51.2 12.3c2-.3 2.7-1.7 2.7-3.1 0-1.7-1.2-3.3-3.5-3.3h-4.6v10h1.6v-3.4h2.1l3 3.4h1.9l-.2-.3-3-3.3zM47.4 11V7.4h3c1.3 0 1.9.9 1.9 1.8s-.6 1.8-1.9 1.8h-3zM30.6 13.6L28 5.9h-1.1l-2.5 7.7-2.6-7.7H20l3.7 10H25l1.4-3.5L27.5 9l1.1 3.4 1.3 3.5h1.4l3.5-10h-1.7z" /> |
| 235 |
<path d="M14.4 5.7c-3 0-5.1 2.2-5.1 5.2 0 2.6 1.6 5.1 5.1 5.1 3.5 0 5.1-2.5 5.1-5.2-.1-2.6-1.7-5.1-5.1-5.1zm-.1 8.9c-2.5 0-3.5-1.9-3.5-3.7 0-2.2 1.2-3.8 3.5-3.8 2.4 0 3.5 2 3.5 3.8.1 2-1 3.7-3.5 3.7zM57.7 11.6h5.5v-1.5h-5.5V7.4h5.7V5.9h-7.3v10h7.3v-1.6h-5.7zM38 14.3v-2.7h5.5v-1.5H38V7.4h5.7V5.9h-7.3v10h7.3v-1.6zM93 10.3l-2.7-4.4h-1.9V6l3.8 5.8v4.1h1.6v-4.1l4-5.8v-.1h-2zM69.3 5.9h-3.8v10h3.8c3.5 0 5.1-2.5 5-5.1-.1-2.5-1.6-4.9-5-4.9zm0 8.4h-2.2V7.4h2.2c2.3 0 3.4 1.7 3.4 3.4s-1 3.5-3.4 3.5zM86.3 10.7c.9-.4 1.4-1.1 1.4-2 0-2-1.5-2.8-3.4-2.8h-4.6v10h4.6c2 0 3.7-.7 3.7-2.8 0-.8-.5-2-1.7-2.4zm-5-3.4h3c1.2 0 1.8.7 1.8 1.4 0 .8-.6 1.3-1.8 1.3h-3V7.3zm3 7.1h-3v-2.9h3c.9 0 2.1.5 2.1 1.6 0 1-1.2 1.3-2.1 1.3zM113.9 13.3h5.3V16c-1.2.9-2.9 1.1-4 1.1-4.2 0-5.6-3.3-5.6-6 0-4.1 2.2-6.1 5.6-6.1 1.4 0 3.2.4 4.8 1.8l3.4-3.4C120.7.6 118.1 0 115.2 0c-7.8 0-11.4 5.6-11.4 11s3.1 10.9 11.4 10.9c4 0 7.6-1.4 8.9-4.1V8.6h-10.2v4.7zM171.9 8.5h-7.4V.6h-5.9v20.8h5.9v-7.8h7.4v7.8h5.9V.6h-5.9zM195.1.6l-4.5 7.1-4.3-7.1h-6.6v.2l7.9 12.3v8.3h5.9v-8.3L201.8.9V.6zM127.4.6h5.9v20.8h-5.9zM147.6.6h-10.1v20.8h5.9v-5.6h4.2c5.6-.1 8.3-3.4 8.3-7.6.1-4.1-2.7-7.6-8.3-7.6zm0 10.2h-4.2V5.6h4.2c1.6 0 2.5 1.2 2.5 2.6 0 1.4-.9 2.6-2.5 2.6z" /> |
| 236 |
</svg> |
| 237 |
</PanelBody> |
| 238 |
{url ? ( |
| 239 |
<PanelBody title={__('Image settings', 'ghostkit')}> |
| 240 |
{inputFields} |
| 241 |
<TextareaControl |
| 242 |
label={__('Alt text (alternative text)')} |
| 243 |
value={alt} |
| 244 |
onChange={(val) => setAttributes({ alt: val })} |
| 245 |
help={ |
| 246 |
<> |
| 247 |
<ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree"> |
| 248 |
{__( |
| 249 |
'Describe the purpose of the image', |
| 250 |
'ghostkit' |
| 251 |
)} |
| 252 |
</ExternalLink> |
| 253 |
{__( |
| 254 |
'Leave empty if the image is purely decorative.', |
| 255 |
'ghostkit' |
| 256 |
)} |
| 257 |
</> |
| 258 |
} |
| 259 |
__nextHasNoMarginBottom |
| 260 |
/> |
| 261 |
</PanelBody> |
| 262 |
) : null} |
| 263 |
</InspectorControls> |
| 264 |
<figure {...blockProps}> |
| 265 |
{!url ? ( |
| 266 |
<Placeholder |
| 267 |
className="ghostkit-gif-placeholder" |
| 268 |
icon={getIcon('block-gif')} |
| 269 |
label={__('GIF', 'ghostkit')} |
| 270 |
instructions={__( |
| 271 |
'Search for a term or paste a Giphy URL', |
| 272 |
'ghostkit' |
| 273 |
)} |
| 274 |
> |
| 275 |
{inputFields} |
| 276 |
</Placeholder> |
| 277 |
) : ( |
| 278 |
<> |
| 279 |
<img |
| 280 |
src={url} |
| 281 |
srcSet={srcset} |
| 282 |
alt={alt} |
| 283 |
width={width} |
| 284 |
height={height} |
| 285 |
/> |
| 286 |
{(!RichText.isEmpty(caption) || isSelected) && |
| 287 |
!!url && ( |
| 288 |
<RichText |
| 289 |
inlineToolbar |
| 290 |
className="ghostkit-gif-caption" |
| 291 |
onChange={(value) => |
| 292 |
setAttributes({ caption: value }) |
| 293 |
} |
| 294 |
placeholder={__( |
| 295 |
'Write caption…', |
| 296 |
'jetpack' |
| 297 |
)} |
| 298 |
tagName="figcaption" |
| 299 |
value={caption} |
| 300 |
/> |
| 301 |
)} |
| 302 |
</> |
| 303 |
)} |
| 304 |
</figure> |
| 305 |
</> |
| 306 |
); |
| 307 |
} |
| 308 |
|