diff --git a/docs/frontend/components.md b/docs/frontend/components.md index 13d8771794..01037d339d 100644 --- a/docs/frontend/components.md +++ b/docs/frontend/components.md @@ -47,13 +47,13 @@ if (file_exists($script_assets)) { For `Dokan Free`, we can import the components via `@/components`: ```js -import { DataViews, useWindowDimensions } from '@/components'; +import { DataViews } from '@/components'; ``` In `Dokan Pro`, components can be imported directly from `@dokan/components`: ```js -import { DataViews, useWindowDimensions } from '@dokan/components'; +import { DataViews } from '@dokan/components'; ``` For external `plugins`, we must include the `dokan-react-components` as scripts dependency and the `@dokan/components` should be introduced as an external resource configuration to resolve the path via `webpack`: diff --git a/docs/frontend/hooks.md b/docs/frontend/hooks.md new file mode 100644 index 0000000000..ef3220cd7c --- /dev/null +++ b/docs/frontend/hooks.md @@ -0,0 +1,90 @@ +# Dokan Hooks + +## Overview + +`Dokan` provides a set of reusable `hooks` that can be used across both `Free` and `Pro` versions. This documentation explains how to properly set up and use `hooks` in your project. + +## Important Dependencies + +For both `Dokan Free and Pro` versions, we must register the `dokan-react-components` dependency when using `global` components. + +### Implementation Example + +```php +// Register scripts with dokan-react-components dependency +$script_assets = 'add_your_script_assets_path_here'; + +if (file_exists($script_assets)) { + $vendor_asset = require $script_assets; + $version = $vendor_asset['version'] ?? ''; + + // Add dokan-react-components as a dependency + $component_handle = 'dokan-react-components'; + $dependencies = $vendor_asset['dependencies'] ?? []; + $dependencies[] = $component_handle; + + // Register Script + wp_register_script( + 'handler-name', + 'path_to_your_script_file', + $dependencies, + $version, + true + ); + + // Register Style + wp_register_style( + 'handler-name', + 'path_to_your_style_file', + [ $component_handle ], + $version + ); +} +``` + +## Component Access + +For `Dokan Free`, we can import the components via `@/hooks`: + +```js +import { useWindowDimensions } from '@/hooks'; +``` + +In `Dokan Pro`, components can be imported directly from `@dokan/components`: + +```js +import { useWindowDimensions } from '@dokan/hooks'; +``` + +For external `plugins`, we must include the `dokan-react-components` as scripts dependency and the `@dokan/hooks` should be introduced as an external resource configuration to resolve the path via `webpack`: + +```js +externals: { + '@dokan/hooks': 'dokan.hooks', + ... +}, +``` + +## Adding Global Components + +### File Structure + +``` +|____ src/ +| |___ hooks/ +| | |___ index.tsx # Main export file +| | |___ ViewportDimensions.tsx # Existing hook +| | |___ YourHook # Your new hook +| | | +| | |___ Other Files +| | +| |___ Other Files +| +|____ Other Files +``` + +**Finally,** we need to export the new `hook` from the `src/hooks/index.tsx` file. Then, we can import the new component from `@dokan/hooks` in `dokan pro` version. + +```tsx +export { default as useWindowDimensions } from '@/hooks/ViewportDimensions'; +``` diff --git a/includes/Assets.php b/includes/Assets.php index 86ee765d08..cd2fe96b62 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -587,17 +587,25 @@ public function get_scripts() { 'src' => $asset_url . '/js/utilities.js', 'version' => filemtime( $asset_path . 'js/utilities.js' ), ], + 'dokan-hooks' => [ + 'deps' => [], + 'src' => $asset_url . '/js/hooks.js', + 'version' => filemtime( $asset_path . 'js/hooks.js' ), + ], ]; - $components_asset_dir = DOKAN_DIR . '/assets/js/components.asset.php'; - if ( file_exists( $components_asset_dir ) ) { - $components_asset = require $components_asset_dir; - $components_asset['dependencies'][] = 'dokan-utilities'; + $components_asset_file = DOKAN_DIR . '/assets/js/components.asset.php'; + if ( file_exists( $components_asset_file ) ) { + $components_asset = require $components_asset_file; + // Register React components. $scripts['dokan-react-components'] = [ - 'src' => $asset_url . '/js/components.js', - 'deps' => $components_asset['dependencies'], 'version' => $components_asset['version'], + 'src' => $asset_url . '/js/components.js', + 'deps' => array_merge( + $components_asset['dependencies'], + [ 'dokan-utilities', 'dokan-hooks' ] + ), ]; } diff --git a/includes/functions-dashboard-navigation.php b/includes/functions-dashboard-navigation.php index 97301b5af5..ceb0f5d29a 100644 --- a/includes/functions-dashboard-navigation.php +++ b/includes/functions-dashboard-navigation.php @@ -259,10 +259,12 @@ function dokan_dashboard_nav( $active_menu = '' ) { } $submenu .= sprintf( - '', + /* translators: 1) submenu class, 2) submenu route, 3) submenu icon, 4) submenu title */ + '', $submenu_class, - isset( $sub['url'] ) ? $sub['url'] : dokan_get_navigation_url( "{$key}/{$sub_key}" ), - isset( $sub['icon'] ) ? $sub['icon'] : '', + $sub['react_route'] ?? '', + $sub['url'] ?? dokan_get_navigation_url( "{$key}/{$sub_key}" ), + $sub['icon'] ?? '', apply_filters( 'dokan_vendor_dashboard_menu_title', $submenu_title, $sub ) ); @@ -278,11 +280,13 @@ function dokan_dashboard_nav( $active_menu = '' ) { } $menu .= sprintf( - '
  • %s %s%s
  • ', + /* translators: 1) menu class, 2) menu route, 3) menu url, 4) menu target, 5) menu icon, 6) menu title, 7) submenu */ + '
  • %5$s %6$s%7$s
  • ', $class, - isset( $item['url'] ) ? $item['url'] : dokan_get_navigation_url( $menu_slug ), - isset( $item['target'] ) ? $item['target'] : '_self', - isset( $item['icon'] ) ? $item['icon'] : '', + $item['react_route'] ?? '', + $item['url'] ?? dokan_get_navigation_url( $menu_slug ), + $item['target'] ?? '_self', + $item['icon'] ?? '', apply_filters( 'dokan_vendor_dashboard_menu_title', $title, $item ), $submenu ); diff --git a/src/components/dataviews/DataViewTable.tsx b/src/components/dataviews/DataViewTable.tsx index 672c4d3da9..bca4c0cd06 100644 --- a/src/components/dataviews/DataViewTable.tsx +++ b/src/components/dataviews/DataViewTable.tsx @@ -3,9 +3,8 @@ import { Slot } from "@wordpress/components"; import { ViewportDimensions } from '@/Hooks/ViewportDimensions'; import type { Action, Field, SupportedLayouts, View } from "@wordpress/dataviews/src/types"; import { kebabCase, snakeCase } from "@/utilities"; -import { useWindowDimensions } from "@/components"; import { useEffect } from "@wordpress/element"; -import type { ReactNode } from "react"; +import { useWindowDimensions } from "@/hooks"; import './style.scss'; type ItemWithId = { id: string }; @@ -31,7 +30,7 @@ type DataViewsProps< Item > = { onChangeSelection?: ( items: string[] ) => void; onClickItem?: ( item: Item ) => void; isItemClickable?: ( item: Item ) => boolean; - header?: ReactNode; + header?: JSX.Element; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); diff --git a/src/components/index.tsx b/src/components/index.tsx index 0c86eab8a0..73eb471cf3 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,5 +1,4 @@ export { default as DataViews } from './dataviews/DataViewTable'; -export { default as useWindowDimensions } from '@/hooks/ViewportDimensions'; export { DataForm, diff --git a/src/hooks/ViewportDimensions.tsx b/src/hooks/ViewportDimensions.tsx new file mode 100644 index 0000000000..ccfad55898 --- /dev/null +++ b/src/hooks/ViewportDimensions.tsx @@ -0,0 +1,46 @@ +import { useState, useEffect, useCallback } from '@wordpress/element'; + +interface ViewportDimensions { + width: number | null; + height: number | null; +} + +/** + * Hook to track viewport dimensions. + * + * @since DOKAN_PRO_SINCE + * + * @return {ViewportDimensions} The viewport dimensions. + */ +export default function useWindowDimensions() { + const getViewportDimensions = useCallback((): ViewportDimensions => ({ + width: typeof window !== 'undefined' ? window.innerWidth : null, + height: typeof window !== 'undefined' ? window.innerHeight : null, + }), []); + + const [viewport, setViewport] = useState(getViewportDimensions()); + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + const handleResize = () => { + // Use requestAnimationFrame to throttle updates + window.requestAnimationFrame(() => { + setViewport(getViewportDimensions()); + }); + }; + + window.addEventListener('resize', handleResize); + + // Initial measurement after mount + handleResize(); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [getViewportDimensions]); + + return viewport; +}; diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx new file mode 100644 index 0000000000..1f34f92ccb --- /dev/null +++ b/src/hooks/index.tsx @@ -0,0 +1 @@ +export { default as useWindowDimensions } from '@/hooks/ViewportDimensions'; diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 0de5d0ed26..21b4d068a4 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -7,6 +7,7 @@ import { } from '@wordpress/components'; import { PluginArea } from '@wordpress/plugins'; import { DokanToaster } from "@getdokan/dokan-ui"; +import { useLocation } from 'react-router-dom'; // Create a ThemeContext const ThemeContext = createContext( null ); @@ -43,6 +44,34 @@ interface LayoutProps { footerComponent?: JSX.Element|React.ReactNode; } +const handleMenuActiveStates = ( currentPath ) => { + const menuRoute = currentPath.replace( /^\//, '' ); // Remove leading slash. + const menuItem = document.querySelector( `.dokan-dashboard-menu li[data-react-route='${ menuRoute }']` ) || null; + + // Return if menu item not found. + if ( ! menuItem ) { + return; + } + + document.querySelectorAll( '.dokan-dashboard-menu li' ).forEach( item => { + item.classList.remove( 'active' ); + item.querySelectorAll( '.navigation-submenu li' ).forEach( subItem => { + subItem.classList.remove( 'current' ); + }); + }); + + // Get parent menu item if this is a submenu item. + const parentMenuItem = menuItem.closest( '.dokan-dashboard-menu > li' ); + if ( parentMenuItem ) { // Add `active` to parent menu. + parentMenuItem.classList.add( 'active' ); + } + + const subMenuItem = document.querySelector( `.navigation-submenu li[data-react-route='${ menuRoute }']` ); + if ( subMenuItem ) { // Add `current` to submenu item. + subMenuItem.classList.add( 'current' ); + } +}; + // Create a Layout component that uses the ThemeProvider const Layout = ( { children, @@ -51,6 +80,9 @@ const Layout = ( { headerComponent, footerComponent, }: LayoutProps ) => { + const location = useLocation(); // Use the location hook to get the current path. + handleMenuActiveStates( location?.pathname ); + return ( diff --git a/webpack.config.js b/webpack.config.js index c13fefa66a..ed05acfb39 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,6 +15,9 @@ const updatedConfig = { 'utilities': { import: '@/utilities/index.ts', }, + 'hooks': { + import: '@/hooks/index.tsx', + }, }, output: { path: path.resolve(__dirname, './assets/js'),