/** * AKSRT Gutenberg Blocks * * Custom Gutenberg block extensions for the AKSRT theme. * Provides 9 generic content blocks adapted from legacy. * * 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 RawHTML = wp.element.RawHTML; var registerBlockTypeRaw = wp.blocks.registerBlockType; var blockEditor = wp.blockEditor || wp.editor || {}; var InspectorControls = blockEditor.InspectorControls || null; var InnerBlocks = blockEditor.InnerBlocks || null; var RichText = blockEditor.RichText || null; var MediaUpload = blockEditor.MediaUpload || 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; var Notice = wp.components.Notice; var useSelect = window.wp.data && window.wp.data.useSelect; if (!registerBlockTypeRaw || !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 }) ); // Register category BEFORE any registerBlockType (filter-after-register is too late). if (window.wp.hooks && window.wp.hooks.addFilter) { wp.hooks.addFilter('blocks.getBlockCategories', 'aksrt/block-categories', function (categories) { var list = Array.isArray(categories) ? categories.slice() : []; var hasAksrt = list.some(function (c) { return c && c.slug === CATEGORY; }); if (hasAksrt) return list; list.push({ slug: CATEGORY, title: 'AKSRT', icon: null, }); return list; }); } // WP 6.9+ expects apiVersion 3 for iframe editor compatibility. function registerBlockType(name, settings) { settings = settings || {}; if (settings.apiVersion == null) { settings.apiVersion = 3; } if (!settings.category) { settings.category = CATEGORY; } return registerBlockTypeRaw(name, settings); } // ================================================================ // 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: 'fa fas fa-star', title: '特性一', desc: '特性描述' }], ['aksrt/feature-item', { icon: 'fa fas fa-heart', title: '特性二', desc: '特性描述' }], ['aksrt/feature-item', { icon: 'fa 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: 'fa 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', }) ); }, }); // ── Carousel Block (Gallery → Swiper) ── // MediaUpload lives on wp.blockEditor; skip registration if unavailable. if (MediaUpload) { 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; }, }); } // ================================================================ // 9. Paid Fragment Block // ================================================================ function usePaidReadingConfigured() { if (!useSelect) return true; return useSelect(function (select) { var editor = select('core/editor'); if (!editor || !editor.getEditedPostAttribute) return true; var meta = editor.getEditedPostAttribute('meta') || {}; var pay = meta._aksrt_pay || {}; if (pay.pay_type !== 'reading') return false; return pay.pay_modo === 'points' ? Number(pay.points_price || 0) > 0 : Number(pay.price || 0) > 0; }, []); } registerBlockType('aksrt/paid-fragment', { title: '付费可见', icon: 'lock', category: CATEGORY, description: '仅购买文章付费阅读后可见的片段。', attributes: { content: { type: 'string', default: '' }, }, edit: function (props) { var configured = usePaidReadingConfigured(); return h('div', { className: 'aksrt-paid-fragment-block' }, !configured && Notice ? h(Notice, { status: 'warning', isDismissible: false }, '请在“付费内容设置”中选择“付费阅读”并设置有效价格;未配置时此片段会公开显示。' ) : null, h('div', { className: 'aksrt-paid-fragment-block__label' }, h('span', { className: 'dashicons dashicons-lock', 'aria-hidden': true }), '付费可见片段' ), h(RichText, { tagName: 'div', className: 'aksrt-paid-fragment-block__content', value: props.attributes.content, onChange: function (value) { props.setAttributes({ content: value }); }, placeholder: '输入购买后可见的内容…', }) ); }, save: function (props) { // Save a single canonical shortcode so hand-written and block-authored // content use the identical server-side rendering path. var shortcode = '[hidecontent type="payshow"]' + props.attributes.content + '[/hidecontent]'; return RawHTML ? h(RawHTML, null, shortcode) : shortcode; }, }); })();