register_routes();
});
// Initialize Shortcode Manager
Shortcode_Manager::get_instance();
// Register custom widgets with Elementor
add_action('elementor/widgets/register', [$this, 'register_custom_widgets']);
}
/** Handle prefix for the per-instance scripts carrying widget custom JS. */
const INLINE_JS_HANDLE = 'jltma-widget-builder-inline';
/**
* Normalise an includes payload to the canonical shape, dropping entries
* that could not be enqueued anyway.
*
* Every consumer used to re-implement its own subset of these checks, so a
* library with a relative src was skipped by the Elementor widget but still
* enqueued by the shortcode. One shape, validated once.
*
* @param mixed $includes Raw includes payload.
* @return array {
* @type array $css_libraries List of ['handle' => string, 'src' => string, 'dependencies' => string[]].
* @type array $js_libraries Same shape.
* }
*/
public static function normalize_includes($includes) {
$normalized = ['css_libraries' => [], 'js_libraries' => []];
if (!is_array($includes)) {
return $normalized;
}
foreach (array_keys($normalized) as $group) {
if (empty($includes[$group]) || !is_array($includes[$group])) {
continue;
}
foreach ($includes[$group] as $lib) {
if (!is_array($lib) || empty($lib['handle']) || empty($lib['src'])) {
continue;
}
// Only absolute URLs: these become wp_enqueue_* sources.
if (!filter_var($lib['src'], FILTER_VALIDATE_URL)) {
continue;
}
$deps = (!empty($lib['dependencies']) && is_array($lib['dependencies']))
? array_values(array_map('sanitize_text_field', $lib['dependencies']))
: [];
$normalized[$group][] = [
'handle' => sanitize_text_field($lib['handle']),
'src' => esc_url_raw($lib['src']),
'dependencies' => $deps,
];
}
}
return $normalized;
}
/**
* The external CSS/JS libraries a widget may load, after the premium gate.
*
* Declaring external libraries is a premium capability. The free build
* resolves to an empty set; the Pro build returns the stored libraries by
* filtering `master_addons/widget_builder/render_includes` — see
* MasterAddons\Pro\Classes\Pro_Modules. The stored value is passed as the
* second argument, already normalised.
*
* @param int $widget_id Widget post ID.
* @return array Normalised includes.
*/
public static function get_widget_includes($widget_id) {
$stored = self::normalize_includes(get_post_meta($widget_id, '_jltma_widget_includes', true));
$includes = apply_filters(
'master_addons/widget_builder/render_includes',
self::normalize_includes(null),
$stored,
$widget_id
);
return self::normalize_includes($includes);
}
/**
* Are we rendering for the Elementor editor (canvas, preview iframe, or an
* editor ajax round-trip) rather than for a visitor?
*
* @return bool
*/
public static function is_editor_context() {
if (!class_exists('\Elementor\Plugin')) {
return is_admin();
}
$elementor = \Elementor\Plugin::$instance;
if (is_admin() || (isset($elementor->editor) && $elementor->editor->is_edit_mode())) {
return true;
}
return isset($elementor->preview) && $elementor->preview->is_preview_mode();
}
/**
* Queue a widget's rendered custom JS for the footer.
*
* Widget markup is emitted from inside the `the_content` filter chain, and
* core's convert_chars() rewrites every bare `&` in that output to `&`
* without skipping #is', '', $code);
$code = preg_replace('#?script\b[^>]*>#i', '', $code);
return $code;
}
/**
* Strip PHP tags and