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