id}_scripts", [$this, 'register_default_scripts']); add_action("wu_{$this->id}_scripts", [$this, 'register_scripts']); add_action('wp', [$this, 'maybe_setup']); add_action('admin_head', [$this, 'setup_for_admin'], 100); add_filter('pre_render_block', [$this, 'setup_for_block_editor'], 100, 2); add_action('wu_element_preview', [$this, 'setup_preview']); // Init should be the correct time to call this to avoid the deprecated notice from I18N. // But it doesn't work for some reason, fix later. // add_action('init', function () { do_action('wu_element_loaded', $this); // } ); if ($this->public) { self::register_public_element($this); } } /** * Register a public element to further use. * * @since 2.0.24 * @param mixed $element The element instance to be registered. * @return void */ public static function register_public_element($element): void { static::$public_elements[] = $element; } /** * Retrieves the public registered elements. * * @since 2.0.24 * @return array */ public static function get_public_elements() { return static::$public_elements; } /** * Sets blocks up for the block editor. * * @since 2.0.0 * * @param null $short_circuit The value passed. * @param array $block The parsed block data. * @return null */ public function setup_for_block_editor($short_circuit, $block) { $should_load = false; if ($block['blockName'] === $this->get_id()) { $should_load = true; } /** * We might need to add additional blocks later. * * @since 2.0.0 * @return array */ $blocks_to_check = apply_filters( 'wu_element_block_types_to_check', [ 'core/shortcode', 'core/paragraph', ] ); if (in_array($block['blockName'], $blocks_to_check, true)) { if ($this->contains_current_element($block['innerHTML'])) { $should_load = true; } } if ($should_load) { if ($this->is_preview()) { $this->setup_preview(); } else { $this->setup(); } } return $short_circuit; } /** * Search for an element id on the list of metaboxes. * * Builds a cached list of elements on the first run. * Then uses the cache to run a simple in_array check. * * @since 2.0.0 * * @param string $element_id The element ID. * @return bool */ protected static function search_in_metaboxes($element_id) { global $wp_meta_boxes, $pagenow; /* * Bail if things don't look normal or in the right context. */ if ( ! function_exists('get_current_screen')) { return; } $screen = get_current_screen(); /* * First, check on cache, to avoid recalculating it time and time again. */ if (is_array(self::$metabox_cache)) { return in_array($element_id, self::$metabox_cache, true); } $contains_metaboxes = wu_get_isset($wp_meta_boxes, $screen->id) || wu_get_isset($wp_meta_boxes, $pagenow); $elements_to_cache = []; $found = false; if (is_array($wp_meta_boxes) && $contains_metaboxes && is_array($wp_meta_boxes[ $screen->id ])) { foreach ($wp_meta_boxes[ $screen->id ] as $position => $priorities) { foreach ($priorities as $priority => $metaboxes) { foreach ($metaboxes as $metabox_id => $metabox) { $elements_to_cache[] = $metabox_id; if ($metabox_id === $element_id) { $found = true; } } } } /** * Set a local cache so we don't have to loop it all over again. */ self::$metabox_cache = $elements_to_cache; } return $found; } /** * Setup element on admin pages. * * @since 2.0.0 * @return void */ public function setup_for_admin(): void { if ($this->loaded === true) { return; } $element_id = "wp-ultimo-{$this->id}-element"; if (self::search_in_metaboxes($element_id)) { $this->loaded = true; $this->setup(); } } /** * Maybe run setup, when the shortcode or block is found. * * @todo check if this is working only when necessary. * @since 2.0.0 * @return void */ public function maybe_setup(): void { global $post; if (is_admin() || empty($post)) { return; } if ($this->contains_current_element($post->post_content, $post)) { if ($this->is_preview()) { $this->setup_preview(); } else { $this->setup(); } } } /** * Runs early on the request lifecycle as soon as we detect the shortcode is present. * * @since 2.0.0 * @return void */ public function setup() {} /** * Allows the setup in the context of previews. * * @since 2.0.0 * @return void */ public function setup_preview() {} /** * Checks content to see if the current element is present. * * This check uses different methods, covering classic shortcodes, * blocks. It also adds a generic filter so developers can * add additional tests for different builders and so on. * * @since 2.0.0 * * @param string $content The content that might contain the element. * @param null|\WP_Post $post The WP Post, if it exists. * @return bool */ protected function contains_current_element($content, $post = null) { /** * If parameters where pre-loaded, * we can skip the entire check and return true. */ if (is_array($this->pre_loaded_attributes)) { return true; } /* * First, check for default shortcodes * saved as regular post content. */ $shortcode = $this->get_shortcode_id(); if (has_shortcode($content, $shortcode)) { $this->pre_loaded_attributes = $this->maybe_extract_arguments($content, 'shortcode'); $this->actually_loaded = true; return true; } /* * Handle the Block Editor * and Gutenberg. */ $block = $this->get_id(); if (has_block($block, $content)) { $this->pre_loaded_attributes = $this->maybe_extract_arguments($content, 'block'); $this->actually_loaded = true; return true; } /* * Runs generic version so plugins can extend it. */ $this->pre_loaded_attributes = $this->maybe_extract_arguments($content, 'other'); $contains_element = false; /** * Last option is to check for the post force setting. */ if ($post && get_post_meta($post->ID, '_wu_force_elements_loading', true)) { $contains_element = true; } /** * Allow developers to change the results of the initial search. * * This is useful for third-party builders and such. * * @since 2.0.0 * @param bool $contains_elements If the element is contained on the content. * @param string $content The content being examined. * @param self The current element. */ return apply_filters('wu_contains_element', $contains_element, $content, $this, $post); } /** * Tries to extract element arguments depending on the element type. * * @since 2.0.0 * * @param string $content The content to parse. * @param string $type The element type. Can be one of shortcode, block, and other. * @return false|array */ protected function maybe_extract_arguments($content, $type = 'shortcode') { if ('shortcode' === $type) { /** * Tries to parse the shortcode out of the content * passed using the WordPress shortcode regex. */ $shortcode_regex = get_shortcode_regex([$this->get_shortcode_id()]); preg_match_all('/' . $shortcode_regex . '/', $content, $matches, PREG_SET_ORDER); return ! empty($matches) ? shortcode_parse_atts($matches[0][3]) : false; } elseif ('block' === $type) { /** * Next, try to parse attrs from blocks * by parsing them out and finding the correct one. */ $block_content = parse_blocks($content); foreach ($block_content as $block) { if ($block['blockName'] === $this->get_id()) { return $block['attrs']; } } return false; } /** * Adds generic filter to allow developers * to extend this parser to deal with additional * builders or plugins. * * @since 2.0.0 * @return false|array */ return apply_filters('wu_element_maybe_extract_arguments', false, $content, $type, $this); } /** * Adds custom CSS to the signup screen. * * @since 2.0.0 * @return void */ public function enqueue_element_scripts(): void { global $post; if ( ! is_a($post, '\WP_Post')) { return; } $should_enqueue_scripts = apply_filters('wu_element_should_enqueue_scripts', false, $post, $this->get_shortcode_id()); if ($should_enqueue_scripts || $this->contains_current_element($post->post_content, $post)) { /** * Triggers the enqueue scripts hook. * * This is used by the element to hook its * register_scripts method. * * @since 2.0.0 */ do_action("wu_{$this->id}_scripts", $post, $this); } } /** * Tries to parse the shortcode content on page load. * * This allow us to have access to parameters before the shortcode * gets actually parsed by the post content functions such as * the_content(). It is useful if you need to access that * date way earlier in the page lifecycle. * * @since 2.0.0 * * @param string $name The parameter name. * @param mixed $default The default value. * @return mixed */ public function get_pre_loaded_attribute($name, $default = false) { if ($this->pre_loaded_attributes === false || ! is_array($this->pre_loaded_attributes)) { return false; } return wu_get_isset($this->pre_loaded_attributes, $name, $default); } /** * Registers the shortcode. * * @since 2.0.0 * @return void */ public function register_shortcode(): void { if (wu_get_current_site()->get_type() === Site_Type::CUSTOMER_OWNED && is_admin() === false) { return; } add_shortcode($this->get_shortcode_id(), [$this, 'display']); } /** * Registers the forms. * * @since 2.0.0 * @return void */ public function register_form(): void { /* * Add Generator Forms */ wu_register_form( "shortcode_{$this->id}", [ 'render' => [$this, 'render_generator_modal'], 'handler' => '__return_empty_string', 'capability' => 'manage_network', ] ); /* * Add Customize Forms */ wu_register_form( "customize_{$this->id}", [ 'render' => [$this, 'render_customize_modal'], 'handler' => [$this, 'handle_customize_modal'], 'capability' => 'manage_network', ] ); } /** * Adds the modal to copy the shortcode for this particular element. * * @since 2.0.0 * @return void */ public function render_generator_modal(): void { $fields = $this->fields(); $defaults = $this->defaults(); $state = []; foreach ($fields as $field_slug => &$field) { if ($field['type'] === 'header' || $field['type'] === 'note') { unset($fields[ $field_slug ]); continue; } /* * Additional State. * * We need to keep track of the state * specially when we're dealing with * complex fields, such as group. */ $additional_state = []; if ($field['type'] === 'group') { foreach ($field['fields'] as $sub_field_slug => &$sub_field) { $sub_field['html_attr'] = [ 'v-model.lazy' => "attributes.{$sub_field_slug}", ]; $additional_state[ $sub_field_slug ] = wu_request($sub_field_slug, wu_get_isset($defaults, $sub_field_slug)); } continue; } /* * Set v-model */ $field['html_attr'] = [ 'v-model.lazy' => "attributes.{$field_slug}", ]; $required = wu_get_isset($field, 'required'); if (wu_get_isset($field, 'required')) { $shows = []; foreach ($required as $key => $value) { $value = is_string($value) ? "\"$value\"" : $value; $shows[] = "attributes.{$key} == $value"; } $field['wrapper_html_attr'] = [ 'v-show' => implode(' && ', $shows), ]; $state[ $field_slug . '_shortcode_requires' ] = $required; } $state[ $field_slug ] = wu_request($field_slug, wu_get_isset($defaults, $field_slug)); } $fields['shortcode_result'] = [ 'type' => 'note', 'wrapper_classes' => 'sm:wu-block', 'desc' => '
' . __('Result', 'wp-ultimo') . '
', ]; $fields['submit_copy'] = [ 'type' => 'submit', 'title' => __('Copy Shortcode', 'wp-ultimo'), 'value' => 'edit', 'classes' => 'button button-primary wu-w-full wu-copy', 'wrapper_classes' => 'wu-items-end', 'html_attr' => [ 'data-clipboard-action' => 'copy', 'data-clipboard-target' => '#wu-shortcode', ], ]; $form = new \WP_Ultimo\UI\Form( $this->id, $fields, [ 'views' => 'admin-pages/fields', 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0 wu-w-full', 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', 'html_attr' => [ 'data-wu-app' => "{$this->id}_generator", 'data-state' => wu_convert_to_state( [ 'id' => $this->get_shortcode_id(), 'defaults' => $defaults, 'attributes' => $state, ] ), ], ] ); echo '
'; $form->render(); echo '
'; } /** * Adds the modal customize the widget block * * @since 2.0.0 * @return void */ public function render_customize_modal(): void { $fields = []; $fields['hide'] = [ 'type' => 'toggle', 'title' => __('Hide Element', 'wp-ultimo'), 'desc' => __('Be careful. Hiding an element from the account page might remove important functionality from your customers\' reach.', 'wp-ultimo'), 'value' => $this->hidden_by_default, 'classes' => 'button button-primary wu-w-full', 'wrapper_classes' => 'wu-items-end', ]; $fields = array_merge($fields, $this->fields()); $saved_settings = $this->get_widget_settings(); $defaults = $this->defaults(); $state = array_merge($defaults, $saved_settings); foreach ($fields as $field_slug => &$field) { if ($field['type'] === 'header') { unset($fields[ $field_slug ]); continue; } $value = wu_get_isset($saved_settings, $field_slug, null); if (null !== $value) { $field['value'] = $value; } } $fields['save_line'] = [ 'type' => 'group', 'classes' => 'wu-justify-between', 'wrapper_classes' => 'wu-bg-gray-100', 'fields' => [ 'restore' => [ 'type' => 'submit', 'title' => __('Reset Settings', 'wp-ultimo'), 'value' => 'edit', 'classes' => 'button', 'wrapper_classes' => 'wu-mb-0', ], 'submit' => [ 'type' => 'submit', 'title' => __('Save Changes', 'wp-ultimo'), 'value' => 'edit', 'classes' => 'button button-primary', 'wrapper_classes' => 'wu-mb-0', ], ], ]; $form = new \WP_Ultimo\UI\Form( $this->id, $fields, [ 'views' => 'admin-pages/fields', 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0', 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', 'html_attr' => [ 'data-wu-app' => "{$this->id}_customize", 'data-state' => wu_convert_to_state($state), ], ] ); echo '
'; $form->render(); echo '
'; } /** * Saves the customization settings for a given widget. * * @since 2.0.0 * @return void */ public function handle_customize_modal(): void { $settings = []; if (wu_request('submit') !== 'restore') { $fields = $this->fields(); $fields['hide'] = [ 'type' => 'toggle', ]; foreach ($fields as $field_slug => $field) { $setting = wu_request($field_slug, false); if (false !== $setting || $field['type'] === 'toggle') { $settings[ $field_slug ] = $setting; } } } $this->save_widget_settings($settings); wp_send_json_success( [ 'send' => [ 'scope' => 'window', 'function_name' => 'wu_block_ui', 'data' => '#wpcontent', ], 'redirect_url' => add_query_arg('updated', 1, $_SERVER['HTTP_REFERER']), ] ); } /** * Registers scripts and styles necessary to render this. * * @since 2.0.0 * @return void */ public function register_default_scripts(): void { wp_enqueue_style('wu-admin'); } /** * Registers scripts and styles necessary to render this. * * @since 2.0.0 * @return void */ public function register_scripts() {} /** * Loads dependencies that might not be available at render time. * * @since 2.0.0 * @return void */ public function dependencies() {} /** * Returns the ID of this UI element. * * @since 2.0.0 * @return string */ public function get_id() { return sprintf('wp-ultimo/%s', $this->id); } /** * Returns the ID of this UI element. * * @since 2.0.0 * @return string */ public function get_shortcode_id() { return str_replace('-', '_', sprintf('wu_%s', $this->id)); } /** * Treats the attributes before passing them down to the output method. * * @since 2.0.0 * * @param array $atts The element attributes. * @return string */ public function display($atts) { if ( ! $this->should_display()) { return; // bail if the display was set to false. } $this->dependencies(); $atts = wp_parse_args($atts, $this->defaults()); /* * Account for the 'className' Gutenberg attribute. */ $atts['className'] = trim('wu-' . $this->id . ' wu-element ' . wu_get_isset($atts, 'className', '')); /* * Pass down the element so we can use helpers. */ $atts['element'] = $this; return call_user_func([$this, 'output'], $atts); } /** * Retrieves a cleaned up version of the content. * * This method strips out vue reactivity tags and more. * * @since 2.0.0 * * @param array $atts The element attributes. * @return string */ public function display_template($atts) { $content = $this->display($atts); $content = str_replace( [ 'v-', 'data-wu', 'data-state', ], 'inactive-', $content ); $content = str_replace( [ '{{', '}}', ], '', $content ); return $content; } /** * Checks if we need to display admin management attachments. * * @since 2.0.0 * * @return bool */ public function should_display_customize_controls() { return apply_filters('wu_element_display_super_admin_notice', current_user_can('manage_network'), $this); } /** * Adds the element as a inline block, without the admin widget frame. * * @since 2.0.0 * * @param string $screen_id The screen id. * @param string $hook The hook to add the content to. Defaults to admin_notices. * @param array $atts Array containing the shortcode attributes. * @return void */ public function as_inline_content($screen_id, $hook = 'admin_notices', $atts = []): void { if ( ! function_exists('get_current_screen')) { _doing_it_wrong(__METHOD__, __('An element can not be loaded as inline content unless the get_current_screen() function is already available.', 'wp-ultimo'), '2.0.0'); return; } $screen = get_current_screen(); if ( ! $screen || $screen->id !== $screen_id) { return; } /* * Run the setup in this case; */ $this->setup(); if ( ! $this->should_display()) { return; // bail if the display was set to false. } if (empty($atts)) { $atts = $this->get_widget_settings(); } $control_classes = ''; if (wu_get_isset($atts, 'hide', $this->hidden_by_default)) { if ( ! $this->should_display_customize_controls()) { return; } $control_classes = 'wu-customize-mode wu-opacity-25'; } add_action( $hook, function () use ($atts, $control_classes) { echo '
'; echo '
'; echo $this->display($atts); echo '
'; $this->super_admin_notice(); echo '
'; } ); do_action("wu_{$this->id}_scripts", null, $this); } /** * Save the widget options. * * @since 2.0.0 * * @param array $settings The settings to save. Key => value array. * @return void */ public function save_widget_settings($settings): void { $key = wu_replace_dashes($this->id); wu_save_setting("widget_{$key}_settings", $settings); } /** * Retrieves the settings for a particular widget. * * @since 2.0.0 * @return array */ public function get_widget_settings() { $key = wu_replace_dashes($this->id); return wu_get_setting("widget_{$key}_settings", []); } /** * Adds the element as a metabox. * * @since 2.0.0 * * @param string $screen_id The screen id. * @param string $position Position on the screen. * @param array $atts Array containing the shortcode attributes. * @return void */ public function as_metabox($screen_id, $position = 'normal', $atts = []): void { $this->setup(); if ( ! $this->should_display()) { return; // bail if the display was set to false. } if (empty($atts)) { $atts = $this->get_widget_settings(); } $control_classes = ''; if (wu_get_isset($atts, 'hide')) { if ( ! $this->should_display_customize_controls()) { return; } $control_classes = 'wu-customize-mode wu-opacity-25'; } add_meta_box( "wp-ultimo-{$this->id}-element", $this->get_title(), function () use ($atts, $control_classes) { echo '
'; echo $this->display($atts); echo '
'; $this->super_admin_notice(); }, $screen_id, $position, 'high' ); do_action("wu_{$this->id}_scripts", null, $this); } /** * Adds note for super admins. * * Adds an admin notice to let the super admin know * how to use the widgets. * * @since 2.0.0 * @return void */ public function super_admin_notice(): void { $should_display = $this->should_display_customize_controls(); if ($should_display) { // translators: %1$s is the URL to the customize modal. %2$s is the URL of the shortcode generation modal $message = __('Customize this element, or generate a shortcode to use it on the front-end!', 'wp-ultimo'); $message .= wu_tooltip(__('You are seeing this because you are a super admin', 'wp-ultimo')); $link_shortcode = wu_get_form_url("shortcode_{$this->id}"); $link_customize = wu_get_form_url("customize_{$this->id}"); $text = sprintf( $message, $link_customize, $link_shortcode ); $html = '
' . $text . '
'; echo $html; } } /** * Checks if we are in a preview context. * * @since 2.0.0 * @return boolean */ public function is_preview() { $is_preview = false; if (did_action('init')) { $is_preview = wu_request('preview') && current_user_can('edit_posts'); } return apply_filters('wu_element_is_preview', false, $this); } /** * Get controls whether or not the widget and element should display.. * * @since 2.0.0 * @return boolean */ public function should_display() { return $this->display || $this->is_preview(); } /** * Set controls whether or not the widget and element should display.. * * @since 2.0.0 * @param boolean $display Controls whether or not the widget and element should display. * @return void */ public function set_display($display): void { $this->display = $display; } /** * Checks if the current element was actually loaded. * * @since 2.0.11 * @return boolean */ public function is_actually_loaded() { return $this->actually_loaded; } }