PluginProbe ʕ •ᴥ•ʔ
VK Blocks / 1.123.0
VK Blocks v1.123.0
1.123.0 1.122.0 1.121.0 1.120.0 1.119.2 1.119.1 1.119.0 1.118.7 1.82.0.0 1.82.0.1 1.83.0.0 1.83.0.1 1.84.0.0 1.84.0.1 1.85.0.2 1.85.0.3 1.85.1.0 1.85.1.1 1.86.0.0 1.86.0.1 1.86.1.0 1.87.0.0 1.87.0.1 1.88.0.1 1.88.0.2 1.89.0.0 1.9.2 1.90.0.0 1.90.0.1 1.90.1.1 1.91.0.0 1.91.1.1 1.92.0.0 1.92.0.1 1.92.1.0 1.92.1.1 1.93.0.0 trunk 1.93.0.1 0.1.1 1.93.1.0 0.10.3 1.93.1.1 0.11.0 1.94.0.0 0.12.6 1.94.0.1 0.15.1 1.94.1.0 0.16.2 1.94.2.1 0.17.6 1.94.2.2 0.2.2 1.95.0.3 0.22.4 1.95.0.4 0.26.7 1.96.2.0 0.3.2 1.96.2.1 0.31.0 1.97.0.1 0.35.5 1.97.0.2 0.37.5 1.99.0.0 0.38.8 1.99.0.1 0.39.4 0.4.7 0.41.0 0.42.0 0.44.13 0.45.2 0.46.1 0.47.0 0.48.0 0.49.8 0.5.2 0.50.3 0.51.0 0.52.2 0.53.2 0.54.0 0.55.0 0.56.2 0.56.3 0.57.6 0.58.10 0.59.0 0.6.0 0.60.1 0.61.2 0.7.1 0.8.2 1.0.16 1.10.0 1.100.0.0 1.100.0.1 1.102.0.0 1.102.0.1 1.103.0.0 1.104.0.0 1.104.0.1 1.105.1.0 1.105.1.1 1.106.0.0 1.106.0.1 1.107.0.0 1.107.0.1 1.107.0.2 1.108.0.0 1.108.0.1 1.109.0.0 1.109.0.1 1.11.4 1.110.0.0 1.110.0.1 1.111.0.0 1.111.0.1 1.111.0.2 1.112.0.0 1.112.0.1 1.113.0.0 1.113.0.1 1.114.0.0 1.114.0.1 1.114.2.0 1.114.2.1 1.115.0.0 1.115.0.1 1.115.1.0 1.115.1.1 1.115.2.0 1.115.2.1 1.116.0.0 1.116.0.1 1.116.1.0 1.116.1.1 1.116.2.0 1.117.0.0 1.117.0.1 1.117.1.0 1.118.2 1.118.5 1.12.0 1.13.2 1.14.1 1.15.1 1.16.11 1.17.0 1.18.6 1.19.0 1.19.1 1.2.3 1.20.7 1.21.0 1.22.4 1.23.0 1.24.5 1.25.1 1.26.2 1.27.7.2 1.28.0.1 1.29.2.0 1.3.9 1.30.0.1 1.31.0.1 1.32.0.2 1.33.2.1 1.36.1.5 1.37.0.0 1.39.2.1 1.4.6 1.40.0.1 1.40.1.0 1.40.1.1 1.41.0.0 1.41.0.1 1.41.2.2 1.41.2.3 1.43.0.0 1.43.0.1 1.43.0.2 1.44.0.0 1.44.0.1 1.45.0.0 1.45.0.1 1.46.0.0 1.46.0.1 1.47.0.0 1.47.0.1 1.47.1.0 1.48.0.0 1.48.0.1 1.48.0.2 1.48.1.0 1.48.1.1 1.5.0 1.50.0.0 1.50.0.1 1.51.0.0 1.51.0.1 1.52.0.0 1.52.0.1 1.53.0.0 1.53.0.1 1.54.0.0 1.54.0.1 1.55.0.0 1.55.0.1 1.56.0.0 1.56.0.1 1.57.0.0 1.57.0.2 1.57.0.3 1.57.0.4 1.57.0.5 1.57.1.0 1.57.1.1 1.57.1.2 1.58.0.0 1.58.0.1 1.59.0.0 1.59.0.1 1.6.0 1.60.0.0 1.60.0.1 1.63.0.0 1.63.0.1 1.64.0.0 1.64.0.1 1.64.1.0 1.64.1.2 1.67.0.0 1.67.0.1 1.68.0.0 1.68.0.1 1.69.0.0 1.69.0.1 1.69.1.1 1.69.1.2 1.7.1 1.70.0.0 1.70.0.1 1.71.0.0 1.71.0.1 1.72.0.0 1.72.1.0 1.72.1.1 1.73.0.0 1.73.0.1 1.74.0.0 1.74.0.1 1.75.0.0 1.75.1.0 1.75.1.1 1.76.0.0 1.76.0.1 1.76.1.0 1.76.1.1 1.76.2.0 1.76.2.1 1.77.0.0 1.77.0.1 1.78.0.0 1.78.0.1 1.79.0.0 1.79.0.1 1.79.0.2 1.79.0.3 1.79.1.0 1.79.1.1 1.8.2 1.80.1.0 1.80.1.1 1.80.1.2 1.81.0.1 1.81.0.2
vk-blocks / src / extensions / core / group / style.js
vk-blocks / src / extensions / core / group Last commit date
deprecated 2 weeks ago style.js 2 months ago style.scss 2 months ago view.js 2 weeks ago
style.js
1296 lines
1 /**
2 * group-style block type
3 *
4 * @package
5 */
6 import { convertColorClass } from '@vkblocks/utils/color-code-to-class.js';
7 import { __ } from '@wordpress/i18n';
8 import { addFilter } from '@wordpress/hooks';
9 import {
10 PanelBody,
11 ToolbarGroup,
12 ToggleControl,
13 BaseControl,
14 } from '@wordpress/components';
15 import {
16 InspectorControls,
17 ColorPalette,
18 BlockControls,
19 useBlockProps,
20 InnerBlocks,
21 } from '@wordpress/block-editor';
22 import { createHigherOrderComponent } from '@wordpress/compose';
23 import { useSelect } from '@wordpress/data';
24 import { Fragment, useEffect, useRef } from '@wordpress/element';
25 import { VkPanelIcon } from '@vkblocks/components/vk-icon';
26 import LinkToolbar from '@vkblocks/components/link-toolbar';
27 import HorizontalScrollControls, {
28 scrollbarAttributes,
29 buildScrollDataProps,
30 SCROLL_DATA_ATTR_KEYS,
31 } from '@vkblocks/utils/horizontal-scroll-controls';
32 import deprecated from './deprecated/index';
33
34 /**
35 * Check if the block type is valid for customization.
36 *
37 * @param {string} name The name of the block type.
38 * @return {boolean} Whether the block type is valid.
39 */
40 const isValidBlockType = (name) => {
41 const validBlockTypes = ['core/group'];
42 return validBlockTypes.includes(name);
43 };
44
45 const groupScrollableTargetBlocks = [
46 'core/group',
47 'core/columns',
48 'core/gallery',
49 'core/social-links',
50 'core/buttons',
51 'vk-blocks/grid-column',
52 'vk-blocks/gridcolcard',
53 'vk-blocks/button-outer',
54 'vk-blocks/icon-outer',
55 'vk-blocks/card',
56 'vk-blocks/icon-card',
57 ];
58
59 // ユーザー向けに表示するブロック名のリスト(core/groupは除外)
60 const displayableScrollableTargetBlocks = groupScrollableTargetBlocks.filter(
61 (blockName) => blockName !== 'core/group'
62 );
63
64 const isFlexGridGroupBlock = (block) => {
65 if (block.name !== 'core/group') {
66 return false;
67 }
68 const layoutType = block.attributes?.layout?.type;
69 return layoutType === 'grid' || layoutType === 'flex';
70 };
71
72 const isGroupScrollableAllowedBlock = (block) => {
73 if (isFlexGridGroupBlock(block)) {
74 return true;
75 }
76 if (groupScrollableTargetBlocks.includes(block.name)) {
77 return true;
78 }
79 if (block?.name !== 'core/group') {
80 return false;
81 }
82 return (
83 hasOnlyGroupScrollableTargetBlocks(block.innerBlocks) ||
84 containsFlexGridGroupBlocks(block.innerBlocks)
85 );
86 };
87
88 const hasOnlyGroupScrollableTargetBlocks = (blocks = []) => {
89 if (blocks.length === 0) {
90 return false;
91 }
92 return blocks.every((block) => isGroupScrollableAllowedBlock(block));
93 };
94
95 const containsFlexGridGroupBlocks = (blocks = []) =>
96 blocks.some(
97 (block) =>
98 isFlexGridGroupBlock(block) ||
99 (block.innerBlocks?.length &&
100 containsFlexGridGroupBlocks(block.innerBlocks))
101 );
102
103 /**
104 * Add custom attributes to the block settings.
105 *
106 * @param {Object} settings The block settings.
107 * @return {Object} The modified block settings.
108 */
109 export const addAttribute = (settings) => {
110 if (isValidBlockType(settings.name)) {
111 settings.attributes = {
112 ...settings.attributes,
113 color: {
114 type: 'string',
115 default: '',
116 },
117 linkUrl: {
118 type: 'string',
119 default: '',
120 },
121 linkTarget: {
122 type: 'string',
123 default: '',
124 },
125 relAttribute: {
126 type: 'string',
127 default: '',
128 },
129 linkDescription: {
130 type: 'string',
131 default: '',
132 },
133 linkToPost: {
134 type: 'boolean',
135 default: false,
136 },
137 tagName: {
138 type: 'string',
139 default: 'div',
140 },
141 scrollable: {
142 type: 'boolean',
143 },
144 scrollableAutoDisabled: {
145 type: 'boolean',
146 default: false,
147 },
148 scrollBreakpoint: {
149 type: 'string',
150 default: 'group-scrollable-mobile',
151 },
152 showScrollMessage: {
153 type: 'boolean',
154 default: false,
155 },
156 scrollMessageText: {
157 type: 'string',
158 default: __('You can scroll', 'vk-blocks'),
159 },
160 scrollIconLeft: {
161 type: 'string',
162 default: 'fa-solid fa-caret-left',
163 },
164 scrollIconRight: {
165 type: 'string',
166 default: 'fa-solid fa-caret-right',
167 },
168 iconOutputLeft: {
169 type: 'boolean',
170 default: true,
171 },
172 iconOutputRight: {
173 type: 'boolean',
174 default: true,
175 },
176 textNoWrap: {
177 type: 'boolean',
178 default: true,
179 },
180 tableMode: {
181 type: 'boolean',
182 default: false,
183 },
184 ...scrollbarAttributes,
185 };
186 if (settings.name === 'core/group') {
187 settings.usesContext = [
188 ...(settings.usesContext || []),
189 'postId',
190 'postType',
191 'queryId',
192 ];
193 }
194 }
195 return settings;
196 };
197 addFilter('blocks.registerBlockType', 'vk-blocks/group-style', addAttribute);
198
199 /**
200 * Add custom controls to the block edit interface.
201 *
202 * @param {Function} BlockEdit The block edit component.
203 * @return {Function} The modified block edit component.
204 */
205 export const addBlockControl = createHigherOrderComponent((BlockEdit) => {
206 let activeColor = '';
207
208 return (props) => {
209 const { attributes, setAttributes, name, clientId, context } = props;
210 if (!isValidBlockType(name)) {
211 return <BlockEdit {...props} />;
212 }
213 const isDescendentOfQueryLoop =
214 typeof context?.queryId === 'number' &&
215 Number.isFinite(context.queryId);
216 const {
217 scrollable,
218 scrollableAutoDisabled,
219 scrollBreakpoint,
220 showScrollMessage,
221 scrollMessageText,
222 scrollIconLeft,
223 scrollIconRight,
224 iconOutputLeft,
225 iconOutputRight,
226 textNoWrap,
227 tableMode,
228 scrollbarVisible,
229 scrollbarColor,
230 scrollbarTrackColor,
231 } = attributes || {};
232
233 // レイアウトタイプを取得
234 const layoutType = attributes?.layout?.type;
235 const isGridOrFlexLayout =
236 layoutType === 'grid' || layoutType === 'flex';
237
238 // カラムブロックの数をカウント(再帰的にチェック)
239 const countColumnBlocks = (blocks) =>
240 blocks.reduce(
241 (count, block) =>
242 count +
243 (block.name === 'core/columns' ? 1 : 0) +
244 (block.innerBlocks?.length
245 ? countColumnBlocks(block.innerBlocks)
246 : 0),
247 0
248 );
249
250 // 子ブロックを取得してカラムブロックの数をカウント
251 const columnBlockCount = useSelect(
252 (select) => {
253 const block = select('core/block-editor').getBlock(clientId);
254 if (!block || !block.innerBlocks) {
255 return 0;
256 }
257 return countColumnBlocks(block.innerBlocks);
258 },
259 [clientId]
260 );
261
262 const { hasScrollableTargetBlocks } = useSelect(
263 (select) => {
264 const block = select('core/block-editor').getBlock(clientId);
265 if (!block || !block.innerBlocks) {
266 return {
267 hasScrollableTargetBlocks: false,
268 };
269 }
270 return {
271 hasScrollableTargetBlocks:
272 hasOnlyGroupScrollableTargetBlocks(block.innerBlocks),
273 };
274 },
275 [clientId, layoutType]
276 );
277 const hasScrollableParentGroup = useSelect(
278 (select) => {
279 const parentIds =
280 select('core/block-editor').getBlockParents(clientId) || [];
281 if (!parentIds.length) {
282 return false;
283 }
284 return parentIds.some((parentId) => {
285 const parent =
286 select('core/block-editor').getBlock(parentId);
287 return (
288 parent?.name === 'core/group' &&
289 parent.attributes?.scrollable === true
290 );
291 });
292 },
293 [clientId]
294 );
295 // ブロックタイプの�
296 報を取得して、ブロック名を人間が読みやすい形式に変換
297 const blockTypes = useSelect((select) => {
298 const { getBlockTypes } = select('core/blocks');
299 return getBlockTypes();
300 }, []);
301 // ブロック名から人間が読みやすいタイトルを取得する関数
302 const getBlockTitle = (blockName) => {
303 const blockType = blockTypes.find(
304 (type) => type.name === blockName
305 );
306 return blockType?.title || blockName;
307 };
308 const isScrollableTargetEligible =
309 !isGridOrFlexLayout && hasScrollableTargetBlocks;
310 const shouldShowScrollablePanel = !isGridOrFlexLayout;
311 const shouldShowUnsupportedNotice =
312 shouldShowScrollablePanel && !isScrollableTargetEligible;
313 const shouldAutoDisableForUnsupported =
314 !isGridOrFlexLayout && !hasScrollableTargetBlocks;
315
316 // 複数のカラムブロックがあるかどうか
317 const hasMultipleColumns = columnBlockCount >= 2;
318 const previousScrollableRef = useRef(scrollable);
319 const userToggledScrollableRef = useRef(false);
320
321 useEffect(() => {
322 if (!isScrollableTargetEligible && scrollable) {
323 setAttributes({
324 scrollable: false,
325 scrollableAutoDisabled: shouldAutoDisableForUnsupported,
326 showScrollMessage: false,
327 scrollBreakpoint: 'group-scrollable-mobile',
328 tableMode: false,
329 });
330 }
331 }, [
332 isScrollableTargetEligible,
333 scrollable,
334 shouldAutoDisableForUnsupported,
335 setAttributes,
336 ]);
337 useEffect(() => {
338 if (
339 isScrollableTargetEligible &&
340 !scrollable &&
341 scrollableAutoDisabled
342 ) {
343 setAttributes({
344 scrollable: true,
345 scrollableAutoDisabled: false,
346 });
347 }
348 }, [
349 isScrollableTargetEligible,
350 scrollable,
351 scrollableAutoDisabled,
352 setAttributes,
353 ]);
354 useEffect(() => {
355 if (userToggledScrollableRef.current) {
356 userToggledScrollableRef.current = false;
357 previousScrollableRef.current = scrollable;
358 return;
359 }
360 const previousScrollable = previousScrollableRef.current;
361 if (
362 previousScrollable &&
363 !scrollable &&
364 !isScrollableTargetEligible &&
365 shouldAutoDisableForUnsupported &&
366 !scrollableAutoDisabled
367 ) {
368 setAttributes({ scrollableAutoDisabled: true });
369 }
370 previousScrollableRef.current = scrollable;
371 }, [
372 isScrollableTargetEligible,
373 scrollable,
374 scrollableAutoDisabled,
375 shouldAutoDisableForUnsupported,
376 setAttributes,
377 ]);
378
379 // カラムブロックの数が変わった時に tableMode を自動調整
380 useEffect(() => {
381 if (!scrollable || tableMode !== true) {
382 return;
383 }
384 if (!hasMultipleColumns || isGridOrFlexLayout) {
385 // カラムブロックが1つ以下、またはグリッド/フレックスの場合は無効化
386 setAttributes({ tableMode: false });
387 }
388 }, [
389 columnBlockCount,
390 tableMode,
391 hasMultipleColumns,
392 isGridOrFlexLayout,
393 scrollable,
394 setAttributes,
395 ]);
396
397 // レイアウトが flex/grid に変わったら scrollable を明示OFF(ヒントもOFF)
398 useEffect(() => {
399 if (isGridOrFlexLayout && scrollable) {
400 setAttributes({
401 scrollable: false,
402 showScrollMessage: false,
403 });
404 }
405 }, [isGridOrFlexLayout, scrollable, setAttributes]);
406
407 // グリッド/フレックスレイアウトの場合は textNoWrap を false に設定
408 useEffect(() => {
409 if (isGridOrFlexLayout && textNoWrap !== false && scrollable) {
410 setAttributes({ textNoWrap: false });
411 }
412 }, [isGridOrFlexLayout, textNoWrap, scrollable, setAttributes]);
413
414 // スクロールヒントをエディタ�
415 に表示(BlockEdit の前に�
416 �置)。
417 // BlockListBlock HOC はクラス名のみを追加し、ヒント要素は BlockEdit �
418 でレンダリング。
419 // これにより、二重ラップを避けつつ、エディタ�
420 でのみヒントを表示。
421 // ヒントをコンテンツの前に置くのは意図的(編集時に「このブロックは横スクロール可能」を�
422 �に示すため)。
423 const scrollHintInEditor =
424 scrollable && showScrollMessage ? (
425 <div
426 className="vk-scroll-hint"
427 data-scroll-breakpoint={scrollBreakpoint}
428 data-icon-output-left={iconOutputLeft ? 'true' : undefined}
429 data-icon-output-right={
430 iconOutputRight ? 'true' : undefined
431 }
432 data-hint-icon-left={
433 iconOutputLeft ? scrollIconLeft : undefined
434 }
435 data-hint-icon-right={
436 iconOutputRight ? scrollIconRight : undefined
437 }
438 >
439 {iconOutputLeft && (
440 <i className={`${scrollIconLeft} left-icon`} />
441 )}
442 <span>{scrollMessageText}</span>
443 {iconOutputRight && (
444 <i className={`${scrollIconRight} right-icon`} />
445 )}
446 </div>
447 ) : null;
448
449 if (isValidBlockType(name) && props.isSelected) {
450 if (attributes.color) {
451 activeColor = attributes.color;
452 } else {
453 activeColor = '#fffd6b';
454 }
455
456 // スクロール可能トグル変更のハンドル
457 const handleToggleChange = (checked) => {
458 userToggledScrollableRef.current = true;
459 setAttributes({
460 scrollable: checked,
461 scrollableAutoDisabled: false,
462 });
463
464 if (!checked) {
465 setAttributes({
466 showScrollMessage: false,
467 scrollBreakpoint: 'group-scrollable-mobile',
468 scrollbarVisible: true,
469 scrollbarColor: '',
470 scrollbarTrackColor: '',
471 });
472 }
473 };
474
475 // ブレークポイント選択変更のハンドル
476 const handleSelectChange = (value) => {
477 setAttributes({ scrollBreakpoint: value });
478 };
479
480 return (
481 <Fragment>
482 {scrollHintInEditor}
483 <BlockEdit {...props} />
484 <BlockControls>
485 <ToolbarGroup>
486 <LinkToolbar
487 linkUrl={attributes.linkUrl}
488 setLinkUrl={(url) =>
489 setAttributes({ linkUrl: url })
490 }
491 linkTarget={attributes.linkTarget}
492 setLinkTarget={(target) =>
493 setAttributes({ linkTarget: target })
494 }
495 relAttribute={attributes.relAttribute}
496 setRelAttribute={(rel) =>
497 setAttributes({ relAttribute: rel })
498 }
499 linkDescription={attributes.linkDescription}
500 setLinkDescription={(description) =>
501 setAttributes({
502 linkDescription: description,
503 })
504 }
505 isDescendentOfQueryLoop={
506 isDescendentOfQueryLoop
507 }
508 linkToPost={attributes.linkToPost}
509 setLinkToPost={(checked) =>
510 setAttributes({ linkToPost: !!checked })
511 }
512 />
513 </ToolbarGroup>
514 </BlockControls>
515 <InspectorControls>
516 <PanelBody
517 title={__('Border Color', 'vk-blocks')}
518 initialOpen={false}
519 className="group-border-color-controle"
520 >
521 <p className="font-size-11px alert alert-danger">
522 {__(
523 'Because of the theme that enabled theme.json become can specify the color from border panel that, specification from here is deprecated.',
524 'vk-blocks'
525 )}
526 </p>
527 <ColorPalette
528 value={activeColor}
529 disableCustomColors={true}
530 onChange={(newColor) => {
531 let newClassName =
532 convertColorClass(newColor);
533
534 if (attributes.className) {
535 let inputClassName =
536 attributes.className;
537
538 inputClassName =
539 inputClassName.split(' ');
540
541 const filterClassName =
542 inputClassName.filter(
543 function (name) {
544 return (
545 -1 ===
546 name.indexOf('vk-has-')
547 );
548 }
549 );
550
551 filterClassName.push(newClassName);
552
553 newClassName =
554 filterClassName.join(' ');
555 }
556
557 activeColor = newColor;
558 setAttributes({
559 className: newClassName,
560 color: newColor,
561 });
562 }}
563 />
564 </PanelBody>
565 {shouldShowScrollablePanel && (
566 <PanelBody
567 title={__(
568 'Group Horizontal Scroll',
569 'vk-blocks'
570 )}
571 icon={<VkPanelIcon isActive={scrollable} />}
572 initialOpen={false}
573 >
574 {hasScrollableParentGroup && (
575 <div className="alert alert-warning">
576 {__(
577 'This Group is inside a parent Group with horizontal scroll enabled. The parent scroll setting takes precedence.',
578 'vk-blocks'
579 )}
580 </div>
581 )}
582 {shouldShowUnsupportedNotice && (
583 <div className="alert alert-warning">
584 <p>
585 {__(
586 'This setting applies only when all direct child blocks are the following block.',
587 'vk-blocks'
588 )}
589 </p>
590 <ul>
591 {displayableScrollableTargetBlocks.map(
592 (blockName) => (
593 <li key={blockName}>
594 {getBlockTitle(
595 blockName
596 )}
597 </li>
598 )
599 )}
600 </ul>
601 <p>
602 {__(
603 'Groups are supported only when their layout is Flex/Grid or when their direct inner blocks are supported.',
604 'vk-blocks'
605 )}
606 </p>
607 </div>
608 )}
609 {!shouldShowUnsupportedNotice && (
610 <HorizontalScrollControls
611 scrollable={scrollable}
612 scrollBreakpoint={scrollBreakpoint}
613 onScrollableChange={handleToggleChange}
614 onBreakpointChange={handleSelectChange}
615 prefix="group-scrollable-"
616 scrollbarVisible={scrollbarVisible}
617 scrollbarColor={scrollbarColor}
618 scrollbarTrackColor={
619 scrollbarTrackColor
620 }
621 onScrollbarVisibleChange={(checked) => {
622 setAttributes({
623 scrollbarVisible: checked,
624 });
625 }}
626 onScrollbarColorChange={(color) => {
627 setAttributes({
628 scrollbarColor: color || '',
629 });
630 }}
631 onScrollbarTrackColorChange={(
632 color
633 ) => {
634 setAttributes({
635 scrollbarTrackColor:
636 color || '',
637 });
638 }}
639 description={
640 <p>
641 {__(
642 'Enable horizontal scrolling for all blocks inside this group.',
643 'vk-blocks'
644 )}
645 </p>
646 }
647 scrollHintProps={{
648 showScrollMessage,
649 scrollMessageText,
650 scrollIconLeft,
651 scrollIconRight,
652 ...props,
653 }}
654 >
655 <ToggleControl
656 label={__(
657 'Prevent text wrapping',
658 'vk-blocks'
659 )}
660 checked={textNoWrap !== false}
661 onChange={(checked) => {
662 setAttributes({
663 textNoWrap: checked,
664 });
665 }}
666 disabled={
667 tableMode === true ||
668 isGridOrFlexLayout
669 }
670 help={
671 tableMode === true ||
672 isGridOrFlexLayout
673 ? __(
674 'This option is disabled when Table Mode is enabled or when using Grid/Flex layouts.',
675 'vk-blocks'
676 )
677 : __(
678 'Prevents text from wrapping to multiple lines and ensures horizontal scrolling. Disable this only if you want text to wrap naturally within the scrollable area.',
679 'vk-blocks'
680 )
681 }
682 />
683 <ToggleControl
684 label={__(
685 'Table Mode (Core Column Blocks Only)',
686 'vk-blocks'
687 )}
688 checked={tableMode === true}
689 onChange={(checked) => {
690 setAttributes({
691 tableMode: checked,
692 });
693 }}
694 disabled={
695 !hasMultipleColumns ||
696 isGridOrFlexLayout
697 }
698 help={
699 hasMultipleColumns
700 ? __(
701 'Unifies column widths across multiple rows. Only works with core Column blocks.',
702 'vk-blocks'
703 )
704 : __(
705 'This option is only available when there are multiple core Column blocks.',
706 'vk-blocks'
707 )
708 }
709 />
710 {hasMultipleColumns && (
711 <BaseControl>
712 <div
713 style={{
714 display: 'flex',
715 gap: '16px',
716 marginTop: '12px',
717 fontSize: '12px',
718 }}
719 >
720 <div style={{ flex: 1 }}>
721 <div
722 style={{
723 marginBottom:
724 '8px',
725 fontWeight: 600,
726 color: '#757575',
727 }}
728 >
729 {__(
730 'OFF',
731 'vk-blocks'
732 )}
733 </div>
734 <div
735 style={{
736 display: 'flex',
737 flexDirection:
738 'column',
739 gap: '4px',
740 }}
741 >
742 <div
743 style={{
744 display:
745 'flex',
746 gap: '4px',
747 }}
748 >
749 <div
750 style={{
751 background:
752 '#e0e0e0',
753 padding:
754 '8px',
755 borderRadius:
756 '4px',
757 flex: 1,
758 textAlign:
759 'center',
760 fontSize:
761 '10px',
762 }}
763 >
764 {__(
765 'Col 1',
766 'vk-blocks'
767 )}
768 </div>
769 <div
770 style={{
771 background:
772 '#e0e0e0',
773 padding:
774 '8px',
775 borderRadius:
776 '4px',
777 flex: 2,
778 textAlign:
779 'center',
780 fontSize:
781 '10px',
782 }}
783 >
784 {__(
785 'Col 2',
786 'vk-blocks'
787 )}
788 </div>
789 </div>
790 <div
791 style={{
792 display:
793 'flex',
794 gap: '4px',
795 }}
796 >
797 <div
798 style={{
799 background:
800 '#e0e0e0',
801 padding:
802 '8px',
803 borderRadius:
804 '4px',
805 flex: 2,
806 textAlign:
807 'center',
808 fontSize:
809 '10px',
810 }}
811 >
812 {__(
813 'Col 1',
814 'vk-blocks'
815 )}
816 </div>
817 <div
818 style={{
819 background:
820 '#e0e0e0',
821 padding:
822 '8px',
823 borderRadius:
824 '4px',
825 flex: 1,
826 textAlign:
827 'center',
828 fontSize:
829 '10px',
830 }}
831 >
832 {__(
833 'Col 2',
834 'vk-blocks'
835 )}
836 </div>
837 </div>
838 </div>
839 <div
840 style={{
841 marginTop:
842 '8px',
843 fontSize:
844 '11px',
845 color: '#757575',
846 textAlign:
847 'center',
848 }}
849 >
850 {__(
851 'Different widths',
852 'vk-blocks'
853 )}
854 </div>
855 </div>
856 <div style={{ flex: 1 }}>
857 <div
858 style={{
859 marginBottom:
860 '8px',
861 fontWeight: 600,
862 color: '#757575',
863 }}
864 >
865 {__(
866 'ON',
867 'vk-blocks'
868 )}
869 </div>
870 <div
871 style={{
872 display: 'flex',
873 flexDirection:
874 'column',
875 gap: '4px',
876 }}
877 >
878 <div
879 style={{
880 display:
881 'flex',
882 gap: '4px',
883 }}
884 >
885 <div
886 style={{
887 background:
888 'var(--vk-color-primary)',
889 padding:
890 '8px',
891 borderRadius:
892 '4px',
893 flex: 2,
894 textAlign:
895 'center',
896 fontSize:
897 '10px',
898 color: '#fff',
899 }}
900 >
901 {__(
902 'Col 1',
903 'vk-blocks'
904 )}
905 </div>
906 <div
907 style={{
908 background:
909 'var(--vk-color-primary)',
910 padding:
911 '8px',
912 borderRadius:
913 '4px',
914 flex: 2,
915 textAlign:
916 'center',
917 fontSize:
918 '10px',
919 color: '#fff',
920 }}
921 >
922 {__(
923 'Col 2',
924 'vk-blocks'
925 )}
926 </div>
927 </div>
928 <div
929 style={{
930 display:
931 'flex',
932 gap: '4px',
933 }}
934 >
935 <div
936 style={{
937 background:
938 'var(--vk-color-primary)',
939 padding:
940 '8px',
941 borderRadius:
942 '4px',
943 flex: 2,
944 textAlign:
945 'center',
946 fontSize:
947 '10px',
948 color: '#fff',
949 }}
950 >
951 {__(
952 'Col 1',
953 'vk-blocks'
954 )}
955 </div>
956 <div
957 style={{
958 background:
959 'var(--vk-color-primary)',
960 padding:
961 '8px',
962 borderRadius:
963 '4px',
964 flex: 2,
965 textAlign:
966 'center',
967 fontSize:
968 '10px',
969 color: '#fff',
970 }}
971 >
972 {__(
973 'Col 2',
974 'vk-blocks'
975 )}
976 </div>
977 </div>
978 </div>
979 <div
980 style={{
981 marginTop:
982 '8px',
983 fontSize:
984 '11px',
985 color: '#757575',
986 textAlign:
987 'center',
988 }}
989 >
990 {__(
991 'Unified to max width',
992 'vk-blocks'
993 )}
994 </div>
995 </div>
996 </div>
997 </BaseControl>
998 )}
999 </HorizontalScrollControls>
1000 )}
1001 </PanelBody>
1002 )}
1003 </InspectorControls>
1004 </Fragment>
1005 );
1006 }
1007 return (
1008 <Fragment>
1009 {scrollHintInEditor}
1010 <BlockEdit {...props} />
1011 </Fragment>
1012 );
1013 };
1014 }, 'addMyCustomBlockControls');
1015
1016 addFilter('editor.BlockEdit', 'vk-blocks/group-style', addBlockControl);
1017
1018 // Attach classes/props to the editor block wrapper (BlockListBlock)
1019 const attachPropsToBlockList = createHigherOrderComponent((BlockListBlock) => {
1020 return (props) => {
1021 if (!isValidBlockType(props.name)) {
1022 return <BlockListBlock {...props} />;
1023 }
1024
1025 const { attributes = {}, className: existingClassName } = props;
1026 const { className = '', scrollable } = attributes;
1027
1028 // Build attached class name (scroll hint is rendered inside block via BlockEdit)
1029 let attachedClass = [existingClassName, className]
1030 .filter(Boolean)
1031 .join(' ');
1032 if (scrollable === true) {
1033 attachedClass = [attachedClass, 'is-style-vk-group-scrollable']
1034 .filter(Boolean)
1035 .join(' ');
1036 }
1037
1038 // エディタのブロックラッパーにdata属性を追加して、
1039 // エディタ用CSSのブレークポイントセレクタがマッチするようにする
1040 const { dataAttrs, styles } = buildScrollDataProps(attributes);
1041 const wrapperProps = {
1042 ...(props.wrapperProps || {}),
1043 ...dataAttrs,
1044 };
1045 if (Object.keys(styles).length > 0) {
1046 wrapperProps.style = {
1047 ...(wrapperProps.style || {}),
1048 ...styles,
1049 };
1050 }
1051
1052 return (
1053 <BlockListBlock
1054 {...props}
1055 className={attachedClass}
1056 wrapperProps={wrapperProps}
1057 />
1058 );
1059 };
1060 }, 'attachPropsToGroupBlockList');
1061
1062 addFilter(
1063 'editor.BlockListBlock',
1064 'vk-blocks/group-blocklist',
1065 attachPropsToBlockList
1066 );
1067
1068 /**
1069 * Define the save function for the group block, including link settings.
1070 *
1071 * @param {Object} props The block properties.
1072 * @return {JSX.Element} The saved content.
1073 */
1074 const save = (props) => {
1075 const { attributes = {} } = props;
1076 const {
1077 linkUrl,
1078 linkTarget,
1079 relAttribute,
1080 linkDescription,
1081 linkToPost,
1082 className = '',
1083 tagName: CustomTag = 'div',
1084 scrollable,
1085 } = attributes;
1086
1087 // スクロール関連のクラスと属性を組み立て
1088 const hasLink = linkUrl || linkToPost;
1089 const effectiveUrl = linkToPost ? '' : linkUrl;
1090 const classNames = [];
1091 if (hasLink) {
1092 classNames.push(className, 'has-link');
1093 } else {
1094 classNames.push(className);
1095 }
1096
1097 // scrollableが明示的にtrueの場合のみクラスを追加
1098 if (scrollable === true) {
1099 classNames.push('is-style-vk-group-scrollable');
1100 }
1101
1102 // Use block properties, setting className to include has-link if linkUrl is present
1103 const baseBlockProps = useBlockProps.save({
1104 className: classNames.filter(Boolean).join(' ').trim(),
1105 });
1106
1107 // スクロール関連の属性を追加(buildScrollDataProps で一�
1108 �管理)
1109 const { dataAttrs, styles: scrollStyles } =
1110 buildScrollDataProps(attributes);
1111 Object.assign(baseBlockProps, dataAttrs);
1112 if (Object.keys(scrollStyles).length > 0) {
1113 baseBlockProps.style = { ...baseBlockProps.style, ...scrollStyles };
1114 }
1115 // アクセシビリティ属性
1116 if (scrollable === true) {
1117 if (!baseBlockProps.role) {
1118 baseBlockProps.role = 'region';
1119 }
1120 if (baseBlockProps.tabIndex === undefined) {
1121 baseBlockProps.tabIndex = 0;
1122 }
1123 if (!baseBlockProps['aria-label']) {
1124 baseBlockProps['aria-label'] = __(
1125 'Horizontal scroll area',
1126 'vk-blocks'
1127 );
1128 }
1129 }
1130
1131 const blockProps = baseBlockProps;
1132
1133 // Extract prefix for custom link class
1134 const prefix = 'wp-block-group';
1135
1136 return (
1137 <CustomTag {...blockProps}>
1138 <InnerBlocks.Content />
1139 {hasLink && (
1140 <a
1141 href={effectiveUrl}
1142 {...(linkToPost ? { 'data-vk-link-to-post': '1' } : {})}
1143 {...(linkTarget ? { target: linkTarget } : {})}
1144 {...(relAttribute ? { rel: relAttribute } : {})}
1145 className={`${prefix}-vk-link`}
1146 >
1147 <span className="screen-reader-text">
1148 {linkDescription
1149 ? linkDescription
1150 : __('Group link', 'vk-blocks')}
1151 </span>
1152 </a>
1153 )}
1154 </CustomTag>
1155 );
1156 };
1157
1158 // Support for existing group blocks and version management
1159 import { assign } from 'lodash';
1160
1161 /**
1162 * Override block settings to include custom save function and attributes.
1163 *
1164 * @param {Object} settings The block settings.
1165 * @param {string} name The block name.
1166 * @param {Object} currentDeprecated
1167 * @return {Object} The modified block settings.
1168 */
1169 const overrideBlockSettings = (settings, name, currentDeprecated) => {
1170 if (name !== 'core/group') {
1171 return settings;
1172 }
1173
1174 // layout の値を取得
1175 const layoutValue =
1176 settings.supports && 'layout' in settings.supports
1177 ? settings.supports.layout
1178 : {};
1179
1180 let newSettings = assign({}, settings, {
1181 supports: {
1182 ...settings.supports,
1183 layout: layoutValue,
1184 },
1185 });
1186
1187 // deprecated は currentDeprecated === null のときだけマージする
1188 if (currentDeprecated === null) {
1189 const newDeprecated = [...(settings.deprecated || [])];
1190 const sorted = [...(Array.isArray(deprecated) ? deprecated : [])].sort(
1191 (a, b) =>
1192 (b.targetVersion || newDeprecated.length) -
1193 (a.targetVersion || newDeprecated.length)
1194 );
1195 sorted.forEach((item) => {
1196 const index = item.targetVersion || newDeprecated.length;
1197 const copy = { ...item };
1198 delete copy.targetVersion;
1199 newDeprecated.splice(index, 0, copy);
1200 });
1201 newSettings = {
1202 ...newSettings,
1203 deprecated: newDeprecated,
1204 save,
1205 };
1206 }
1207
1208 newSettings.attributes = {
1209 ...(newSettings.attributes || {}),
1210 linkUrl: { type: 'string', default: '' },
1211 linkTarget: { type: 'string', default: '' },
1212 relAttribute: { type: 'string', default: '' },
1213 linkDescription: { type: 'string', default: '' },
1214 tagName: { type: 'string', default: 'div' },
1215 };
1216
1217 return newSettings;
1218 };
1219
1220 addFilter(
1221 'blocks.registerBlockType',
1222 'vk-blocks/group-save',
1223 overrideBlockSettings
1224 );
1225
1226 // 保存時に追加のプロパティを設定
1227 const addExtraProps = (saveElementProps, blockType, attributes) => {
1228 if (isValidBlockType(blockType.name)) {
1229 // buildScrollDataProps で属性を一�
1230 �構築
1231 const { dataAttrs, styles } = buildScrollDataProps(attributes);
1232
1233 // scrollableがfalseの場合、不要なクラスを削除(他のプラグインなどで追加された場合の対策)
1234 if (!attributes.scrollable) {
1235 if (saveElementProps.className) {
1236 saveElementProps.className = saveElementProps.className
1237 .replace('is-style-vk-group-scrollable', '')
1238 .replace(/\s+/g, ' ')
1239 .trim();
1240 }
1241 }
1242
1243 // すべてのスクロール関連data属性をクリーンアップしてから再適用
1244 for (const key of SCROLL_DATA_ATTR_KEYS) {
1245 delete saveElementProps[key];
1246 }
1247 // scrollbar 関連の CSS カスタムプロパティもクリーンアップ
1248 if (saveElementProps.style) {
1249 delete saveElementProps.style['--vk-scrollbar-color'];
1250 delete saveElementProps.style['--vk-scrollbar-track-color'];
1251 }
1252
1253 // buildScrollDataProps の結果を適用
1254 Object.assign(saveElementProps, dataAttrs);
1255 if (Object.keys(styles).length > 0) {
1256 saveElementProps.style = {
1257 ...saveElementProps.style,
1258 ...styles,
1259 };
1260 }
1261
1262 // アクセシビリティ属性
1263 if (attributes.scrollable) {
1264 if (!saveElementProps.role) {
1265 saveElementProps.role = 'region';
1266 }
1267 if (saveElementProps.tabIndex === undefined) {
1268 saveElementProps.tabIndex = 0;
1269 }
1270 if (!saveElementProps['aria-label']) {
1271 saveElementProps['aria-label'] = __(
1272 'Horizontal scroll area',
1273 'vk-blocks'
1274 );
1275 }
1276 }
1277 } else if (
1278 blockType.name !== 'core/table' &&
1279 blockType.name !== 'vk-blocks/tab'
1280 ) {
1281 // 他のブロックでは不要な属性を削除
1282 // core/table はテーブルブロック独自の横スクロール機能で data-scroll-breakpoint 等を使用するため除外
1283 // vk-blocks/tab は独自の data-scrollbar-* 属性を save.js で設定するため除外
1284 for (const key of SCROLL_DATA_ATTR_KEYS) {
1285 delete saveElementProps[key];
1286 }
1287 }
1288
1289 return saveElementProps;
1290 };
1291 addFilter(
1292 'blocks.getSaveContent.extraProps',
1293 'vk-blocks/group-style',
1294 addExtraProps
1295 );
1296