PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
fluent-cart / app / CPT / FluentProducts.php

FluentProducts.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.1, at app/CPT/FluentProducts.php

480 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\CPT;
4
5
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\App;
8 use FluentCart\App\Services\URL;
9 use FluentCart\App\Vite;
10 use FluentCart\Framework\Support\Arr;
11
12 class FluentProducts
13 {
14
15 const CPT_NAME = 'fluent-products';
16
17 protected $showStandaloneMenu = false;
18
19 public function register()
20 {
21 add_filter('get_edit_post_link', function ($link, $postId) {
22 if ($this->showStandaloneMenu) {
23 return $link;
24 }
25
26 $post = get_post($postId);
27 if ($post && $post->post_type === 'fluent-products') {
28 return URL::getDashboardUrl('products/' . $postId);
29 }
30
31 return $link;
32 }, 99, 2);
33
34
35 add_action('init', function () {
36
37 $this->showStandaloneMenu = apply_filters('fluent_cart/show_standalone_product_menu', false);
38
39 $this->registerPostType();
40 $this->registerProductTaxonomies();
41 });
42
43 add_action('admin_enqueue_scripts', function () {
44 $screen = get_current_screen();
45 $isProductEditingScreen = $screen && $screen->post_type === 'fluent-products' && $screen->base === 'post';
46
47 if ($this->showStandaloneMenu) {
48 if ($isProductEditingScreen && App::request()->get('custom-editor') === 'true') {
49 wp_enqueue_style('wp-admin');
50 $this->enqueueCustomEditorStyles();
51 }
52 return;
53 }
54
55 if (!$isProductEditingScreen) {
56 if ($screen && $screen->post_type === 'fluent-products' && ($screen->base == 'edit-tags' || $screen->base == 'term')) {
57 $this->customizeTaxonomyScreen();
58 }
59 return;
60 }
61
62 // Make sure the default admin styles are enqueued
63 wp_enqueue_style('wp-admin');
64
65 $this->enqueueCustomEditorStyles();
66
67 wp_register_script('fluent-products-inline-js', '', [], FLUENTCART_VERSION, true);
68 wp_enqueue_script('fluent-products-inline-js');
69
70 $custom_js = "window.addEventListener('click', function (event) {
71 const anchor = event.target.closest('a');
72 if (anchor) {
73 event.preventDefault();
74 let href = anchor.getAttribute('href');
75 if (href && href != '#' && !href.startsWith('javascript:')) {
76 window.open(href, '_blank');
77 }
78 }
79 });";
80
81 // wp_add_inline_script('fluent-products-inline-js', $custom_js);
82
83 });
84
85 // add_action('elementor/editor/after_enqueue_scripts', function () {
86 // $this->registerElementorScript();
87 // });
88
89 add_action('enqueue_block_editor_assets', function () {
90 global $post;
91
92 // Only apply to book post type
93 if (!isset($post->post_type) || 'fluent-products' !== $post->post_type) {
94 return;
95 }
96
97
98 wp_add_inline_script(
99 'wp-blocks',
100 "
101 wp.domReady( function() {
102 // Force fullscreen mode
103 wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'fullscreenMode', true );
104
105 // Hide the fullscreen toggle button
106 const style = document.createElement( 'style' );
107 style.textContent = '.edit-post-fullscreen-mode-close, .components-button[aria-label=\"Exit fullscreen\"], .edit-post-fullscreen-mode-close ~ .edit-post-header-toolbar__left button[aria-label=\"Exit fullscreen\"] { display: none !important; }';
108 document.head.appendChild( style );
109 } );
110 "
111 );
112 });
113
114 $this->handleClassicEditorModalSave();
115
116 add_action('update_post_meta', [$this, 'handleThumbChange'], 10, 4);
117 add_action('added_post_meta', [$this, 'handleThumbChange'], 10, 4);
118 }
119
120 /**
121 * When Gutenberg is disabled (e.g. "Disable Gutenberg" plugin) the modal iframe
122 * shows the classic edit screen, whose Update button is a full form POST to
123 * post.php followed by a redirect. Carry `custom-editor` through both so the
124 * iframe stays chrome-less, then tell the parent modal the save finished.
125 */
126 private function handleClassicEditorModalSave()
127 {
128 $isCustomEditor = function () {
129 return App::request()->get('custom-editor') === 'true'
130 || (isset($_POST['custom-editor']) && $_POST['custom-editor'] === 'true'); // phpcs:ignore WordPress.Security.NonceVerification
131 };
132
133 add_action('edit_form_top', function ($post) use ($isCustomEditor) {
134 if ($post->post_type === self::CPT_NAME && $isCustomEditor()) {
135 echo '<input type="hidden" name="custom-editor" value="true" />';
136 }
137 });
138
139 add_filter('redirect_post_location', function ($location, $postId) use ($isCustomEditor) {
140 if (get_post_type($postId) === self::CPT_NAME && $isCustomEditor()) {
141 // WP builds $location from get_edit_post_link(), which this class
142 // filters to the SPA dashboard URL; send the iframe back to the
143 // real edit screen instead.
144 wp_parse_str((string)wp_parse_url($location, PHP_URL_QUERY), $query);
145
146 return add_query_arg([
147 'post' => $postId,
148 'action' => 'edit',
149 'message' => (int)($query['message'] ?? 1) ?: 1,
150 'custom-editor' => 'true',
151 ], admin_url('post.php'));
152 }
153 return $location;
154 }, 10, 2);
155
156 add_action('admin_footer', function () {
157 $screen = get_current_screen();
158 if (!$screen || $screen->post_type !== self::CPT_NAME || $screen->base !== 'post') {
159 return;
160 }
161
162 $request = App::request();
163 if ($request->get('custom-editor') !== 'true' || !$request->get('message') || $request->get('is-preview-mode')) {
164 return;
165 }
166
167 // Notify once, then drop `message` from the URL so a later reload of
168 // this iframe doesn't re-send the save signal and close the modal.
169 echo '<script>if (window.parent !== window) {'
170 . 'window.parent.postMessage({type: "classicEditorSaved"}, "*");'
171 . 'var u = new URL(window.location.href); u.searchParams.delete("message");'
172 . 'window.history.replaceState(null, "", u.toString());'
173 . '}</script>';
174 });
175 }
176
177 public function registerElementorScript()
178 {
179 $custom_js = "console.log('doc ready - editor context');
180
181 // Wait for Elementor editor to be ready
182 jQuery(window).on('elementor:init', () => {
183 console.log('Elementor editor initialized');
184
185 jQuery(document).on('click', '#elementor-editor-wrapper-v2 .MuiButtonGroup-root > button', function() {
186 console.log('Button clicked!', this);
187
188 let content = '';
189
190 try {
191 const previewFrame = jQuery('#elementor-preview-iframe')[0];
192 if (previewFrame && previewFrame.contentDocument) {
193 const previewContent = previewFrame.contentDocument.querySelector('.elementor');
194 if (previewContent) {
195 content = previewContent.outerHTML;
196 console.log('Content from preview frame:', content.substring(0, 200) + '...');
197 }
198 }
199 } catch (e) {
200 console.log('Could not access preview frame:', e);
201 }
202
203 // Fallback: Get structured data
204 if (!content && typeof elementor !== 'undefined') {
205 try {
206 content = JSON.stringify(elementor.documents.currentDocument.container.children.toJSON());
207 } catch (e) {
208 console.log('Could not get elementor data:', e);
209 }
210 }
211
212 // Send message to parent window
213 window.parent.postMessage(
214 {
215 type: 'gutenbergContentChanged',
216 content: content,
217 timestamp: Date.now()
218 },
219 '*'
220 );
221 });
222 });";
223
224 wp_add_inline_script('elementor-editor', $custom_js);
225 }
226
227 public function registerPostType()
228 {
229 $productSlug = (new StoreSettings())->get('product_slug') ?? 'item';
230 $urlSlug = apply_filters('fluent_cart/front_url_slug', $productSlug, []);
231 $urlWithFront = apply_filters('fluent_cart/product_url_with_front', true, [
232 'slug' => $urlSlug
233 ]);
234
235 $singularName = __('Product', 'fluent-cart');
236
237 if (defined('WC_PLUGIN_FILE')) {
238 $singularName = __('Product (FluentCart)', 'fluent-cart');
239 }
240
241 register_post_type(self::CPT_NAME, [
242 'capability_type' => 'post',
243 // 'capabilities' => [
244 // 'edit_post' => 'edit_your_post_type',
245 // 'read_post' => 'read_your_post_type',
246 // 'delete_post' => 'do_not_allow', // block trash/delete
247 // 'edit_posts' => 'edit_your_post_types',
248 // 'edit_others_posts' => 'edit_others_your_post_types',
249 // 'publish_posts' => 'publish_your_post_types',
250 // 'read_private_posts' => 'read_private_your_post_types',
251 // ],
252 // 'map_meta_cap' => true,
253 'label' => __('Products', 'fluent-cart'),
254 'labels' => [
255 'name' => __('Products', 'fluent-cart'),
256 'singular_name' => $singularName,
257 'add_new' => _x('Add New Product', 'product', 'fluent-cart'),
258 'add_new_item' => __('Add New Product', 'fluent-cart'),
259 'edit_item' => __('Edit Product', 'fluent-cart'),
260 'view_item' => __('View Product', 'fluent-cart'),
261 'search_items' => __('Search products', 'fluent-cart'),
262 ],
263 //'_edit_link' => 'admin.php?page=fluent-cart#/products/%d/pricing',
264 'description' => __('FluentCart products post type', 'fluent-cart'),
265 'public' => true,
266 'hierarchical' => false,
267 'exclude_from_search' => false,
268 'publicly_queryable' => true,
269 'show_ui' => true,
270 'show_in_menu' => $this->showStandaloneMenu,
271 'show_in_nav_menus' => true,
272 'show_in_admin_bar' => true,
273 'menu_position' => 24,
274 'has_archive' => true,
275 'show_in_rest' => true,
276 // 'rest_base' => $urlSlug, // Optional: customize REST API base
277 'rest_controller_class' => 'WP_REST_Posts_Controller', // Optional: use default controller
278 'supports' => [
279 'title',
280 'editor',
281 'excerpt',
282 'thumbnail',
283 'author',
284 'revisions',
285 'custom-fields',
286 ],
287 'rewrite' => [
288 'slug' => $urlSlug,
289 'with_front' => $urlWithFront,
290 'feeds' => true,
291 'pages' => true,
292 ],
293 'query_var' => $urlSlug,
294 'can_export' => true,
295 'delete_with_user' => false
296 ]);
297
298 $this->disableCreateAndEditForFluentProducts();
299 }
300
301 public function disableCreateAndEditForFluentProducts()
302 {
303 add_action('admin_init', function () {
304 // return for ajax and rest api
305 if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
306 return;
307 }
308
309 global $pagenow;
310
311 if ($pagenow != 'post-new.php') {
312 return;
313 }
314
315 // disable direct create product
316 $postType = $_GET['post_type'] ?? '';
317 if ($postType == self::CPT_NAME) {
318 wp_redirect(admin_url('admin.php?page=fluent-cart#/products/?add-new=true'));
319 exit;
320 }
321 });
322 }
323
324 public function registerProductTaxonomies()
325 {
326 register_taxonomy('product-categories', self::CPT_NAME, [
327 'hierarchical' => true,
328 'show_ui' => true,
329 'show_admin_column' => true,
330 'show_in_rest' => true,
331 'query_var' => true,
332 'rewrite' => ['slug' => 'product-categories'],
333 'labels' => [
334 'name' => __('Categories', 'fluent-cart'),
335 'singular_name' => __('Category', 'fluent-cart'),
336 'search_items' => __('Search Categories', 'fluent-cart'),
337 'all_items' => __('All Categories', 'fluent-cart'),
338 'parent_item' => __('Parent Category', 'fluent-cart'),
339 'parent_item_colon' => __('Parent Category:', 'fluent-cart'),
340 'edit_item' => __('Edit Category', 'fluent-cart'),
341 'update_item' => __('Update Category', 'fluent-cart'),
342 'add_new_item' => __('Add New Category', 'fluent-cart'),
343 'new_item_name' => __('New Category Name', 'fluent-cart'),
344 'menu_name' => __('Product Category', 'fluent-cart'),
345 'not_found' => __('No categories found.', 'fluent-cart'),
346 ],
347 ]);
348
349 register_taxonomy('product-brands', self::CPT_NAME, [
350 'hierarchical' => true,
351 'show_ui' => true,
352 'show_in_rest' => true,
353 'show_admin_column' => false,
354 'query_var' => true,
355 'rewrite' => ['slug' => 'product-brands'],
356 'labels' => [
357 'name' => __('Brands', 'fluent-cart'),
358 'singular_name' => __('Brand', 'fluent-cart'),
359 'search_items' => __('All Brand', 'fluent-cart'),
360 'all_items' => __('All Brands', 'fluent-cart'),
361 'parent_item' => __('Parent Brand', 'fluent-cart'),
362 'edit_item' => __('Edit Brand', 'fluent-cart'),
363 'update_item' => __('Update Brand', 'fluent-cart'),
364 'add_new_item' => __('Add New Brand', 'fluent-cart'),
365 'new_item_name' => __('New Brand Name', 'fluent-cart'),
366 'menu_name' => __('Product Brand', 'fluent-cart'),
367 'view_item' => __('View Brand', 'fluent-cart'),
368 'not_found' => __('No Brand found', 'fluent-cart'),
369 ],
370 ]);
371
372 // product-tags is deliberately NOT registered: FluentCart ships
373 // categories and brands only (product decision 2026-08-06). Stores that
374 // register it themselves can still filter the shop shortcode with
375 // taxonomies="product-tags:…".
376 }
377
378 private function enqueueCustomEditorStyles()
379 {
380 $custom_css =
381 '#editor .editor-sidebar__panel .editor-post-summary .editor-post-trash,' .
382 '#wpadminbar,' .
383 '#adminmenu,' .
384 '#adminmenuback,' .
385 '#adminmenuwrap {' .
386 'display: none !important;' .
387 '}' .
388 '#wpbody-content .interface-interface-skeleton {' .
389 'left: 0 !important;' .
390 'top: 0 !important;' .
391 '}' .
392 '#wpcontent, #wpfooter {' .
393 'margin-left: 0 !important;' .
394 'margin-right: 0 !important;' .
395 '}';
396
397 wp_add_inline_style('wp-admin', $custom_css);
398 }
399
400 public function customizeTaxonomyScreen()
401 {
402
403 Vite::enqueueScript('fluent_cart_admin_global_js',
404 'admin/global.js',
405 );
406
407 Vite::enqueueStyle('fluent_cart_admin_app_css',
408 'styles/tailwind/style.css',
409 );
410
411 Vite::enqueueStyle('fluent_cart_taxonomy_css',
412 'styles/tailwind/taxonomy.scss',
413 );
414
415 add_action('in_admin_header', function () {
416 ?>
417 <div style="margin-left: -20px;" class="fc_taxonomy_menu" id="fct_admin_menu_holder">
418 <?php do_action('fluent_cart/admin_menu'); ?>
419 </div>
420 <?php
421 });
422 }
423
424 public function handleThumbChange($meta_id, $object_id, $meta_key, $_meta_value)
425 {
426 if ($meta_key !== '_thumbnail_id') {
427 return;
428 }
429
430
431 $post = get_post($object_id);
432
433
434 if ($post->post_type !== FluentProducts::CPT_NAME) {
435 return;
436 }
437
438
439 //get post meta
440 $oldGallery = get_post_meta($object_id, FluentProducts::CPT_NAME . '-gallery-image', true);
441
442 if (!is_array($oldGallery)) {
443 $oldGallery = [];
444 }
445
446 $currentThumbnailId = $_meta_value;
447
448
449 //get thumbnail image with all info like title id and url
450 $currentThumbnail = wp_prepare_attachment_for_js($currentThumbnailId);
451 $currentThumbnail = [
452 'id' => $currentThumbnailId,
453 'url' => Arr::get($currentThumbnail, 'url'),
454 'name' => Arr::get($currentThumbnail, 'title')
455 ];
456
457 $currentThumbnailId = Arr::get($currentThumbnail, 'id');
458
459
460 $found = false;
461 $index = 0;
462 foreach ($oldGallery as $index => $image) {
463 if ($image['id'] == $currentThumbnailId) {
464 $found = true;
465 break;
466 }
467 }
468
469
470 //if found bring it on top
471 if ($found) {
472 unset($oldGallery[$index]);
473 array_unshift($oldGallery, $image);
474 } else {
475 array_unshift($oldGallery, $currentThumbnail);
476 }
477 update_post_meta($object_id, FluentProducts::CPT_NAME . '-gallery-image', $oldGallery);
478 }
479 }
480