utils
1 week ago
block.json
2 months ago
edit.js
1 week ago
icon.svg
2 months ago
index.js
2 months ago
index.php
2 months ago
save.js
2 months ago
style.scss
1 week ago
edit.js
336 lines
| 1 | /* global vkBlocksVisualEmbed */ |
| 2 | /* vkBlocksVisualEmbed は PHP 側で wp_localize_script により注� |
| 3 | �されるグローバル */ |
| 4 | import { __ } from '@wordpress/i18n'; |
| 5 | import { |
| 6 | InspectorControls, |
| 7 | useBlockProps, |
| 8 | BlockControls, |
| 9 | } from '@wordpress/block-editor'; |
| 10 | |
| 11 | import { |
| 12 | PanelBody, |
| 13 | TextareaControl, |
| 14 | TextControl, |
| 15 | Notice, |
| 16 | } from '@wordpress/components'; |
| 17 | import { useEffect, useRef, useState } from '@wordpress/element'; |
| 18 | |
| 19 | import { getCssLength } from './utils/get-css-length'; |
| 20 | import { isAllowedSrc as isAllowedSrcByPatterns } from './utils/match-url-pattern'; |
| 21 | import { getYouTubePreviewData } from './utils/youtube-preview'; |
| 22 | |
| 23 | const allowedUrlPatterns = |
| 24 | typeof vkBlocksVisualEmbed !== 'undefined' && |
| 25 | vkBlocksVisualEmbed.allowedUrlPatterns |
| 26 | ? vkBlocksVisualEmbed.allowedUrlPatterns |
| 27 | : []; |
| 28 | |
| 29 | // 許可するURLパターンの� |
| 30 | �列 |
| 31 | const ALLOWED_URL_PATTERNS = [ |
| 32 | 'https://*.google.com/*', |
| 33 | 'https://*.youtube.com/embed/*', |
| 34 | 'https://www.openstreetmap.org/export/*', |
| 35 | 'https://player.vimeo.com/*', |
| 36 | ]; |
| 37 | |
| 38 | // フィルターフックを使用してURLパターンを変更 |
| 39 | const filteredAllowedUrlPatterns = [ |
| 40 | ...ALLOWED_URL_PATTERNS, |
| 41 | |
| 42 | ...allowedUrlPatterns, |
| 43 | ]; |
| 44 | |
| 45 | export default function EmbedCodeEdit({ attributes, setAttributes }) { |
| 46 | const { iframeCode, iframeWidth, iframeHeight } = attributes; |
| 47 | const [tempIframeCode, setTempIframeCode] = useState(iframeCode); |
| 48 | const prevIframeWidth = useRef(attributes.iframeWidth); |
| 49 | |
| 50 | useEffect(() => { |
| 51 | // align がユーザーによって設定されていたら変更しない |
| 52 | if (attributes.align !== undefined) { |
| 53 | return; |
| 54 | } |
| 55 | |
| 56 | const isFullWidth = String(attributes.iframeWidth) === '100%'; |
| 57 | const wasFullWidth = String(prevIframeWidth.current) === '100%'; |
| 58 | |
| 59 | // 100% から 100% 以外に変わった瞬間のみ align: "center" を適用 |
| 60 | if (wasFullWidth && !isFullWidth) { |
| 61 | setAttributes({ align: 'center' }); |
| 62 | } |
| 63 | |
| 64 | // iframeWidth の変更を記録 |
| 65 | prevIframeWidth.current = attributes.iframeWidth; |
| 66 | }, [attributes.iframeWidth]); |
| 67 | |
| 68 | useEffect(() => { |
| 69 | if (iframeWidth && isIframe) { |
| 70 | updateIframeAttributes(iframeWidth, iframeHeight); |
| 71 | } |
| 72 | }, [iframeWidth, iframeHeight]); |
| 73 | |
| 74 | // iframeを解析する関数 |
| 75 | const parseIframeCode = (code) => { |
| 76 | const parser = new window.DOMParser(); |
| 77 | const doc = parser.parseFromString(code, 'text/html'); |
| 78 | const iframe = doc.querySelector('iframe'); |
| 79 | return iframe ? iframe : false; |
| 80 | }; |
| 81 | const [isIframe, setIsIframe] = useState(!!parseIframeCode(iframeCode)); |
| 82 | |
| 83 | // 外部からの属性変更(RTC・Undo/Redo等)をローカルstateに反映 |
| 84 | useEffect(() => { |
| 85 | setTempIframeCode(iframeCode); |
| 86 | setIsIframe(!!parseIframeCode(iframeCode)); |
| 87 | }, [iframeCode]); |
| 88 | |
| 89 | const blockProps = useBlockProps({ |
| 90 | className: 'vk-visual-embed', |
| 91 | }); |
| 92 | const youtubePreviewData = getYouTubePreviewData(iframeCode); |
| 93 | const youtubePreviewStyle = { |
| 94 | width: getCssLength(iframeWidth), |
| 95 | height: getCssLength(iframeHeight), |
| 96 | }; |
| 97 | |
| 98 | // iframeのsrc属性を検証する関数(許可パターン判定は utils に切り出し済み) |
| 99 | const isAllowedSrc = (src) => |
| 100 | isAllowedSrcByPatterns(src, filteredAllowedUrlPatterns); |
| 101 | |
| 102 | // iframeタグ以外を削除する関数 |
| 103 | const sanitizeIframeCode = (code) => { |
| 104 | if (!code) { |
| 105 | return ''; |
| 106 | } |
| 107 | |
| 108 | // DOMParserが利用できない環境の場合は� |
| 109 | �力をそのまま返す |
| 110 | if (typeof window.DOMParser === 'undefined') { |
| 111 | return code; |
| 112 | } |
| 113 | |
| 114 | const iframe = parseIframeCode(code); |
| 115 | |
| 116 | if (!iframe) { |
| 117 | return ''; |
| 118 | } |
| 119 | |
| 120 | // src属性を検証 |
| 121 | const src = iframe.getAttribute('src'); |
| 122 | if (!isAllowedSrc(src)) { |
| 123 | return __('Only allowed URLs can be embedded.', 'vk-blocks'); |
| 124 | } |
| 125 | |
| 126 | return iframe.outerHTML; |
| 127 | }; |
| 128 | |
| 129 | // iframeの属性を解析して� |
| 130 | と高さを取得 |
| 131 | const extractIframeAttributes = (code) => { |
| 132 | if (typeof window.DOMParser === 'undefined') { |
| 133 | return false; |
| 134 | } |
| 135 | |
| 136 | const iframe = parseIframeCode(code); |
| 137 | |
| 138 | if (iframe) { |
| 139 | const newWidth = iframe.getAttribute('width') || iframeWidth; |
| 140 | const newHeight = iframe.getAttribute('height') || iframeHeight; |
| 141 | |
| 142 | // 抽出した値を設定パネルに反映 |
| 143 | setAttributes({ |
| 144 | iframeWidth: newWidth, |
| 145 | iframeHeight: newHeight, |
| 146 | }); |
| 147 | |
| 148 | return true; // iframeが見つかった場合はtrueを返す |
| 149 | } |
| 150 | |
| 151 | return false; // iframeが見つからない場合はfalseを返す |
| 152 | }; |
| 153 | |
| 154 | // iframeの属性を解析・更新する関数 |
| 155 | const updateIframeAttributes = (newWidth, newHeight) => { |
| 156 | if (!iframeCode || typeof window.DOMParser === 'undefined') { |
| 157 | return; |
| 158 | } |
| 159 | |
| 160 | const iframe = parseIframeCode(iframeCode); |
| 161 | |
| 162 | if (iframe) { |
| 163 | if (newWidth) { |
| 164 | iframe.setAttribute('width', newWidth); |
| 165 | } |
| 166 | if (newHeight) { |
| 167 | iframe.setAttribute('height', newHeight); |
| 168 | } |
| 169 | |
| 170 | // 更新後のiframeコードを設定 |
| 171 | setAttributes({ |
| 172 | iframeCode: iframe.outerHTML, |
| 173 | iframeWidth: newWidth || iframeWidth, |
| 174 | iframeHeight: newHeight || iframeHeight, |
| 175 | }); |
| 176 | setTempIframeCode(iframe.outerHTML); |
| 177 | } |
| 178 | }; |
| 179 | |
| 180 | return ( |
| 181 | <div {...blockProps}> |
| 182 | <BlockControls /> |
| 183 | <InspectorControls> |
| 184 | <PanelBody title={__('Embed Code Settings', 'vk-blocks')}> |
| 185 | <TextareaControl |
| 186 | label={__('Embed Code', 'vk-blocks')} |
| 187 | value={tempIframeCode} |
| 188 | onChange={(newCode) => { |
| 189 | setTempIframeCode(newCode); |
| 190 | }} |
| 191 | onBlur={() => { |
| 192 | if (!tempIframeCode) { |
| 193 | setAttributes({ iframeCode: '' }); |
| 194 | setIsIframe(false); |
| 195 | return; |
| 196 | } |
| 197 | const sanitizedCode = |
| 198 | sanitizeIframeCode(tempIframeCode); |
| 199 | setAttributes({ iframeCode: sanitizedCode }); |
| 200 | if (sanitizedCode) { |
| 201 | extractIframeAttributes(sanitizedCode); |
| 202 | } |
| 203 | |
| 204 | setIsIframe(!!parseIframeCode(sanitizedCode)); |
| 205 | setTempIframeCode(sanitizedCode); |
| 206 | }} |
| 207 | help={__( |
| 208 | 'Please paste the iframe embed code directly. Only iframe tags with allowed URLs (Google Maps, Google Calendar, Google Forms, YouTube、OpenStreetMap, Vimeo) are permitted.', |
| 209 | 'vk-blocks' |
| 210 | )} |
| 211 | /> |
| 212 | {!iframeCode && ( |
| 213 | <Notice |
| 214 | status="error" |
| 215 | isDismissible={false} |
| 216 | className="vk-visual-embed_notice" |
| 217 | > |
| 218 | {__( |
| 219 | 'Please enter an iframe embed code.', |
| 220 | 'vk-blocks' |
| 221 | )} |
| 222 | </Notice> |
| 223 | )} |
| 224 | {iframeCode && !sanitizeIframeCode(iframeCode) && ( |
| 225 | <Notice |
| 226 | status="error" |
| 227 | isDismissible={false} |
| 228 | className="vk-visual-embed_notice" |
| 229 | > |
| 230 | {__( |
| 231 | 'The provided URL is not allowed. Please use an approved embed source.', |
| 232 | 'vk-blocks' |
| 233 | )} |
| 234 | </Notice> |
| 235 | )} |
| 236 | <TextControl |
| 237 | label={__('Iframe Width', 'vk-blocks')} |
| 238 | value={iframeWidth} |
| 239 | onChange={(newWidth) => { |
| 240 | setAttributes({ iframeWidth: newWidth }); |
| 241 | }} |
| 242 | onBlur={() => { |
| 243 | if (!iframeWidth) { |
| 244 | extractIframeAttributes(iframeCode); |
| 245 | return; |
| 246 | } |
| 247 | if (/^\d+(px|%)?$/.test(iframeWidth)) { |
| 248 | updateIframeAttributes( |
| 249 | iframeWidth, |
| 250 | iframeHeight |
| 251 | ); |
| 252 | } else { |
| 253 | setAttributes({ iframeWidth: '' }); |
| 254 | updateIframeAttributes('', iframeHeight); |
| 255 | } |
| 256 | }} |
| 257 | disabled={!isIframe} |
| 258 | /> |
| 259 | <TextControl |
| 260 | label={__('Iframe Height', 'vk-blocks')} |
| 261 | value={iframeHeight} |
| 262 | onChange={(newHeight) => { |
| 263 | setAttributes({ iframeHeight: newHeight }); |
| 264 | }} |
| 265 | onBlur={() => { |
| 266 | if (!iframeHeight) { |
| 267 | extractIframeAttributes(iframeCode); |
| 268 | return; |
| 269 | } |
| 270 | if (/^\d+(px|%)?$/.test(iframeHeight)) { |
| 271 | updateIframeAttributes( |
| 272 | iframeWidth, |
| 273 | iframeHeight |
| 274 | ); |
| 275 | } else { |
| 276 | setAttributes({ iframeHeight: '' }); |
| 277 | updateIframeAttributes(iframeWidth, ''); |
| 278 | } |
| 279 | }} |
| 280 | disabled={!isIframe} |
| 281 | /> |
| 282 | {!isIframe && ( |
| 283 | <Notice status="warning" isDismissible={false}> |
| 284 | {__( |
| 285 | 'Note: These settings are only applicable to iframe tags. Other embed codes will not respond to these adjustments.', |
| 286 | 'vk-blocks' |
| 287 | )} |
| 288 | </Notice> |
| 289 | )} |
| 290 | </PanelBody> |
| 291 | </InspectorControls> |
| 292 | <div style={{ position: 'relative' }}> |
| 293 | {iframeCode && youtubePreviewData && ( |
| 294 | <div |
| 295 | className="vk-visual-embed-preview vk-visual-embed-preview--youtube" |
| 296 | style={youtubePreviewStyle} |
| 297 | title={__( |
| 298 | 'Preview only. The video plays on the published page.', |
| 299 | 'vk-blocks' |
| 300 | )} |
| 301 | > |
| 302 | <img |
| 303 | className="vk-visual-embed-preview__youtube-thumbnail" |
| 304 | src={youtubePreviewData.thumbnailUrl} |
| 305 | alt={__('YouTube video preview', 'vk-blocks')} |
| 306 | decoding="async" |
| 307 | /> |
| 308 | <span |
| 309 | className="vk-visual-embed-preview__youtube-play" |
| 310 | aria-hidden="true" |
| 311 | > |
| 312 | <svg |
| 313 | className="vk-visual-embed-preview__youtube-play-icon" |
| 314 | viewBox="0 0 17 20" |
| 315 | xmlns="http://www.w3.org/2000/svg" |
| 316 | focusable="false" |
| 317 | > |
| 318 | <path d="M0 0 L17 10 L0 20 Z" /> |
| 319 | </svg> |
| 320 | </span> |
| 321 | </div> |
| 322 | )} |
| 323 | {iframeCode && !youtubePreviewData && ( |
| 324 | <div |
| 325 | className="vk-visual-embed-preview" |
| 326 | dangerouslySetInnerHTML={{ __html: iframeCode }} |
| 327 | style={{ |
| 328 | pointerEvents: 'none', |
| 329 | }} |
| 330 | /> |
| 331 | )} |
| 332 | </div> |
| 333 | </div> |
| 334 | ); |
| 335 | } |
| 336 |