>}>
*/
private static $collect = array();
/**
* Tab groups rendered in this request, for unique IDs.
*
* @var int
*/
private static $tab_groups = 0;
/**
* Tooltips rendered in this request, for unique IDs.
*
* @var int
*/
private static $tooltips = 0;
/**
* QR codes rendered in this request, for unique IDs.
*
* @var int
*/
private static $qr_codes = 0;
/**
* Table of contents requests of the current content, by placeholder number.
*
* @var array>
*/
private static $tocs = array();
/**
* Marker on headings inside collapsible parts (tab panels, accordion entries, spoilers): a jump
* there would land in a closed area, so the table of contents leaves them out.
*/
const TOC_SKIP = ' data-threece-toc="skip"';
/**
* Notice icons, Tabler Icons (MIT, see credits.txt), inline so nothing loads from elsewhere.
*/
const NOTICE_ICONS = array(
'info' => '',
'tip' => '',
'warning' => '',
'success' => '',
);
/**
* Deepest supported nesting of the same element.
*/
const MAX_DEPTH = 4;
/**
* Hooks.
*/
public static function init(): void {
add_action( 'init', array( __CLASS__, 'register' ) );
// Before wpautop (10) and do_shortcode (11), so both see the renamed inner shortcodes.
add_filter( 'the_content', array( __CLASS__, 'number_nested' ), 9 );
add_filter( 'widget_text_content', array( __CLASS__, 'number_nested' ), 9 );
// After do_shortcode (11): the headings are final, the placeholders of [threece_toc] are in place.
add_filter( 'the_content', array( __CLASS__, 'build_tocs' ), 12 );
// After do_shortcode (11): buttons alone in their line get a paragraph back (see wrap_buttons()).
add_filter( 'the_content', array( __CLASS__, 'wrap_buttons' ), 12 );
add_filter( 'widget_text_content', array( __CLASS__, 'wrap_buttons' ), 12 );
// Automatic excerpts: WordPress strips every shortcode with its text; the containers keep theirs.
add_filter( 'strip_shortcodes_tagnames', array( __CLASS__, 'keep_text_in_excerpts' ) );
}
/**
* Elements whose text belongs to the post (tabs, accordion, spoiler, notice, columns, tooltip, button). Without
* this, wp_trim_excerpt() drops a whole FAQ written in an accordion. Elements without reading text (footnote,
* table of contents, video, QR code, countdown) stay out of the excerpt.
*/
const EXCERPT_KEEP = array( 'threece_tabs', 'threece_tab', 'threece_accordion', 'threece_item', 'threece_spoiler', 'threece_notice', 'threece_columns', 'threece_column', 'threece_tooltip', 'threece_button' );
/**
* Whether WordPress is building an automatic excerpt right now (wp_trim_excerpt() runs the_content inside
* get_the_excerpt). The elements then render as plain reading order: a title once, then the text.
*/
public static function in_excerpt(): bool {
return doing_filter( 'get_the_excerpt' );
}
/**
* Filter for strip_shortcodes_tagnames: takes the containers (all nesting aliases) off the list to remove.
*
* @param mixed $tags Shortcode names WordPress is about to strip.
* @return mixed
*/
public static function keep_text_in_excerpts( $tags ) {
if ( ! is_array( $tags ) ) {
return $tags;
}
$keep = array();
foreach ( self::EXCERPT_KEEP as $name ) {
for ( $level = 1; $level <= self::MAX_DEPTH; $level++ ) {
$keep[] = self::alias( $name, $level );
}
}
return array_values( array_diff( $tags, $keep ) );
}
/**
* A button shortcode alone in its line loses its paragraph to shortcode_unautop() and would stand as a bare
* link between the blocks. Block themes align only blocks with the content column, so it slipped to the edge
* (Twenty Twenty-Five). Lines that hold nothing but buttons get a paragraph; buttons inside a sentence stay.
*
* @param mixed $content Rendered content.
* @return mixed
*/
public static function wrap_buttons( $content ) {
if ( ! is_string( $content ) || false === strpos( $content, 'threece-button' ) ) {
return $content;
}
// One line, nothing but buttons (links, or spans without address) and spaces. Labels are escaped text, no tags.
return (string) preg_replace_callback(
'#^[ \t]*((?:<(a|span) class="threece-button[^"]*"[^>]*>[^<]*\\2>[ \t]*)+)$#m',
function ( $m ) {
return '
' . trim( $m[1] ) . '
';
},
$content
);
}
/**
* The WordPress shortcode parser cannot nest a shortcode in itself ([threece_tabs] inside
* [threece_tabs] closes at the first [/threece_tabs]). Inner levels are therefore renamed to
* registered aliases (threece_tabs__2 …) before parsing, so authors can nest naturally.
*
* @param mixed $content Post content from the filter.
* @return mixed
*/
public static function number_nested( $content ) {
if ( ! is_string( $content ) || false === strpos( $content, '[threece_' ) ) {
return $content;
}
$depth = array();
$names = implode( '|', array_map( 'preg_quote', array_keys( self::definitions() ) ) );
return (string) preg_replace_callback(
'#\[(/?)(' . $names . ')(?=[\s\]/])([^\]]*)\]#',
static function ( $m ) use ( &$depth ) {
$name = $m[2];
$current = $depth[ $name ] ?? 0;
if ( '/' === $m[1] ) {
$depth[ $name ] = max( 0, $current - 1 );
return '[/' . self::alias( $name, $current ) . ']';
}
if ( '/' === substr( rtrim( $m[3] ), -1 ) ) {
return '[' . self::alias( $name, $current + 1 ) . $m[3] . ']';
}
$depth[ $name ] = $current + 1;
return '[' . self::alias( $name, $current + 1 ) . $m[3] . ']';
},
$content
);
}
/**
* Shortcode name for a nesting level: level 1 keeps the name, deeper levels get __2 … __4.
*
* @param string $name Shortcode name.
* @param int $level Nesting level, 1 for the outermost.
* @return string
*/
private static function alias( $name, $level ) {
$level = min( max( 1, $level ), self::MAX_DEPTH );
return 1 === $level ? $name : $name . '__' . $level;
}
/**
* Every element in one place: label, callback, fields with type and default, parent and child.
* Defaults, attribute cleaning and the inserter dialog are all derived from here.
*
* Field types: text (empty falls back to the default), bool (yes, 1, true, on), select (one of
* "values", else the default), number (whole number between "min" and "max", else the default).
*
* @return array>
*/
public static function definitions() {
$open = array(
'type' => 'bool',
'label' => __( 'Open at first', '3task-toolbar-for-classic-editor' ),
'default' => false,
);
$definitions = array(
'threece_tabs' => array(
'label' => __( 'Tabs', '3task-toolbar-for-classic-editor' ),
'callback' => 'tabs',
'fields' => array(
'active' => array(
'type' => 'number',
'label' => __( 'Tab open at first (number)', '3task-toolbar-for-classic-editor' ),
'default' => 1,
'min' => 1,
'max' => 50,
),
'mobile' => array(
'type' => 'select',
'label' => __( 'On phones', '3task-toolbar-for-classic-editor' ),
'help' => __( 'How the tabs behave on narrow screens.', '3task-toolbar-for-classic-editor' ),
'default' => 'wrap',
'values' => array(
'wrap' => __( 'Tabs wrap to new lines', '3task-toolbar-for-classic-editor' ),
'scroll' => __( 'Tabs scroll sideways', '3task-toolbar-for-classic-editor' ),
'stack' => __( 'All contents one below the other', '3task-toolbar-for-classic-editor' ),
),
),
'anchor_in_url' => array(
'type' => 'bool',
'label' => __( 'Show the open tab in the address', '3task-toolbar-for-classic-editor' ),
'default' => false,
),
),
'child' => 'threece_tab',
),
'threece_tab' => array(
'label' => __( 'Tab', '3task-toolbar-for-classic-editor' ),
'callback' => 'tab',
'fields' => array(
'title' => self::title_field( __( 'Tab', '3task-toolbar-for-classic-editor' ) ),
'anchor' => array(
'type' => 'text',
'label' => __( 'Link name (opens this tab via #name)', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
),
'content' => true,
'parent' => 'threece_tabs',
),
'threece_accordion' => array(
'label' => __( 'Accordion', '3task-toolbar-for-classic-editor' ),
'callback' => 'accordion',
'fields' => array(),
'child' => 'threece_item',
),
'threece_item' => array(
'label' => __( 'Entry', '3task-toolbar-for-classic-editor' ),
'callback' => 'item',
'fields' => array(
'title' => self::title_field( __( 'Show more', '3task-toolbar-for-classic-editor' ) ),
'open' => $open,
),
'content' => true,
'parent' => 'threece_accordion',
),
'threece_spoiler' => array(
'label' => __( 'Spoiler', '3task-toolbar-for-classic-editor' ),
'callback' => 'spoiler',
'fields' => array(
'title' => self::title_field( __( 'Show more', '3task-toolbar-for-classic-editor' ) ),
'open' => $open,
),
'content' => true,
),
'threece_notice' => array(
'label' => __( 'Notice box', '3task-toolbar-for-classic-editor' ),
'callback' => 'notice',
'fields' => array(
'type' => array(
'type' => 'select',
'label' => __( 'Kind', '3task-toolbar-for-classic-editor' ),
'default' => 'info',
'values' => array(
'info' => __( 'Info', '3task-toolbar-for-classic-editor' ),
'tip' => __( 'Tip', '3task-toolbar-for-classic-editor' ),
'warning' => __( 'Warning', '3task-toolbar-for-classic-editor' ),
'success' => __( 'Success', '3task-toolbar-for-classic-editor' ),
),
),
'title' => array(
'type' => 'text',
'label' => __( 'Title (optional)', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
),
'content' => true,
),
'threece_button' => array(
'label' => __( 'Button', '3task-toolbar-for-classic-editor' ),
'callback' => 'button',
'fields' => array(
'url' => array(
'type' => 'url',
'label' => __( 'Link (https://, mailto:, tel:, /page/ or #anchor)', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
'style' => array(
'type' => 'select',
'label' => __( 'Style', '3task-toolbar-for-classic-editor' ),
'default' => 'primary',
'values' => array(
'primary' => __( 'Primary', '3task-toolbar-for-classic-editor' ),
'secondary' => __( 'Secondary', '3task-toolbar-for-classic-editor' ),
'outline' => __( 'Outline', '3task-toolbar-for-classic-editor' ),
),
),
'new_tab' => array(
'type' => 'bool',
'label' => __( 'Open in a new tab', '3task-toolbar-for-classic-editor' ),
'default' => false,
),
'nofollow' => array(
'type' => 'bool',
'label' => __( 'Partner or paid link (nofollow sponsored)', '3task-toolbar-for-classic-editor' ),
'help' => __( 'For affiliate and paid links, as search engines ask for it.', '3task-toolbar-for-classic-editor' ),
'default' => false,
),
'download' => array(
'type' => 'bool',
'label' => __( 'Download file', '3task-toolbar-for-classic-editor' ),
'default' => false,
),
),
'content' => true,
),
'threece_columns' => array(
'label' => __( 'Columns', '3task-toolbar-for-classic-editor' ),
'callback' => 'columns',
'fields' => array(),
'child' => 'threece_column',
),
'threece_column' => array(
'label' => __( 'Column', '3task-toolbar-for-classic-editor' ),
'callback' => 'column',
'fields' => array(
'span' => array(
'type' => 'number',
'label' => __( 'Width (1 to 3 parts)', '3task-toolbar-for-classic-editor' ),
'default' => 1,
'min' => 1,
'max' => 3,
),
),
'content' => true,
'parent' => 'threece_columns',
),
'threece_toc' => array(
'label' => __( 'Table of contents', '3task-toolbar-for-classic-editor' ),
'callback' => 'toc',
'fields' => array(
'title' => array(
'type' => 'text',
'label' => __( 'Title', '3task-toolbar-for-classic-editor' ),
'default' => __( 'Contents', '3task-toolbar-for-classic-editor' ),
),
'from' => array(
'type' => 'number',
'label' => __( 'From heading level', '3task-toolbar-for-classic-editor' ),
'help' => __( '2 starts with the H2 headings, the usual first level inside a post.', '3task-toolbar-for-classic-editor' ),
'default' => 2,
'min' => 2,
'max' => 6,
),
'to' => array(
'type' => 'number',
'label' => __( 'To heading level', '3task-toolbar-for-classic-editor' ),
'default' => 3,
'min' => 2,
'max' => 6,
),
'open' => array(
'type' => 'bool',
'label' => __( 'Open at first', '3task-toolbar-for-classic-editor' ),
'default' => true,
),
'numbered' => array(
'type' => 'bool',
'label' => __( 'Numbered', '3task-toolbar-for-classic-editor' ),
'default' => false,
),
),
),
'threece_video' => array(
'label' => __( 'Video (YouTube, Vimeo)', '3task-toolbar-for-classic-editor' ),
'callback' => 'video',
'fields' => array(
'url' => array(
'type' => 'url',
'label' => __( 'Video address', '3task-toolbar-for-classic-editor' ),
'help' => __( 'YouTube or Vimeo. Nothing loads from there before the click.', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
'title' => array(
'type' => 'text',
'label' => __( 'Title', '3task-toolbar-for-classic-editor' ),
'help' => __( 'Shown on the preview image and read out by screen readers.', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
'image' => array(
'type' => 'number',
'label' => __( 'Preview image (media ID, optional)', '3task-toolbar-for-classic-editor' ),
'default' => 0,
'min' => 0,
'max' => 2147483647,
// With the right to upload, the dialog picks the image in the media library instead.
'media' => array(
'label' => __( 'Preview image (optional)', '3task-toolbar-for-classic-editor' ),
'help' => __( 'Shown before the click instead of a plain surface. It comes from your own media library.', '3task-toolbar-for-classic-editor' ),
),
),
'start' => array(
'type' => 'number',
'label' => __( 'Start at second', '3task-toolbar-for-classic-editor' ),
'default' => 0,
'min' => 0,
'max' => 86400,
),
),
),
'threece_countdown' => array(
'label' => __( 'Countdown', '3task-toolbar-for-classic-editor' ),
'callback' => 'countdown',
'fields' => array(
'to' => array(
'type' => 'text',
'label' => __( 'Ends at (YYYY-MM-DD HH:MM, time zone of the site)', '3task-toolbar-for-classic-editor' ),
'help' => __( 'Example: 2026-12-24 18:00. The time zone is set under Settings, General.', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
'expired' => array(
'type' => 'text',
'label' => __( 'Text after the end', '3task-toolbar-for-classic-editor' ),
'default' => __( 'The countdown has ended.', '3task-toolbar-for-classic-editor' ),
),
),
),
'threece_qr' => array(
'label' => __( 'QR code', '3task-toolbar-for-classic-editor' ),
'callback' => 'qr',
'fields' => array(
'text' => array(
'type' => 'text',
'label' => __( 'Content (link or text)', '3task-toolbar-for-classic-editor' ),
'help' => __( 'A link or any text. The code is made on your server.', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
'size' => array(
'type' => 'number',
'label' => __( 'Size in pixels', '3task-toolbar-for-classic-editor' ),
'default' => 200,
'min' => 64,
'max' => 1024,
),
'ecc' => array(
'type' => 'select',
'label' => __( 'Error correction', '3task-toolbar-for-classic-editor' ),
'help' => __( 'Higher values still scan when the code is small or damaged, but the code gets denser.', '3task-toolbar-for-classic-editor' ),
'default' => 'M',
'values' => array(
'L' => __( 'Low (7 %)', '3task-toolbar-for-classic-editor' ),
'M' => __( 'Medium (15 %)', '3task-toolbar-for-classic-editor' ),
'Q' => __( 'Quartile (25 %)', '3task-toolbar-for-classic-editor' ),
'H' => __( 'High (30 %)', '3task-toolbar-for-classic-editor' ),
),
),
'title' => array(
'type' => 'text',
'label' => __( 'Description for screen readers (optional)', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
),
),
'threece_tooltip' => array(
'label' => __( 'Tooltip', '3task-toolbar-for-classic-editor' ),
'callback' => 'tooltip',
'fields' => array(
'text' => array(
'type' => 'text',
'label' => __( 'Explanation', '3task-toolbar-for-classic-editor' ),
'default' => '',
),
),
'content' => true,
),
'threece_fn' => array(
'label' => __( 'Footnote', '3task-toolbar-for-classic-editor' ),
'callback' => 'footnote',
'fields' => array(),
'content' => true,
),
);
/**
* Elements of add-ons. Each entry needs 'label', 'callback' (a method of this class or any callable),
* 'fields' and, for the dialog, a 'card' with group, description, keywords and icon; 'content' => true for
* an inner content, 'child' and 'parent' for containers. Attributes are cleaned by field type like our own.
*
* @since 1.0.2
* @param array> $definitions Elements by shortcode name.
*/
$definitions = apply_filters( 'threece_elements', $definitions );
// Every element accepts extra CSS classes for themes and agencies.
foreach ( $definitions as $tag => $definition ) {
$definitions[ $tag ]['fields']['class'] = array(
'type' => 'class',
'label' => __( 'CSS class (optional)', '3task-toolbar-for-classic-editor' ),
'default' => '',
);
}
return $definitions;
}
/**
* Most extra CSS classes per element and longest class name.
*/
const MAX_CLASSES = 5;
const MAX_CLASS_LENGTH = 50;
/**
* Class attribute: own base classes plus the cleaned extra classes, without duplicates.
*
* @param string $base Own classes, space separated.
* @param string $extra Cleaned extra classes from atts().
* @return string Escaped for an HTML attribute.
*/
public static function classes( $base, $extra ) {
$all = array_unique( array_merge( explode( ' ', $base ), '' === $extra ? array() : explode( ' ', $extra ) ) );
return esc_attr( implode( ' ', $all ) );
}
/**
* Title field with its fallback text.
*
* @param string $fallback Shown when the title is empty.
* @return array
*/
public static function title_field( $fallback ) {
return array(
'type' => 'text',
'label' => __( 'Title', '3task-toolbar-for-classic-editor' ),
'default' => $fallback,
);
}
/**
* Attributes of a shortcode, cleaned by its definition. Unknown attributes are dropped.
*
* @param string $tag Shortcode name without nesting suffix.
* @param array|string $raw Attributes as WordPress passes them.
* @return array
*/
public static function atts( $tag, $raw ) {
$fields = self::definitions()[ $tag ]['fields'] ?? array();
$defaults = array();
foreach ( $fields as $name => $field ) {
$defaults[ $name ] = $field['default'];
}
$atts = shortcode_atts( $defaults, $raw, $tag );
foreach ( $fields as $name => $field ) {
$atts[ $name ] = self::clean( $field, $atts[ $name ] );
}
return $atts;
}
/**
* One attribute value by field type.
*
* @param array $field Field definition.
* @param mixed $value Raw value.
* @return mixed
*/
private static function clean( $field, $value ) {
switch ( $field['type'] ) {
case 'bool':
return is_bool( $value ) ? $value : self::is_yes( $value );
case 'select':
return array_key_exists( (string) $value, $field['values'] ) ? (string) $value : $field['default'];
case 'number':
if ( ! is_numeric( $value ) ) {
return $field['default'];
}
return (int) min( max( (int) $value, $field['min'] ), $field['max'] );
case 'url':
return esc_url_raw( trim( (string) $value ), array( 'http', 'https', 'mailto', 'tel' ) );
case 'class':
$classes = array();
foreach ( preg_split( '#\s+#', (string) $value ) as $class ) {
$class = substr( sanitize_html_class( $class ), 0, self::MAX_CLASS_LENGTH );
if ( '' !== $class && ! in_array( $class, $classes, true ) ) {
$classes[] = $class;
}
}
return implode( ' ', array_slice( $classes, 0, self::MAX_CLASSES ) );
default:
return '' !== trim( (string) $value ) ? (string) $value : $field['default'];
}
}
/**
* Elements as the inserter dialog shows them: top-level elements with their fields and child.
*
* @return array>
*/
public static function catalog() {
$definitions = self::definitions();
$catalog = array();
foreach ( $definitions as $tag => $definition ) {
if ( isset( $definition['parent'] ) ) {
continue;
}
$card = isset( $definition['card'] ) && is_array( $definition['card'] ) ? $definition['card'] : ( self::cards()[ $tag ] ?? array() );
$entry = array(
'tag' => $tag,
'label' => $definition['label'],
'group' => $card['group'] ?? '',
'description' => $card['description'] ?? '',
'keywords' => $card['keywords'] ?? array(),
'icon' => $card['icon'] ?? '',
'fields' => self::catalog_fields( $definition ),
);
if ( isset( $definition['child'] ) ) {
$child = $definitions[ $definition['child'] ];
$entry['child'] = array(
'tag' => $definition['child'],
'label' => $child['label'],
'fields' => self::catalog_fields( $child ),
'content' => ! empty( $child['content'] ),
);
} elseif ( ! empty( $definition['content'] ) ) {
$entry['content'] = true;
}
$catalog[] = $entry;
}
return $catalog;
}
/**
* Groups of the element dialog, in the order the dialog shows them.
*
* @return array
*/
public static function groups() {
$groups = array(
array(
'key' => 'structure',
'label' => __( 'Structure', '3task-toolbar-for-classic-editor' ),
),
array(
'key' => 'highlight',
'label' => __( 'Highlight', '3task-toolbar-for-classic-editor' ),
),
array(
'key' => 'embed',
'label' => __( 'Embed', '3task-toolbar-for-classic-editor' ),
),
);
/**
* Groups of add-on elements, shown after our own.
*
* @since 1.0.2
* @param array $groups Groups in dialog order.
*/
return apply_filters( 'threece_element_groups', $groups );
}
/**
* What a card in the element dialog shows: group, one sentence, search words and icon (name in inserter-icons.js).
* Search words are translated with the plugin, so each language finds its own words.
*
* @return array
*/
private static function cards() {
return array(
'threece_tabs' => array(
'group' => 'structure',
'description' => __( 'Contents side by side, one tab is open.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'tabs', '3task-toolbar-for-classic-editor' ), __( 'switch', '3task-toolbar-for-classic-editor' ), __( 'register', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'layout-navbar',
),
'threece_accordion' => array(
'group' => 'structure',
'description' => __( 'Questions and answers that fold open.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'faq', '3task-toolbar-for-classic-editor' ), __( 'questions', '3task-toolbar-for-classic-editor' ), __( 'collapse', '3task-toolbar-for-classic-editor' ), __( 'toggle', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'layout-list',
),
'threece_spoiler' => array(
'group' => 'structure',
'description' => __( 'One section that appears on click.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'hide', '3task-toolbar-for-classic-editor' ), __( 'reveal', '3task-toolbar-for-classic-editor' ), __( 'read more', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'eye-off',
),
'threece_columns' => array(
'group' => 'structure',
'description' => __( 'Two or three columns, stacked on phones.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'grid', '3task-toolbar-for-classic-editor' ), __( 'layout', '3task-toolbar-for-classic-editor' ), __( 'side by side', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'columns',
),
'threece_notice' => array(
'group' => 'highlight',
'description' => __( 'Info, tip, warning or success.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'alert', '3task-toolbar-for-classic-editor' ), __( 'box', '3task-toolbar-for-classic-editor' ), __( 'callout', '3task-toolbar-for-classic-editor' ), __( 'warning', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'info-square-rounded',
),
'threece_button' => array(
'group' => 'highlight',
'description' => __( 'A link that looks like a button.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'link', '3task-toolbar-for-classic-editor' ), __( 'call to action', '3task-toolbar-for-classic-editor' ), __( 'cta', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'hand-click',
),
'threece_tooltip' => array(
'group' => 'highlight',
'description' => __( 'An explanation on hover or tap.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'hint', '3task-toolbar-for-classic-editor' ), __( 'explain', '3task-toolbar-for-classic-editor' ), __( 'glossary', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'message-circle-question',
),
'threece_fn' => array(
'group' => 'highlight',
'description' => __( 'A raised number, the text at the end of the post.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'source', '3task-toolbar-for-classic-editor' ), __( 'reference', '3task-toolbar-for-classic-editor' ), __( 'citation', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'superscript',
),
'threece_toc' => array(
'group' => 'embed',
'description' => __( 'Builds itself from the headings.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'contents', '3task-toolbar-for-classic-editor' ), __( 'toc', '3task-toolbar-for-classic-editor' ), __( 'headings', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'list-tree',
),
'threece_video' => array(
'group' => 'embed',
'description' => __( 'YouTube or Vimeo, loads only after a click.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'youtube', '3task-toolbar-for-classic-editor' ), __( 'vimeo', '3task-toolbar-for-classic-editor' ), __( 'embed', '3task-toolbar-for-classic-editor' ), __( 'privacy', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'player-play',
),
'threece_qr' => array(
'group' => 'embed',
'description' => __( 'Made on your server, without an outside service.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'barcode', '3task-toolbar-for-classic-editor' ), __( 'scan', '3task-toolbar-for-classic-editor' ), __( 'phone', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'qrcode',
),
'threece_countdown' => array(
'group' => 'embed',
'description' => __( 'Counts down to a date and time.', '3task-toolbar-for-classic-editor' ),
'keywords' => array( __( 'timer', '3task-toolbar-for-classic-editor' ), __( 'deadline', '3task-toolbar-for-classic-editor' ), __( 'date', '3task-toolbar-for-classic-editor' ) ),
'icon' => 'hourglass',
),
);
}
/**
* Fields as the dialog needs them (bool becomes a checkbox).
*
* @param array $definition Element definition.
* @return array>
*/
private static function catalog_fields( $definition ) {
$fields = array();
foreach ( $definition['fields'] as $name => $field ) {
$entry = array(
'name' => $name,
'type' => 'bool' === $field['type'] ? 'checkbox' : ( 'class' === $field['type'] ? 'text' : $field['type'] ),
'label' => $field['label'],
'default' => $field['default'],
);
foreach ( array( 'help', 'values', 'min', 'max' ) as $key ) {
if ( isset( $field[ $key ] ) ) {
$entry[ $key ] = $field[ $key ];
}
}
// JavaScript sorts numeric object keys first; a list of pairs keeps the order of the choices (add-ons list posts by id).
if ( isset( $entry['values'] ) && is_array( $entry['values'] ) && array_keys( $entry['values'] ) !== array_filter( array_keys( $entry['values'] ), 'is_string' ) ) {
$pairs = array();
foreach ( $entry['values'] as $value => $label ) {
$pairs[] = array( (string) $value, $label );
}
$entry['values'] = $pairs;
}
// Only who may use the media library gets the image picker; everyone else keeps the number field.
if ( isset( $field['media'] ) && current_user_can( 'upload_files' ) ) {
$entry['type'] = 'image';
$entry['label'] = $field['media']['label'];
$entry['help'] = $field['media']['help'];
}
$fields[] = $entry;
}
return $fields;
}
/**
* Registers shortcodes.
*/
public static function register(): void {
wp_register_style( 'threece-elements', THREECE_URL . 'assets/css/elements.css', array(), THREECE_VERSION );
wp_register_script_module( 'threece-tabs', THREECE_URL . 'assets/js/tabs.js', array(), THREECE_VERSION );
wp_register_script_module( 'threece-video', THREECE_URL . 'assets/js/video.js', array(), THREECE_VERSION );
wp_register_script_module( 'threece-countdown', THREECE_URL . 'assets/js/countdown.js', array(), THREECE_VERSION );
wp_register_script_module( 'threece-tooltip', THREECE_URL . 'assets/js/tooltip.js', array(), THREECE_VERSION );
foreach ( self::definitions() as $name => $definition ) {
// Our own elements name a method of this class; add-ons pass any callable.
$callback = is_string( $definition['callback'] ) && method_exists( __CLASS__, $definition['callback'] ) ? array( __CLASS__, $definition['callback'] ) : $definition['callback'];
if ( ! is_callable( $callback ) ) {
continue;
}
for ( $level = 1; $level <= self::MAX_DEPTH; $level++ ) {
add_shortcode( self::alias( $name, $level ), $callback );
}
}
}
/**
* [threece_tabs active="1" mobile="wrap|scroll|stack" anchor_in_url] with [threece_tab title="" anchor=""]
* inside. Follows the WAI-ARIA tabs pattern; without JavaScript every panel stays visible, the script
* hides the inactive ones. A tab's anchor is a data attribute, never an id, so it cannot clash with
* IDs of the theme; the script opens the tab when the address ends in #anchor.
*
* @param array|string $atts Attributes.
* @param string|null $content Tabs.
* @return string
*/
public static function tabs( $atts, $content = null ) {
$atts = self::atts( 'threece_tabs', $atts );
$tabs = self::collect_children( 'threece_tab', $content );
if ( empty( $tabs ) ) {
return '';
}
if ( self::in_excerpt() ) {
$plain = '';
foreach ( $tabs as $tab ) {
$plain .= '
#', '', $content );
return self::balance_paragraphs( trim( $content ) );
}
/**
* Text that starts on the shortcode line arrives as "first.
second" (wpautop wrapped the
* shortcode tags, not the text). Opens the first and closes the last paragraph so every paragraph
* is complete.
*
* @param string $content Inner content.
* @return string
*/
private static function balance_paragraphs( $content ) {
$first_close = strpos( $content, '