mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-10-12 02:01:37 +00:00
Layouts: Added logical theme event and guidance for block registration
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
namespace BookStack\App\Providers;
|
||||
|
||||
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Util\DateFormatter;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
use BookStack\View\ViewBlockPreferences;
|
||||
@@ -38,8 +40,10 @@ class ViewTweaksServiceProvider extends ServiceProvider
|
||||
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
|
||||
|
||||
// View Globals
|
||||
$viewBlockManager = $this->app->make(ViewBlockManager::class);
|
||||
View::share('dates', $this->app->make(DateFormatter::class));
|
||||
View::share('viewBlocks', $this->app->make(ViewBlockManager::class));
|
||||
View::share('viewBlocks', $viewBlockManager);
|
||||
Theme::dispatch(ThemeEvents::VIEW_BLOCKS_REGISTER, $viewBlockManager);
|
||||
|
||||
// Custom blade view directives
|
||||
Blade::directive('icon', function ($expression) {
|
||||
|
||||
@@ -180,6 +180,15 @@ class ThemeEvents
|
||||
*/
|
||||
const THEME_REGISTER_VIEWS = 'theme_register_views';
|
||||
|
||||
/**
|
||||
* View blocks register event.
|
||||
* Runs once a ViewBlockManager instance is available so that custom blocks can be registered
|
||||
* for use within user-configurable layouts in the system.
|
||||
*
|
||||
* @param \BookStack\View\ViewBlockManager $manager
|
||||
*/
|
||||
const VIEW_BLOCKS_REGISTER = 'view_blocks_register';
|
||||
|
||||
/**
|
||||
* Web before middleware action.
|
||||
* Runs before the request is handled but after all other middleware apart from those
|
||||
|
||||
@@ -25,17 +25,21 @@ class ViewBlockManager
|
||||
* Register a block type to be displayed at the given location and position.
|
||||
* @param class-string<ViewBlockInterface> $blockClass
|
||||
*/
|
||||
public function register(string $location, string $position, string $blockClass): void
|
||||
public function register(string $location, string $defaultPosition, string $blockClass): void
|
||||
{
|
||||
if (!isset($this->blocksByLocationAndPosition[$location])) {
|
||||
$this->blocksByLocationAndPosition[$location] = [];
|
||||
}
|
||||
|
||||
if (!isset($this->blocksByLocationAndPosition[$location][$position])) {
|
||||
$this->blocksByLocationAndPosition[$location][$position] = [];
|
||||
if (!isset($this->blocksByLocationAndPosition[$location][$defaultPosition])) {
|
||||
$this->blocksByLocationAndPosition[$location][$defaultPosition] = [];
|
||||
}
|
||||
|
||||
$this->blocksByLocationAndPosition[$location][$position][] = $blockClass;
|
||||
if (!is_a($blockClass, ViewBlockInterface::class, true)) {
|
||||
throw new \InvalidArgumentException('When registering a view block, the block class must implement ViewBlockInterface');
|
||||
}
|
||||
|
||||
$this->blocksByLocationAndPosition[$location][$defaultPosition][] = $blockClass;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -193,4 +193,105 @@ Theme::addSocialDriver('reddit', [
|
||||
$driver->with(['prompt' => 'select_account']);
|
||||
$driver->scopes(['open_id']);
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
## Custom View Layout Block Example
|
||||
|
||||
By listening to the `ThemeEvents::VIEW_BLOCKS_REGISTER` event, it's possible to register
|
||||
custom view blocks which can be shown in a layout in the application. View blocks are typically the sections shown in the sidebar,
|
||||
or the cards shown on the home view.
|
||||
|
||||
As an example of using this system, we'll define a custom view block which will show on the default home view to state how many
|
||||
books there are in the system.
|
||||
To start, we'll need to create a custom class to represent our view block. This must implement `\BookStack\View\ViewBlockInterface`:
|
||||
|
||||
```php
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
|
||||
class BookTotalBlock implements ViewBlockInterface {
|
||||
|
||||
public function __construct(
|
||||
protected BookQueries $bookQueries
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'custom_book_total_block';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return 'Total books displays';
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
return 'blocks.total-blocks';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$totalBooks = $this->bookQueries->visibleForList()->count();
|
||||
return [
|
||||
'totalBooks' => $totalBooks,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The interface has a few required methods:
|
||||
|
||||
- The `getId` method must provide a unique per-block-type string ID.
|
||||
- The `getLabel` method must provide a general string label for the block.
|
||||
- The `getView` method provides a string path to a view file (Can be one custom registered).
|
||||
- This is provided the available view data at time of render, so it can be dynamic based on context.
|
||||
- The `withData` method is called when block is being rendered, and it should return an array of data which will be merged with existing view data.
|
||||
- This is also provided available view data, for use as context.
|
||||
|
||||
In this example, we're also making use of the `BookQueries` internal BookStack class.
|
||||
You're able to inject any other dependent classes/services via the constructor like this, and BookStack will attempt to auto-resolve them.
|
||||
|
||||
We can then register this block class using the logical theme system like so:
|
||||
|
||||
```php
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
|
||||
Theme::listen(ThemeEvents::VIEW_BLOCKS_REGISTER, function (ViewBlockManager $manager) {
|
||||
$manager->register(
|
||||
'home-default', // The location/layout where this block will be displayed
|
||||
'right', // The default position for the block within the location
|
||||
BookTotalBlock::class // The block class to register
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
The above registration code would typically be within your `functions.php` theme file.
|
||||
You could also define the above block class in the same file or separate it into its own file
|
||||
and include it from the `functions.php` via a `require_once()` call.
|
||||
|
||||
Lastly, we'll need to create the view for the registered block.
|
||||
In our example we return `blocks.total-blocks` from the `getView` method, so our view needs to be located at
|
||||
`blocks/total-blocks.blade.php` from a view providing directory.
|
||||
In a theme folder, we can just create it at `blocks/total-blocks.blade.php` within the folder path.
|
||||
If we were building in a module, we'd need to create this at `views/blocks/total-blocks.blade.php` within our module folder.
|
||||
For our example, we'll use this content to make use of the data we're passing to the view:
|
||||
|
||||
```html
|
||||
<div class="card mb-xl">
|
||||
<h3 class="card-title">Total Books</h3>
|
||||
<div class="px-m pb-xs">
|
||||
<p>
|
||||
There are currently {{ $totalBooks }} books in the system!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
This will then show up on the default home grid view, in the right column.
|
||||
The exact position within that location may change depending on user preferences, but the block will be limited
|
||||
to the location provided during registration unless it has also been registered for other locations.
|
||||
|
||||
@@ -394,4 +394,47 @@ CONTENT;
|
||||
|
||||
$this->artisan('view:clear');
|
||||
}
|
||||
|
||||
public function test_view_blocks_register_event()
|
||||
{
|
||||
$this->usingThemeFolder(function (string $folder) {
|
||||
$functionsContent = <<<'CONTENT'
|
||||
<?php
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
class CustomViewBlock implements \BookStack\View\ViewBlockInterface {
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'custom_view';
|
||||
}
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return 'My custom view block';
|
||||
}
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
return 'custom-block';
|
||||
}
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Theme::listen(ThemeEvents::VIEW_BLOCKS_REGISTER, function (ViewBlockManager $manager) {
|
||||
$manager->register('home-default', 'right', CustomViewBlock::class);
|
||||
});
|
||||
CONTENT;
|
||||
|
||||
$viewDir = theme_path();
|
||||
file_put_contents($viewDir . '/functions.php', $functionsContent);
|
||||
file_put_contents($viewDir . '/custom-block.blade.php', '<h2>Hello there from my custom home block!</h2>');
|
||||
|
||||
$this->refreshApplication();
|
||||
$resp = $this->asEditor()->get('/');
|
||||
$html = $this->withHtml($resp);
|
||||
|
||||
$html->assertElementContains('.grid > div:nth-child(3) h2', 'Hello there from my custom home block!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user