acf = $acf; $this->viewMarkup = $viewMarkup; $this->cardMarkup = $cardMarkup; $this->queryBuilder = $queryBuilder; $this->options = $options; $this->cache = $cache; } protected static function getErrorMarkup(string $error): string { return "

{$error}

"; } // static, as called also in AcfGroup public static function isAcfProPluginAvailable(): bool { return class_exists('acf_pro'); } protected function getAcfView(Post $dataPost, int $viewId, int $pageId): AcfView { $viewGroup = $this->cache->getAcfViewData($viewId); // don't use the 'AcfViewData->markup' field, as user can override it (and it shouldn't be supported) $viewMarkup = $this->viewMarkup->getMarkup($viewGroup, $pageId); return new AcfView($this->acf, $viewGroup, $dataPost, $pageId, $viewMarkup); } protected function getAcfCard(AcfCardData $acfCardData): AcfCard { return new AcfCard($acfCardData, $this->queryBuilder, $this->cardMarkup); } 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) { $acfView = $this->getAcfView(new Post($previewPostId), $post->ID, 0); $acfView->insertFields(); // without minify, it's a preview $viewHTML = $acfView->getHTML(false); } else { $viewHTML = $this->viewMarkup->getMarkup($acfViewData, 0); } // 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); $acfCard = $this->getAcfCard($acfCardData); $acfCard->queryPostsAndInsertData(1); $acfViewData = $this->cache->getAcfViewData($acfCardData->acfViewId); // amend to allow work the '#card' alias $viewHTML = str_replace( 'class="acf-card ', 'id="card" class="acf-card ', $acfCard->getHTML(false) ); $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(array $jsData = []): void { $jsData = array_merge_recursive($jsData, [ 'markupTextarea' => [ [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_MARKUP), 'isReadOnly' => true, 'mode' => 'htmlmixed', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CSS_CODE), 'isReadOnly' => false, 'mode' => 'css', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_JS_CODE), 'isReadOnly' => false, 'mode' => 'javascript', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CUSTOM_MARKUP), 'isReadOnly' => false, 'mode' => 'htmlmixed', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_PHP_VARIABLES), 'isReadOnly' => false, 'mode' => 'php', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_MARKUP), 'isReadOnly' => true, 'mode' => 'htmlmixed', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_CSS_CODE), 'isReadOnly' => false, 'mode' => 'css', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_JS_CODE), 'isReadOnly' => false, 'mode' => 'javascript', ], [ 'idSelector' => AcfCardData::getAcfFieldName( AcfCardData::FIELD_CUSTOM_MARKUP ), 'isReadOnly' => false, 'mode' => 'htmlmixed', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_QUERY_PREVIEW), 'isReadOnly' => true, 'mode' => 'javascript', ], ], '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(), ]); wp_enqueue_style(AcfViews::NAME, $this->getAssetsUrl('admin.css'), [], $this->getVersion()); // jquery is necessary for select2 events wp_enqueue_script(AcfViews::NAME, $this->getAssetsUrl('admin.js'), ['jquery',], $this->getVersion()); wp_localize_script(AcfViews::NAME, 'acf_views', $jsData); } public function isShortcodeAvailableForUser(array $userRoles, array $shortcodeArgs): bool { $userWithRoles = (string)($shortcodeArgs['user-with-roles'] ?? ''); $userWithRoles = trim($userWithRoles); $userWithRoles = $userWithRoles ? explode(',', $userWithRoles) : []; $userWithoutRoles = (string)($shortcodeArgs['user-without-roles'] ?? ''); $userWithoutRoles = trim($userWithoutRoles); $userWithoutRoles = $userWithoutRoles ? explode(',', $userWithoutRoles) : []; if (! $userWithRoles && ! $userWithoutRoles) { return true; } $userHasAllowedRoles = ! ! array_intersect($userWithRoles, $userRoles); $userHasDeniedRoles = ! ! array_intersect($userWithoutRoles, $userRoles); if (($userWithRoles && ! $userHasAllowedRoles) || ($userWithoutRoles && $userHasDeniedRoles)) { return false; } return true; } public function getName(): string { return $this->name; } public function getSlug(): string { return $this->slug; } 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/' . $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 setHooks(): void { add_action('admin_notices', [$this, 'showWarningAboutInactiveAcfPlugin']); add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']); add_action('wp_footer', [$this, 'printCustomAssets']); add_action('wp_footer', [$this, 'enqueueGoogleMapsJS']); add_action('activated_plugin', [$this, 'deactivateOtherInstances']); add_action('pre_current_active_plugins', [$this, 'showPluginDeactivatedNotice']); add_shortcode(Plugin::SHORTCODE, [$this, 'acfViewsShortcode']); add_shortcode(Plugin::SHORTCODE_CARDS, [$this, 'acfCardsShortcode']); 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); } 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" plugin requires Advanced Custom Fields plugin to be enabled on your website (either Free or PRO version).' . ' Please enable "Advanced Custom Fields" plugin or "ACF Views" plugin won\'t work properly.

' . '
', $acfPluginInstallLink, $acfFree, $acfPro ); } public function acfCardsShortcode($attrs): string { $attrs = $attrs ? (array)$attrs : []; if (! $this->isShortcodeAvailableForUser(wp_get_current_user()->roles, $attrs)) { return ''; } $cardId = (int)($attrs['card-id'] ?? 0); $acfCardPost = $cardId ? get_post($cardId) : null; $shortCodeName = ':' . self::SHORTCODE_CARDS . ':'; if (! $acfCardPost || ! in_array($acfCardPost->post_type, [AcfCards::NAME,], true) || 'publish' !== $acfCardPost->post_status ) { return self::getErrorMarkup($shortCodeName . ' card-id attribute is missing or wrong'); } $acfCardData = $this->cache->getAcfCardData($cardId); $acfCard = $this->getAcfCard($acfCardData); $acfCard->queryPostsAndInsertData(1); return $acfCard->getHTML(); } public function acfViewsShortcode($attrs): string { $attrs = $attrs ? (array)$attrs : []; if (! $this->isShortcodeAvailableForUser(wp_get_current_user()->roles, $attrs)) { return ''; } $viewId = (int)($attrs['view-id'] ?? 0); $acfViewPost = $viewId ? get_post($viewId) : null; $shortCodeName = ':' . self::SHORTCODE . ':'; if (! $acfViewPost || ! in_array($acfViewPost->post_type, [AcfViews::NAME,], true) || 'publish' !== $acfViewPost->post_status ) { return self::getErrorMarkup($shortCodeName . ' view-id attribute is missing or wrong'); } $dataPostId = (int)($attrs['object-id'] ?? 0); $dataPostId = $dataPostId ?: get_queried_object_id(); $dataPostId = get_post($dataPostId) ? $dataPostId : 0; if (! $dataPostId) { return self::getErrorMarkup($shortCodeName . ' object id is not a post'); } $acfView = $this->getAcfView(new Post($dataPostId), $viewId, get_queried_object_id()); $acfView->insertFields(); return $acfView->getHTML(); } 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))) { $this->enqueueAdminAssets(); } } public function printCustomAssets(): void { $code = ''; $views = $this->viewMarkup->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 $code .= $cssCode ? sprintf("", $view->getSource(), $cssCode) : ''; $code .= $jsCode ? sprintf("", $view->getSource(), $jsCode) : ''; } $cards = $this->cardMarkup->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 $code .= $cssCode ? sprintf("", $card->getSource(), $cssCode) : ''; $code .= $jsCode ? sprintf("", $card->getSource(), $jsCode) : ''; } echo $code; } public function enqueueGoogleMapsJS(): void { if (! function_exists('acf_get_setting')) { return; } $isPageWithMaps = false; $views = $this->viewMarkup->getRenderedViews(); foreach ($views as $view) { if (! $view->isHasMapField()) { continue; } $isPageWithMaps = true; break; } if (! $isPageWithMaps) { 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('maps.min.js'), [], $this->getVersion(), true ); 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, true ); } public function isCPTScreen(string $cptName): 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, ['post', 'add',], 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( '"ACF Views" and "ACF Views PRO" should not be active at the same time. We\'ve automatically deactivated "%s".', 1 === $deactivatedNoticeId ? 'ACF Views' : 'ACF Views PRO' ); $this->options->deleteTransient(Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES); echo sprintf( '
' . '

%s

' . '
', $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( 'Upgrade to PRO to unlock. %s', Plugin::PRO_VERSION_URL, $field['instructions'] ); } } if ($isAcfProField) { $field['instructions'] = sprintf( '(ACF PRO version is required for this feature) %s', 'https://www.advancedcustomfields.com/pro/', $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); } }