viewMarkup = $viewMarkup;
$this->cardMarkup = $cardMarkup;
$this->options = $options;
$this->cache = $cache;
$this->acfViewFactory = $acfViewFactory;
$this->acfCardFactory = $acfCardFactory;
$this->bufferLevel = 0;
}
// static, as called also in AcfGroup
public static function isAcfProPluginAvailable(): bool
{
return class_exists('acf_pro');
}
public static function getThemeTextDomain(): string
{
return (string)wp_get_theme()->get('TextDomain');
}
public static function getLabelTranslation(string $label, string $textDomain = ''): string
{
$textDomain = $textDomain ?: self::getThemeTextDomain();
// escape quotes to keep compatibility with the generated translation file
// (quotes there escaped to prevent breaking the PHP string)
$label = str_replace("'", "'", $label);
$label = str_replace('"', """, $label);
$translation = __($label, $textDomain);
$translation = str_replace("'", "'", $translation);
$translation = str_replace(""", '"', $translation);
return $translation;
}
protected function getViewPreviewJsData(): array
{
$jsData = [
'HTML' => '',
'CSS' => '',
];
global $post;
if (!$this->isCPTScreen(AcfViews::NAME) ||
'publish' !== $post->post_status) {
return $jsData;
}
$acfViewData = $this->cache->getAcfViewData($post->ID);
$previewPostId = $acfViewData->previewPost ?: 0;
if ($previewPostId) {
$postData = new Post($previewPostId, [], false, get_current_user_id());
// without minify, it's a preview
$viewHTML = $this->acfViewFactory->createAndGetHtml(
$postData,
$post->ID,
0,
false,
);
} else {
// $this->viewMarkup->getMarkup give TWIG, there is no sense to show it
// so the HTML is empty until the preview Post ID is selected
$viewHTML = '';
}
// amend to allow work the '#view' alias
$viewHTML = str_replace('class="acf-view ', 'id="view" class="acf-view ', $viewHTML);
$jsData['HTML'] = htmlentities($viewHTML, ENT_QUOTES);
$jsData['CSS'] = htmlentities($acfViewData->getCssCode(false, true), ENT_QUOTES);
$jsData['HOME'] = get_site_url();
return $jsData;
}
protected function getCardPreviewJsData(): array
{
$jsData = [
'HTML' => '',
'CSS' => '',
];
global $post;
if (!$this->isCPTScreen(AcfCards::NAME) ||
'publish' !== $post->post_status) {
return $jsData;
}
$acfCardData = $this->cache->getAcfCardData($post->ID);
$acfCardHtml = $this->acfCardFactory->createAndGetHtml($acfCardData, 1, false);
$viewId = $this->cache->getPostIdByUniqueId($acfCardData->acfViewId, AcfViews::NAME);
if (!$viewId) {
return $jsData;
}
$acfViewData = $this->cache->getAcfViewData($viewId);
// amend to allow work the '#card' alias
$viewHTML = str_replace(
'class="acf-card ',
'id="card" class="acf-card ',
$acfCardHtml
);
$jsData['HTML'] = htmlentities($viewHTML, ENT_QUOTES);
// Card CSS without minification as it's for views' purposes
$jsData['CSS'] = htmlentities($acfCardData->getCssCode(false, true), ENT_QUOTES);
$jsData['VIEW_CSS'] = htmlentities($acfViewData->getCssCode(), ENT_QUOTES);
$jsData['HOME'] = get_site_url();
return $jsData;
}
protected function enqueueAdminAssets(string $currentBase, array $jsData = []): void
{
// add, edit pages
if ('post' === $currentBase) {
$jsData = array_merge_recursive($jsData, [
'markupTextarea' => [
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_MARKUP),
'isReadOnly' => true,
'mode' => '_twig',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CSS_CODE),
'isReadOnly' => false,
'mode' => '_css',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_JS_CODE),
'isReadOnly' => false,
'mode' => '_js',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CUSTOM_MARKUP),
'isReadOnly' => false,
'mode' => '_twig',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_PHP_VARIABLES),
'isReadOnly' => false,
'mode' => '_php',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_MARKUP),
'isReadOnly' => true,
'mode' => '_twig',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_CSS_CODE),
'isReadOnly' => false,
'mode' => '_css',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_JS_CODE),
'isReadOnly' => false,
'mode' => '_js',
],
[
'idSelector' => AcfCardData::getAcfFieldName(
AcfCardData::FIELD_CUSTOM_MARKUP
),
'isReadOnly' => false,
'mode' => '_twig',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_QUERY_PREVIEW),
'isReadOnly' => true,
'mode' => '_twig',
],
],
'fieldSelect' => [
[
'mainSelectId' => Item::getAcfFieldName(Item::FIELD_GROUP),
'subSelectId' => Field::getAcfFieldName(Field::FIELD_KEY),
'identifierInputId' => Field::getAcfFieldName(Field::FIELD_ID),
],
[
'mainSelectId' => AcfCardData::getAcfFieldName(
AcfCardData::FIELD_ORDER_BY_META_FIELD_GROUP
),
'subSelectId' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_ORDER_BY_META_FIELD_KEY),
'identifierInputId' => '',
],
[
'mainSelectId' => Field::getAcfFieldName(Field::FIELD_KEY),
'subSelectId' => RepeaterField::getAcfFieldName(RepeaterField::FIELD_KEY),
'identifierInputId' => RepeaterField::getAcfFieldName(RepeaterField::FIELD_ID),
],
[
'mainSelectId' => MetaField::getAcfFieldName(MetaField::FIELD_GROUP),
'subSelectId' => MetaField::getAcfFieldName(MetaField::FIELD_FIELD_KEY),
'identifierInputId' => '',
],
[
'mainSelectId' => TaxField::getAcfFieldName(TaxField::FIELD_TAXONOMY),
'subSelectId' => TaxField::getAcfFieldName(TaxField::FIELD_TERM),
'identifierInputId' => '',
],
],
'viewPreview' => $this->getViewPreviewJsData(),
'cardPreview' => $this->getCardPreviewJsData(),
'ajaxUrl' => admin_url('admin-ajax.php'),
]);
wp_enqueue_style(
AcfViews::NAME . '_cpt-item',
$this->getAssetsUrl('admin/css/cpt-item.min.css'),
[],
$this->getVersion()
);
// jquery is necessary for select2 events
wp_enqueue_script(
AcfViews::NAME . '_cpt-item',
$this->getAssetsUrl('admin/js/cpt-item.min.js'),
// make sure acf is loaded
['jquery', 'acf-input',],
$this->getVersion(),
[
'in_footer' => true,
// in footer, so if we need to include others, like 'ace.js' we can include in header
]
);
wp_localize_script(AcfViews::NAME . '_cpt-item', 'acf_views', $jsData);
}
// 'edit' means 'list page'
if ('edit' === $currentBase) {
wp_enqueue_style(
AcfViews::NAME . '_list-page',
$this->getAssetsUrl('admin/css/list-page.min.css'),
[],
$this->getVersion()
);
}
// 'dashboard' for all the custom pages (but not for edit/add pages)
if (0 === strpos($currentBase, 'acf_views_page_')) {
wp_enqueue_style(
AcfViews::NAME . '_page',
$this->getAssetsUrl('admin/css/dashboard.min.css'),
[],
$this->getVersion()
);
}
// plugin-header for all the pages without exception
wp_enqueue_style(
AcfViews::NAME . '_common',
$this->getAssetsUrl('admin/css/common.min.css'),
[],
$this->getVersion()
);
}
protected function printPluginsCSS(): string
{
return '';
}
public function getName(): string
{
return __('ACF Views', 'acf-views');
}
public function getSlug(): string
{
return $this->slug;
}
public function getShortSlug(): string
{
return $this->shortSlug;
}
public function getVersion(): string
{
// return strval(time());
return $this->version;
}
public function isProVersion(): bool
{
return $this->isProVersion;
}
public function getAssetsUrl(string $file): string
{
return plugin_dir_url(__FILE__) . 'assets/' . $file;
}
public function getAcfProAssetsUrl(string $file): string
{
return plugin_dir_url(__FILE__) . 'AcfPro/assets/' . $file;
}
public function isAcfPluginAvailable(bool $isProOnly = false): bool
{
// don't use 'is_plugin_active()' as the function available lately
return static::isAcfProPluginAvailable() ||
(!$isProOnly && class_exists('ACF'));
}
public function startBuffering(): void
{
ob_start();
$this->bufferLevel = ob_get_level();
}
public function printStylesStub(): void
{
echo '';
}
public function showWarningAboutInactiveAcfPlugin(): void
{
if ($this->isAcfPluginAvailable()) {
return;
}
$acfPluginInstallLink = get_admin_url(null, static::ACF_INSTALL_URL);
$acfFree = 'https://wordpress.org/plugins/advanced-custom-fields/';
$acfPro = 'https://www.advancedcustomfields.com/pro/';
echo sprintf(
'
',
__('"ACF Views" requires', 'acf-views'),
$acfPluginInstallLink,
__('Advanced Custom Fields', 'acf-views'),
$acfFree,
__('free', 'acf-views'),
__('or', 'acf-views'),
$acfPro,
__('pro', 'acf-views'),
__('to be installed and activated.', 'acf-views'),
);
}
public function showWarningAboutOpcacheIssue(): void
{
if (!function_exists('ini_get') ||
'0' !== ini_get('opcache.save_comments')) {
return;
}
$readMoreLink = sprintf(
'%s',
self::CONFLICTS_URL,
__('Read more', 'acf-views')
);
printf(
'',
__('Compatibility issue detected! "ACF Views" plugin requires "PHPDoc" comments in code.', 'acf-views'),
__(
'Please change the "opcache.save_comments" option in your php.ini file to the default value of "1" on your hosting.',
'acf-views'
),
$readMoreLink
);
}
public function enqueueAdminScripts(): void
{
$currentScreen = get_current_screen();
if (!$currentScreen ||
(!in_array($currentScreen->id, [AcfViews::NAME, AcfCards::NAME,], true) &&
!in_array($currentScreen->post_type, [AcfViews::NAME, AcfCards::NAME], true))) {
return;
}
$this->enqueueAdminAssets($currentScreen->base);
}
public function printCustomAssets(): void
{
$allJsCode = '';
$allCssCode = $this->printPluginsCSS();
$views = $this->acfViewFactory->getRenderedViews();
foreach ($views as $view) {
$cssCode = $view->getCssCode();
// 'minify' JS
$jsCode = str_replace(["\t", "\n", "\r"], '', $view->jsCode);
$jsCode = trim($jsCode);
// no escaping, it's a CSS code, so e.g '.a > .b' shouldn't be escaped
$allCssCode .= $cssCode ?
sprintf("\n/*view-%s*/\n%s", $view->getSource(), $cssCode) :
'';
$allJsCode .= $jsCode ?
sprintf("\n/*view-%s*/\n%s", $view->getSource(), $jsCode) :
'';
}
$cards = $this->acfCardFactory->getRenderedCards();
foreach ($cards as $card) {
$cssCode = $card->getCssCode();
// 'minify' JS
$jsCode = str_replace(["\t", "\n", "\r"], '', $card->jsCode);
$jsCode = trim($jsCode);
// no escaping, it's a CSS code, so e.g '.a > .b' shouldn't be escaped
$allCssCode .= $cssCode ?
sprintf("\n/*card-%s*/\n%s", $card->getSource(), $cssCode) :
'';
$allJsCode .= $jsCode ?
sprintf("\n/*card-%s*/\n%s", $card->getSource(), $jsCode) :
'';
}
if (!$allCssCode &&
!$allJsCode) {
// do not close the buffer, if it's not ours
// (then ours will be closed automatically with the end of script execution)
if (ob_get_level() === $this->bufferLevel) {
echo ob_get_clean();
}
return;
}
// close previous buffers. Some plugins may not close, if detect that ob_get_level() is another than was
// e.g. 'lightbox-photoswipe'
while (ob_get_level() > $this->bufferLevel) {
echo ob_get_clean();
}
$pageContent = ob_get_clean();
$cssTag = $allCssCode ?
sprintf("", $allCssCode) :
'';
$pageContent = str_replace('', $cssTag, $pageContent);
echo $pageContent;
if ($allJsCode) {
printf("", $allJsCode);
}
}
public function enqueueGoogleMapsJS(): void
{
if (!function_exists('acf_get_setting') ||
!$this->acfViewFactory->getMaps()) {
return;
}
$apiData = apply_filters('acf/fields/google_map/api', []);
$key = $apiData['key'] ?? '';
$key = !$key ?
acf_get_setting('google_api_key') :
$key;
if (!$key) {
return;
}
wp_enqueue_script(
AcfViews::NAME . '_maps',
$this->getAssetsUrl('front/js/maps.min.js'),
[],
$this->getVersion(),
[
'in_footer' => true,
]
);
wp_localize_script(
AcfViews::NAME . '_maps',
'acfViewsMaps',
$this->acfViewFactory->getMaps()
);
wp_enqueue_script(
AcfViews::NAME . '_google-maps',
sprintf('https://maps.googleapis.com/maps/api/js?key=%s&callback=acfViewsGoogleMaps', $key),
[
// setup deps, to make sure loaded only after plugin's maps.min.js
AcfViews::NAME . '_maps',
],
null,
[
'in_footer' => true,
]
);
}
public function isCPTScreen(string $cptName, array $targetBase = ['post', 'add',]): bool
{
$currentScreen = get_current_screen();
$isTargetPost = in_array($currentScreen->id, [$cptName,], true) ||
in_array($currentScreen->post_type, [$cptName], true);
// base = edit (list management), post (editing), add (adding)
return $isTargetPost &&
in_array($currentScreen->base, $targetBase, true);
}
public function deactivateOtherInstances(string $activatedPlugin): void
{
if (!in_array($activatedPlugin, ['acf-views/acf-views.php', 'acf-views-pro/acf-views-pro.php'], true)) {
return;
}
$pluginToDeactivate = 'acf-views/acf-views.php';
$deactivatedNoticeId = 1;
// If we just activated the free version, deactivate the pro version.
if ($activatedPlugin === $pluginToDeactivate) {
$pluginToDeactivate = 'acf-views-pro/acf-views-pro.php';
$deactivatedNoticeId = 2;
}
if (is_multisite() &&
is_network_admin()) {
$activePlugins = (array)get_site_option('active_sitewide_plugins', []);
$activePlugins = array_keys($activePlugins);
} else {
$activePlugins = (array)get_option('active_plugins', []);
}
foreach ($activePlugins as $pluginBasename) {
if ($pluginToDeactivate !== $pluginBasename) {
continue;
}
$this->options->setTransient(
Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES,
$deactivatedNoticeId,
1 * HOUR_IN_SECONDS
);
deactivate_plugins($pluginBasename);
return;
}
}
// notice when either Basic or Pro was automatically deactivated
public function showPluginDeactivatedNotice(): void
{
$deactivatedNoticeId = (int)$this->options->getTransient(Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES);
// not set = false = 0
if (!in_array($deactivatedNoticeId, [1, 2,], true)) {
return;
}
$message = sprintf(
'%s "%s".',
__(
"'ACF Views' and 'ACF Views Pro' should not be active at the same time. We've automatically deactivated",
'acf-views'
),
1 === $deactivatedNoticeId ?
__('ACF Views', 'acf-views') :
__('ACF Views Pro', 'acf-views')
);
$this->options->deleteTransient(Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES);
echo sprintf(
'',
$message
);
}
public function amendProFieldLabelAndInstruction(array $field): array
{
$isProField = !$this->isProVersion() &&
key_exists('a-pro', $field);
$isAcfProField = !$this->isAcfPluginAvailable(true) &&
key_exists('a-acf-pro', $field);
if (!$isProField &&
!$isAcfProField) {
return $field;
}
$type = $field['type'] ?? '';
$field['label'] = $field['label'] ?? '';
$field['instructions'] = $field['instructions'] ?? '';
if ($isProField) {
if ('tab' === $type) {
$field['label'] = $field['label'] . ' (Pro)';
} else {
$field['instructions'] = sprintf(
'%s %s %s',
Plugin::PRO_VERSION_URL,
__('Upgrade to Pro', 'acf-views'),
__('to unlock.', 'acf-views'),
$field['instructions']
);
}
}
if ($isAcfProField) {
$field['instructions'] = sprintf(
'(%s %s) %s',
'https://www.advancedcustomfields.com/pro/',
__('ACF Pro', 'acf-views'),
__('version is required for this feature', 'acf-views'),
$field['instructions']
);
}
return $field;
}
public function addClassToAdminProFieldClasses(array $wrapper, array $field): array
{
$isProField = !$this->isProVersion() &&
key_exists('a-pro', $field);
$isAcfProField = !$this->isAcfPluginAvailable(true) &&
key_exists('a-acf-pro', $field);
if (!$isProField &&
!$isAcfProField) {
return $wrapper;
}
if (!key_exists('class', $wrapper)) {
$wrapper['class'] = '';
}
$wrapper['class'] .= ' acf-views-pro';
return $wrapper;
}
public function makeEnqueueJSAsync(string $tag, string $handle): string
{
if (!in_array($handle, [
AcfViews::NAME . '_maps',
AcfViews::NAME . '_google-maps'
], true)) {
return $tag;
}
// defer, not async as order should be kept (google-maps will call a callback from maps' js)
return str_replace(' src', ' defer src', $tag);
}
public function getAdminUrl(
string $page = '',
string $cptName = AcfViews::NAME,
string $base = 'edit.php'
): string {
$pageArg = $page ?
'&page=' . $page :
'';
// don't use just '/wp-admin/x' as some websites can have custom admin url, like 'wp.org/wordpress/wp-admin'
$pageUrl = get_admin_url(null, $base . '?post_type=');
return $pageUrl . $cptName . $pageArg;
}
public function printSurveyLink(string $html): string
{
if (!$this->isCPTScreen(AcfViews::NAME, ['post', 'add', 'edit',]) &&
!$this->isCPTScreen(AcfCards::NAME, ['post', 'add', 'edit',])) {
return $html;
}
$content = sprintf(
'%s %s %s %s.',
__('Thank you for creating with', 'acf-views'),
'https://wordpress.org/',
__('WordPress', 'acf-views'),
__('and', 'acf-views'),
self::BASIC_VERSION_URL,
__('ACF Views', 'acf-views')
);
$content .= " " . sprintf(
"%s %s %s",
__('Take', 'acf-views'),
self::SURVEY_URL,
__('2 minute survey', 'acf-views'),
__('to improve the ACF Views plugin.', 'acf-views')
);
return sprintf(
'',
$content
);
}
public function setHooks(): void
{
add_action('admin_notices', [$this, 'showWarningAboutInactiveAcfPlugin']);
add_action('admin_notices', [$this, 'showWarningAboutOpcacheIssue']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
add_action('wp_footer', [$this, 'enqueueGoogleMapsJS']);
// printCustomAssets() contains ob_get_clean, so must be executed after all other scripts
add_action('wp_footer', [$this, 'printCustomAssets'], 9999);
add_action('activated_plugin', [$this, 'deactivateOtherInstances']);
add_action('pre_current_active_plugins', [$this, 'showPluginDeactivatedNotice']);
add_action('wp_head', [$this, 'printStylesStub']);
// don't use 'get_header', as it doesn't work in blocks theme
add_action('template_redirect', [$this, 'startBuffering']);
add_filter('acf/prepare_field', [$this, 'amendProFieldLabelAndInstruction']);
add_filter('acf/field_wrapper_attributes', [$this, 'addClassToAdminProFieldClasses'], 10, 2);
add_filter('script_loader_tag', [$this, 'makeEnqueueJSAsync'], 10, 2);
add_filter('admin_footer_text', [$this, 'printSurveyLink']);
}
}