Files
wordpress-theme-aksrt/assets/js/gutenberg-blocks.js
T

738 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AKSRT Gutenberg Blocks
*
* Custom Gutenberg block extensions for the AKSRT theme.
* Provides 9 generic content blocks adapted from zibll.
*
* Blocks: tabs, tab, enlighter, quote, alert, buttons, collapse, feature, iframe
*
* @package AKSRT
*/
(function () {
'use strict';
if (!window.wp || !window.wp.blocks) return;
var el = wp.element.createElement;
var Fragment = wp.element.Fragment;
var registerBlockType = wp.blocks.registerBlockType;
var InspectorControls = wp.blockEditor ? wp.blockEditor.InspectorControls : (wp.editor ? wp.editor.InspectorControls : null);
var InnerBlocks = wp.blockEditor ? wp.blockEditor.InnerBlocks : (wp.editor ? wp.editor.InnerBlocks : null);
var RichText = wp.blockEditor ? wp.blockEditor.RichText : (wp.editor ? wp.editor.RichText : null);
var ColorPicker = wp.components.ColorPicker;
var PanelBody = wp.components.PanelBody;
var SelectControl = wp.components.SelectControl;
var TextControl = wp.components.TextControl;
var ToggleControl = wp.components.ToggleControl;
var RangeControl = wp.components.RangeControl;
if (!registerBlockType || !el) return;
// ── Helper: create element shorthand ──
function h(tag, props, children) {
var args = [tag, props];
if (children !== undefined) {
if (Array.isArray(children)) {
args = args.concat(children);
} else {
args.push(children);
}
}
return el.apply(null, args);
}
// ── Helper: InspectorControls wrapper ──
function withInspector(title, children) {
if (!InspectorControls) return null;
return h(InspectorControls, { key: 'inspector' },
h(PanelBody, { title: title }, children)
);
}
var CATEGORY = 'aksrt-blocks';
var BLOCK_ICON = h('svg', { viewBox: '0 0 24 24', width: 20, height: 20 },
h('path', { d: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5', fill: 'none', stroke: 'currentColor', strokeWidth: 2 })
);
// ================================================================
// 1. Tabs Block
// ================================================================
registerBlockType('aksrt/tabs', {
title: '标签页',
icon: BLOCK_ICON,
category: CATEGORY,
description: '可切换的标签页内容容器',
attributes: {
style: { type: 'string', default: 'default' },
},
edit: function (props) {
return h(Fragment, null,
withInspector('标签页设置',
h(SelectControl, {
label: '样式',
value: props.attributes.style,
options: [
{ label: '默认', value: 'default' },
{ label: '线条', value: 'line' },
{ label: '卡片', value: 'card' },
],
onChange: function (v) { props.setAttributes({ style: v }); }
})
),
h('div', { className: 'aksrt-block-tabs aksrt-block-tabs--' + props.attributes.style },
h(InnerBlocks, {
allowedBlocks: ['aksrt/tab'],
template: [['aksrt/tab', { title: '标签 1' }], ['aksrt/tab', { title: '标签 2' }]],
})
)
);
},
save: function (props) {
return h('div', { className: 'aksrt-block-tabs aksrt-block-tabs--' + props.attributes.style },
h(InnerBlocks.Content, null)
);
},
});
registerBlockType('aksrt/tab', {
title: '标签页项',
icon: BLOCK_ICON,
category: CATEGORY,
parent: ['aksrt/tabs'],
attributes: {
title: { type: 'string', default: '标签' },
},
edit: function (props) {
return h(Fragment, null,
h(TextControl, {
label: '标签标题',
value: props.attributes.title,
onChange: function (v) { props.setAttributes({ title: v }); }
}),
h('div', { className: 'aksrt-block-tab__content' },
h(InnerBlocks, null)
)
);
},
save: function (props) {
return h('div', { className: 'aksrt-block-tab', 'data-title': props.attributes.title },
h(InnerBlocks.Content, null)
);
},
});
// ================================================================
// 2. EnlighterJS Code Block
// ================================================================
registerBlockType('aksrt/enlighter', {
title: '代码高亮',
icon: 'editor-code',
category: CATEGORY,
description: '带语法高亮的代码块(EnlighterJS)',
attributes: {
code: { type: 'string', default: '' },
language: { type: 'string', default: 'javascript' },
theme: { type: 'string', default: '' },
linenumbers: { type: 'boolean', default: true },
},
edit: function (props) {
return h(Fragment, null,
withInspector('代码块设置',
h(SelectControl, {
label: '语言',
value: props.attributes.language,
options: [
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'PHP', value: 'php' },
{ label: 'Python', value: 'python' },
{ label: 'Java', value: 'java' },
{ label: 'C/C++', value: 'cpp' },
{ label: 'C#', value: 'csharp' },
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'Ruby', value: 'ruby' },
{ label: 'Swift', value: 'swift' },
{ label: 'Kotlin', value: 'kotlin' },
{ label: 'HTML', value: 'html' },
{ label: 'CSS/SCSS', value: 'css' },
{ label: 'SQL', value: 'sql' },
{ label: 'Shell/Bash', value: 'shell' },
{ label: 'JSON', value: 'json' },
{ label: 'YAML', value: 'yaml' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'Docker', value: 'dockerfile' },
{ label: 'Diff', value: 'diff' },
{ label: 'XML', value: 'xml' },
],
onChange: function (v) { props.setAttributes({ language: v }); }
}),
h(ToggleControl, {
label: '显示行号',
checked: props.attributes.linenumbers,
onChange: function (v) { props.setAttributes({ linenumbers: v }); }
})
),
h('div', { className: 'aksrt-block-enlighter' },
h('div', { className: 'aksrt-block-enlighter__header' },
h('span', { className: 'aksrt-block-enlighter__lang' }, props.attributes.language)
),
h('textarea', {
className: 'aksrt-block-enlighter__code',
value: props.attributes.code,
onChange: function (e) { props.setAttributes({ code: e.target.value }); },
placeholder: '在此输入代码...',
rows: 10,
spellCheck: false,
})
)
);
},
save: function (props) {
var attrs = {
className: 'gl',
'data-enlighter-language': props.attributes.language,
};
if (props.attributes.theme) attrs['data-enlighter-theme'] = props.attributes.theme;
if (!props.attributes.linenumbers) attrs['data-enlighter-linenumbers'] = 'false';
return h('pre', { className: 'enlighter-pre' },
h('code', attrs, props.attributes.code)
);
},
});
// ================================================================
// 3. Quote Block
// ================================================================
registerBlockType('aksrt/quote-styled', {
title: '样式引用',
icon: 'editor-quote',
category: CATEGORY,
description: '带颜色样式的引用块',
attributes: {
content: { type: 'string', default: '' },
style: { type: 'string', default: 'gray' },
cite: { type: 'string', default: '' },
},
edit: function (props) {
return h(Fragment, null,
withInspector('引用设置',
h(SelectControl, {
label: '样式',
value: props.attributes.style,
options: [
{ label: '灰色', value: 'gray' },
{ label: '蓝色', value: 'blue' },
{ label: '绿色', value: 'green' },
{ label: '红色', value: 'red' },
{ label: '黄色', value: 'yellow' },
],
onChange: function (v) { props.setAttributes({ style: v }); }
}),
h(TextControl, {
label: '来源',
value: props.attributes.cite,
onChange: function (v) { props.setAttributes({ cite: v }); }
})
),
h('blockquote', { className: 'aksrt-block-quote aksrt-block-quote--' + props.attributes.style },
h(RichText, {
tagName: 'p',
value: props.attributes.content,
onChange: function (v) { props.setAttributes({ content: v }); },
placeholder: '输入引用内容...',
}),
props.attributes.cite ? h('cite', null, props.attributes.cite) : null
)
);
},
save: function (props) {
return h('blockquote', { className: 'aksrt-block-quote aksrt-block-quote--' + props.attributes.style },
h(RichText.Content, { tagName: 'p', value: props.attributes.content }),
props.attributes.cite ? h('cite', null, props.attributes.cite) : null
);
},
});
// ================================================================
// 4. Alert Block
// ================================================================
registerBlockType('aksrt/alert', {
title: '提示框',
icon: 'warning',
category: CATEGORY,
description: '通知/警告/成功/信息提示框',
attributes: {
content: { type: 'string', default: '' },
type: { type: 'string', default: 'info' },
dismissible: { type: 'boolean', default: false },
},
edit: function (props) {
return h(Fragment, null,
withInspector('提示框设置',
h(SelectControl, {
label: '类型',
value: props.attributes.type,
options: [
{ label: '信息', value: 'info' },
{ label: '成功', value: 'success' },
{ label: '警告', value: 'warning' },
{ label: '错误', value: 'error' },
],
onChange: function (v) { props.setAttributes({ type: v }); }
}),
h(ToggleControl, {
label: '可关闭',
checked: props.attributes.dismissible,
onChange: function (v) { props.setAttributes({ dismissible: v }); }
})
),
h('div', {
className: 'aksrt-block-alert aksrt-block-alert--' + props.attributes.type +
(props.attributes.dismissible ? ' aksrt-block-alert--dismissible' : '')
},
h(RichText, {
tagName: 'p',
value: props.attributes.content,
onChange: function (v) { props.setAttributes({ content: v }); },
placeholder: '输入提示内容...',
})
)
);
},
save: function (props) {
var cls = 'aksrt-block-alert aksrt-block-alert--' + props.attributes.type;
if (props.attributes.dismissible) cls += ' aksrt-block-alert--dismissible';
return h('div', { className: cls, role: 'alert' },
h(RichText.Content, { tagName: 'p', value: props.attributes.content }),
props.attributes.dismissible ? h('button', { type: 'button', className: 'aksrt-block-alert__close', 'aria-label': '关闭' }, '×') : null
);
},
});
// ================================================================
// 5. Buttons Block
// ================================================================
registerBlockType('aksrt/buttons', {
title: '按钮组',
icon: 'button',
category: CATEGORY,
description: '多个按钮组合',
attributes: {
align: { type: 'string', default: 'left' },
},
edit: function (props) {
return h(Fragment, null,
withInspector('按钮组设置',
h(SelectControl, {
label: '对齐',
value: props.attributes.align,
options: [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
],
onChange: function (v) { props.setAttributes({ align: v }); }
})
),
h('div', { className: 'aksrt-block-buttons aksrt-block-buttons--' + props.attributes.align },
h(InnerBlocks, {
allowedBlocks: ['aksrt/button'],
template: [['aksrt/button', { text: '按钮', style: 'primary' }]],
})
)
);
},
save: function (props) {
return h('div', { className: 'aksrt-block-buttons aksrt-block-buttons--' + props.attributes.align },
h(InnerBlocks.Content, null)
);
},
});
registerBlockType('aksrt/button', {
title: '按钮',
icon: 'button',
category: CATEGORY,
parent: ['aksrt/buttons'],
attributes: {
text: { type: 'string', default: '按钮' },
url: { type: 'string', default: '' },
style: { type: 'string', default: 'primary' },
size: { type: 'string', default: 'md' },
newTab: { type: 'boolean', default: false },
},
edit: function (props) {
return h(Fragment, null,
withInspector('按钮设置',
h(TextControl, {
label: '链接',
value: props.attributes.url,
onChange: function (v) { props.setAttributes({ url: v }); }
}),
h(SelectControl, {
label: '样式',
value: props.attributes.style,
options: [
{ label: '主要', value: 'primary' },
{ label: '次要', value: 'secondary' },
{ label: '轮廓', value: 'outline' },
{ label: '幽灵', value: 'ghost' },
],
onChange: function (v) { props.setAttributes({ style: v }); }
}),
h(SelectControl, {
label: '大小',
value: props.attributes.size,
options: [
{ label: '小', value: 'sm' },
{ label: '中', value: 'md' },
{ label: '大', value: 'lg' },
],
onChange: function (v) { props.setAttributes({ size: v }); }
}),
h(ToggleControl, {
label: '新窗口打开',
checked: props.attributes.newTab,
onChange: function (v) { props.setAttributes({ newTab: v }); }
})
),
h(RichText, {
tagName: 'span',
className: 'aksrt-block-btn aksrt-block-btn--' + props.attributes.style + ' aksrt-block-btn--' + props.attributes.size,
value: props.attributes.text,
onChange: function (v) { props.setAttributes({ text: v }); },
placeholder: '按钮文字...',
withoutInteractiveFormatting: true,
})
);
},
save: function (props) {
var linkProps = {
className: 'aksrt-block-btn aksrt-block-btn--' + props.attributes.style + ' aksrt-block-btn--' + props.attributes.size,
};
if (props.attributes.url) {
linkProps.href = props.attributes.url;
if (props.attributes.newTab) {
linkProps.target = '_blank';
linkProps.rel = 'noopener noreferrer';
}
return h('a', linkProps, props.attributes.text);
}
return h('span', linkProps, props.attributes.text);
},
});
// ================================================================
// 6. Collapse Block
// ================================================================
registerBlockType('aksrt/collapse', {
title: '折叠面板',
icon: 'arrow-down',
category: CATEGORY,
description: '可展开/收起的内容面板',
attributes: {
title: { type: 'string', default: '点击展开' },
open: { type: 'boolean', default: false },
},
edit: function (props) {
return h(Fragment, null,
withInspector('折叠面板设置',
h(ToggleControl, {
label: '默认展开',
checked: props.attributes.open,
onChange: function (v) { props.setAttributes({ open: v }); }
})
),
h('div', { className: 'aksrt-block-collapse' },
h('div', { className: 'aksrt-block-collapse__header' },
h(RichText, {
tagName: 'span',
className: 'aksrt-block-collapse__title',
value: props.attributes.title,
onChange: function (v) { props.setAttributes({ title: v }); },
placeholder: '面板标题...',
}),
h('span', { className: 'aksrt-block-collapse__arrow' }, '▼')
),
h('div', { className: 'aksrt-block-collapse__body' },
h(InnerBlocks, null)
)
)
);
},
save: function (props) {
return h('details', {
className: 'aksrt-block-collapse',
open: props.attributes.open ? true : undefined,
},
h('summary', { className: 'aksrt-block-collapse__header' },
h('span', { className: 'aksrt-block-collapse__title' }, props.attributes.title)
),
h('div', { className: 'aksrt-block-collapse__body' },
h(InnerBlocks.Content, null)
)
);
},
});
// ================================================================
// 7. Feature Grid Block
// ================================================================
registerBlockType('aksrt/feature', {
title: '特性网格',
icon: 'grid-view',
category: CATEGORY,
description: '图标+标题+描述的特性展示网格',
attributes: {
columns: { type: 'number', default: 3 },
},
edit: function (props) {
return h(Fragment, null,
withInspector('特性网格设置',
h(RangeControl, {
label: '列数',
value: props.attributes.columns,
min: 2,
max: 4,
onChange: function (v) { props.setAttributes({ columns: v }); }
})
),
h('div', { className: 'aksrt-block-feature aksrt-block-feature--cols-' + props.attributes.columns },
h(InnerBlocks, {
allowedBlocks: ['aksrt/feature-item'],
template: [
['aksrt/feature-item', { icon: 'fas fa-star', title: '特性一', desc: '特性描述' }],
['aksrt/feature-item', { icon: 'fas fa-heart', title: '特性二', desc: '特性描述' }],
['aksrt/feature-item', { icon: 'fas fa-bolt', title: '特性三', desc: '特性描述' }],
],
})
)
);
},
save: function (props) {
return h('div', { className: 'aksrt-block-feature aksrt-block-feature--cols-' + props.attributes.columns },
h(InnerBlocks.Content, null)
);
},
});
registerBlockType('aksrt/feature-item', {
title: '特性项',
icon: 'star-filled',
category: CATEGORY,
parent: ['aksrt/feature'],
attributes: {
icon: { type: 'string', default: 'fas fa-star' },
title: { type: 'string', default: '' },
desc: { type: 'string', default: '' },
},
edit: function (props) {
return h('div', { className: 'aksrt-block-feature__item' },
h(TextControl, {
label: '图标类名',
value: props.attributes.icon,
onChange: function (v) { props.setAttributes({ icon: v }); }
}),
h('i', { className: props.attributes.icon + ' aksrt-block-feature__icon' }),
h(RichText, {
tagName: 'h3',
className: 'aksrt-block-feature__title',
value: props.attributes.title,
onChange: function (v) { props.setAttributes({ title: v }); },
placeholder: '标题...',
}),
h(RichText, {
tagName: 'p',
className: 'aksrt-block-feature__desc',
value: props.attributes.desc,
onChange: function (v) { props.setAttributes({ desc: v }); },
placeholder: '描述...',
})
);
},
save: function (props) {
return h('div', { className: 'aksrt-block-feature__item' },
h('i', { className: props.attributes.icon + ' aksrt-block-feature__icon' }),
h('h3', { className: 'aksrt-block-feature__title' }, props.attributes.title),
h('p', { className: 'aksrt-block-feature__desc' }, props.attributes.desc)
);
},
});
// ================================================================
// 8. Responsive Iframe Block
// ================================================================
registerBlockType('aksrt/iframe', {
title: '响应式嵌入',
icon: 'video-alt3',
category: CATEGORY,
description: '响应式 iframe 嵌入(视频、地图等)',
attributes: {
src: { type: 'string', default: '' },
ratio: { type: 'string', default: '16:9' },
title: { type: 'string', default: '' },
},
edit: function (props) {
return h(Fragment, null,
withInspector('嵌入设置',
h(TextControl, {
label: 'URL',
value: props.attributes.src,
onChange: function (v) { props.setAttributes({ src: v }); }
}),
h(SelectControl, {
label: '宽高比',
value: props.attributes.ratio,
options: [
{ label: '16:9', value: '16:9' },
{ label: '4:3', value: '4:3' },
{ label: '1:1', value: '1:1' },
{ label: '自适应', value: 'auto' },
],
onChange: function (v) { props.setAttributes({ ratio: v }); }
}),
h(TextControl, {
label: '标题',
value: props.attributes.title,
onChange: function (v) { props.setAttributes({ title: v }); }
})
),
h('div', { className: 'aksrt-block-iframe aksrt-block-iframe--' + props.attributes.ratio.replace(':', '-') },
props.attributes.src
? h('iframe', { src: props.attributes.src, title: props.attributes.title || '嵌入内容', allowfullscreen: true })
: h('div', { className: 'aksrt-block-iframe__placeholder' }, '请输入嵌入 URL')
)
);
},
save: function (props) {
return h('div', { className: 'aksrt-block-iframe aksrt-block-iframe--' + props.attributes.ratio.replace(':', '-') },
h('iframe', {
src: props.attributes.src,
title: props.attributes.title || '嵌入内容',
allowfullscreen: true,
loading: 'lazy',
})
);
},
});
// ── Register block category ──
if (window.wp.hooks && window.wp.hooks.addFilter) {
wp.hooks.addFilter('blocks.getBlockCategories', 'aksrt/block-categories', function (categories) {
var hasAksrt = categories.some(function (c) { return c.slug === CATEGORY; });
if (hasAksrt) return categories;
return categories.concat([{
slug: CATEGORY,
title: 'AKSRT',
icon: BLOCK_ICON,
}]);
});
}
// ── Carousel Block (Gallery → Swiper) ──
if (MediaUpload && InnerBlocks) {
registerBlockType('aksrt/carousel', {
title: '轮播图',
icon: 'images-alt2',
category: CATEGORY,
description: '图片轮播/幻灯片(基于 Swiper)',
attributes: {
images: { type: 'array', default: [] },
autoplay: { type: 'boolean', default: true },
delay: { type: 'number', default: 3000 },
effect: { type: 'string', default: 'slide' },
},
edit: function (props) {
var attrs = props.attributes;
var setAttributes = props.setAttributes;
var onSelectImages = function (media) {
setAttributes({
images: media.map(function (m) {
return { url: m.url, alt: m.alt || '', id: m.id };
}),
});
};
var inspector = h(InspectorControls, { key: 'insp' },
h(PanelBody, { title: '轮播设置' },
h(ToggleControl, {
label: '自动播放',
checked: attrs.autoplay,
onChange: function (v) { setAttributes({ autoplay: v }); },
}),
h(RangeControl, {
label: '切换间隔(毫秒)',
value: attrs.delay,
min: 1000,
max: 10000,
step: 500,
onChange: function (v) { setAttributes({ delay: v }); },
}),
h(SelectControl, {
label: '切换效果',
value: attrs.effect,
options: [
{ label: '滑动', value: 'slide' },
{ label: '淡入淡出', value: 'fade' },
{ label: '立方体', value: 'cube' },
],
onChange: function (v) { setAttributes({ effect: v }); },
})
)
);
var preview = attrs.images.length > 0
? h('div', { className: 'aksrt-carousel-preview', style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
attrs.images.map(function (img, i) {
return h('img', {
key: i,
src: img.url,
alt: img.alt,
style: { width: '120px', height: '80px', objectFit: 'cover', borderRadius: '6px' },
});
})
)
: h('p', { style: { color: '#999', textAlign: 'center', padding: '20px' } }, '请选择图片');
return h(Fragment, null,
inspector,
h('div', { className: 'aksrt-carousel-block' },
preview,
h(MediaUpload, {
onSelect: onSelectImages,
allowedTypes: ['image'],
multiple: true,
value: attrs.images.map(function (img) { return img.id; }),
render: function (obj) {
return h(wp.components.Button, {
onClick: obj.open,
variant: 'secondary',
style: { marginTop: '8px' },
}, attrs.images.length ? '重新选择图片' : '选择图片');
},
})
)
);
},
save: function () {
// Dynamic block — rendered via PHP
return null;
},
});
}
})();