From 8f1c68db4269bc308935a4811391abd3154dfbd9 Mon Sep 17 00:00:00 2001 From: GuySartorelli <36352093+GuySartorelli@users.noreply.github.com> Date: Fri, 4 Feb 2022 12:50:35 +1300 Subject: [PATCH] DOC Document how to enable the preview panel for custom DataObjects. (#10124) --- .../04_Preview.md | 412 ++++++++++++++++-- 1 file changed, 384 insertions(+), 28 deletions(-) diff --git a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md index da7b1aacd..6b2da22cf 100644 --- a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md +++ b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md @@ -7,16 +7,14 @@ summary: How content previews work in the CMS ## Overview -__Deprecated:__ -The following documentation regarding JavaScript layouts and Entwine applies to legacy code only. -If you're developing new functionality in React powered sections please refer to -[ReactJS in Silverstripe CMS](./How_Tos/Extend_CMS_Interface.md#reactjs-in-silverstripe). - With the addition of side-by-side editing, the preview has the ability to appear -within the CMS window when editing content in the _Pages_ section of the CMS. -The site is rendered into an iframe. It will update itself whenever the content -is saved, and relevant pages will be loaded for editing when the user navigates -around in the preview. +within the CMS window when editing content in the CMS. This is enabled by default +in the _Pages_ section for `SiteTree` models, but as outlined below can be enabled +in other sections and for other models as well. + +Within the preview panel, the site is rendered into an iframe. It will update +itself whenever the content is saved, and relevant pages will be loaded for editing +when the user navigates around in the preview. The root element for preview is `.cms-preview` which maintains the internal states necessary for rendering within the entwine properties. It provides @@ -24,22 +22,380 @@ function calls for transitioning between these states and has the ability to update the appearance of the option selectors. In terms of backend support, it relies on `SilverStripeNavigator` to be rendered -into the `.cms-edit-form`. _LeftAndMain_ will automatically take care of -generating it as long as the `*_SilverStripeNavigator` template is found - -first segment has to match current _LeftAndMain_-derived class (e.g. -`LeftAndMain_SilverStripeNavigator`). +into the form. _LeftAndMain_ will automatically take care of generating it as long +as the `*_SilverStripeNavigator` template is found - first segment has to match the +current _LeftAndMain_-derived class (e.g. `LeftAndMain_SilverStripeNavigator`). + +## PHP +For a DataObject to be previewed using the preview panel there are a few prerequisites: + +- The class must implement the `CMSPreviewable` interface +- At least one preview state must be enabled for the class +- There must be some valid URL to use inside the preview panel + +### CMSPreviewable +The `CMSPreviewable` interface has three methods: `PreviewLink`, `CMSEditLink`, and +`getMimeType`. + +#### PreviewLink +The `PreviewLink` method is what determines the URL used inside the preview panel. If +your `DataObject` is intended to always belong to a page, you might want to preview the +item in the context of where it sits on the page using an anchor. You can also provide +some route specific for previewing this object, for example an action on the ModelAdmin +that is used to manage the object. + +#### CMSEditLink +This method exists so that when a user clicks on a link in the preview panel, the CMS +edit form for the page the link leads to can be loaded. Unless your `DataObject` is +[acting like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1) +this will likely not apply, but as this method is mandatory and public we may as well +set it up correctly. + +If your object belongs to [a custom ModelAdmin](./01_ModelAdmin.md), the edit URL for the +object is predictable enough to construct and return from this method as you'll see below. +The format for that situation is always the same, with increasing complexity if you're +nesting `GridField`s. For the below examples it is assumed you aren't using nested +`GridField`s. + +If your object belongs to a page, you can safely get away with returning `null` or an empty +string, as it won't be used. You can choose to return a valid edit link, but because of the +complexity of the way these links are generated it would be difficult to do so in a general, +reusable way. + +#### getMimeType +In ~90% of cases will be 'text/html', but note it is also possible to display (for example) +an inline PDF document in the preview panel. + +### Preview states +The preview state(s) you apply to your `DataObject` will depend primarily on whether it uses +the [Versioned](api:SilverStripe\Versioned\Versioned) extension or not. + +#### Versioned DataObjects +If your class does use the `Versioned` extension, there are two different states available +to you. It is generally recommended that you enable both, so that content authors can toggle +between viewing the draft and the published content. + +To enable the draft preview state, use the `$show_stage_link` configuration variable. + +```php +private static $show_stage_link = true; +``` + +To enable the published preview state, use the `$show_live_link` configuration variable. + +```php +private static $show_live_link = true; +``` + +#### Unversioned DataObjects +If you are not using the `Versioned` extension for your class, there is only one preview +state you can use. This state will always be active once you enable it. + +To enable the unversioned preview state, use the `$show_unversioned_preview_link` +configuration variable. + +```php +private static $show_unversioned_preview_link = true; +``` + +### Enabling preview for DataObjects in a ModelAdmin +For this example we will take the `Product` and `MyAdmin` classes from the +[ModelAdmin documentation](./01_ModelAdmin.md). + +#### The DataObject implementation +As mentioned above, your `Product` class must implement the `CMSPreviewable` interface. +It also needs at least one preview state enabled. This example assumes we aren't using +the `Versioned` extension. + +```php +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class Product extends DataObject implements CMSPreviewable +{ + private static $show_unversioned_preview_link = true; + + // ... + + public function PreviewLink($action = null) + { + return null; + } + + public function CMSEditLink() + { + return null; + } + + public function getMimeType() + { + return 'text/html'; + } +} +``` + +We will need to add a new action to the `ModelAdmin` to provide the actual preview itself. +For now, assume that action will be called `cmsPreview`. We can very easily craft a valid +URL using the `Link` method on the `MyAdmin` class. + +Note that if you had set up this model to [act like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1), +you could simply `return $this->Link($action)`. In that case the new action would not need +to be added to your `ModelAdmin`. + +```php +public function PreviewLink($action = null) +{ + $admin = MyAdmin::singleton(); + return Controller::join_links( + $admin->Link(str_replace('\\', '-', $this->ClassName)), + 'cmsPreview', + $this->ID + ); +} +``` + +The `CMSEditLink` is also very easy to build, because the edit link used by `ModelAdmin`s +is predictable. +```php +public function CMSEditLink() +{ + $admin = MyAdmin::singleton(); + $sanitisedClassname = str_replace('\\', '-', $this->ClassName); + return Controller::join_links( + $admin->Link($sanitisedClassname), + 'EditForm/field/', + $sanitisedClassname, + 'item', + $this->ID + ); +} +``` + +Let's assume when you display this object on the front end you're just looping through a +list of items and indirectly calling `forTemplate` using the [`$Me` template variable](../01_Templates/01_Syntax.md#me). +This method will be used by the `cmsPreview` action in the `MyAdmin` class to tell the +CMS what to display in the preview panel. + +The `forTemplate` method will probably look something like this: + +```php +public function forTemplate() +{ + // If the template for this DataObject is not an "Include" template, use the appropriate type here e.g. "Layout". + return $this->renderWith(['type' => 'Includes', self::class]); +} +``` + +#### The ModelAdmin implementation +We need to add the `cmsPreview` action to the `MyAdmin` class, which will output the +content which should be displayed in the preview panel. + +Because this is a public method called on a `ModelAdmin`, which will often be executed +in a back-end context using admin themes, it pays to ensure we're loading the front-end +themes whilst rendering out the preview content. + +```php +use SilverStripe\Admin\ModelAdmin; +use SilverStripe\View\SSViewer; + +class MyAdmin extends ModelAdmin +{ + private static $managed_models = [ + Product::class, + ]; + + private static $url_segment = 'products'; + + private static $menu_title = 'Products'; + + private static $allowed_actions = [ + 'cmsPreview', + ]; + + private static $url_handlers = [ + '$ModelClass/cmsPreview/$ID' => 'cmsPreview', + ]; + + public function cmsPreview() + { + $id = $this->urlParams['ID']; + $obj = $this->modelClass::get_by_id($id); + if (!$obj || !$obj->exists()) { + return $this->httpError(404); + } + + // Include use of a front-end theme temporarily. + $oldThemes = SSViewer::get_themes(); + SSViewer::set_themes(SSViewer::config()->get('themes')); + $preview = $obj->forTemplate(); + + // Make sure to set back to backend themes. + SSViewer::set_themes($oldThemes); + + return $preview; + } +} +``` + +### Enabling preview for DataObjects which belong to a page +If the `DataObject` you want to preview belongs to a specific page, for example +through a `has_one` or `has_many` relation, you will most likely want to preview +it in the context of the page it belongs to. + +#### The Page implementation +For this example we will assume the `Product` class is `Versioned`. + +As discussed above, the `CMSEditLink` method is used to load the correct edit form +in the CMS when you click on a link within the preview panel. This uses the +`x-page-id` and `x-cms-edit-link` meta tags in the head of the page (assuming your +page template calls `$MetaTags` in the `` element). When a page loads, +these meta tags are checked and the appropriate form is loaded. + +When rendering a full page in the preview panel to preview a `DataObject` on that +page, the meta tags for that page are present. When a content author toggles between +the draft and published preview states, those meta tags are checked and the page's +edit form would be loaded instead of the `DataObject`'s form. To avoid this +unexpected behaviour, you can include an extra GET parameter in the value returned +by `PreviewLink`. Then in the `MetaTags` method, when the extra parameter is +detected, omit the relevant meta tags. + +Note that this is not necessary for unversioned `DataObjects` as they only have +one preview state. + +```php +use SilverStripe\Control\Controller; +use SilverStripe\View\Parsers\HTML4Value; + +class ProductPage extends Page +{ + //... + + private static $has_many = [ + 'Products' => Product::class, + ]; + + public function MetaTags($includeTitle = true) + { + $tags = parent::MetaTags($includeTitle); + if (!Controller::has_curr()) { + return; + } + // If the 'DataObjectPreview' GET parameter is present, remove 'x-page-id' and 'x-cms-edit-link' meta tags. + // This ensures that toggling between draft/published states doesn't revert the CMS to the page's edit form. + $controller = Controller::curr(); + $request = $controller->getRequest(); + if ($request->getVar('DataObjectPreview') !== null) { + $html = HTML4Value::create($tags); + $xpath = "//meta[@name='x-page-id' or @name='x-cms-edit-link']"; + $removeTags = $html->query($xpath); + $body = $html->getBody(); + foreach ($removeTags as $tag) { + $body->removeChild($tag); + } + $tags = $html->getContent(); + } + return $tags; + } +} +``` + +#### The DataObject Implementation +Make sure the Versioned `Product` class implements `CMSPreviewable` and enables +the draft and published preview states. + +```php +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; +use SilverStripe\Versioned\Versioned; + +class Product extends DataObject implements CMSPreviewable +{ + private static $show_stage_link = true; + private static $show_live_link = true; + + private static $extensions = [ + Versioned::class, + ]; + + private static $has_one = [ + 'ProductPage' => ProductPage::class, + ]; + + // ... + + public function PreviewLink($action = null) + { + return null; + } + + public function CMSEditLink() + { + return null; + } + + public function getMimeType() + { + return 'text/html'; + } + +} +``` + +Implement a method which gives you a unique repeatable anchor for each +distinct `Product` object. + +```php +/** + * Used to generate the id for the product element in the template. + */ +public function getAnchor() +{ + return 'product-' . $this->getUniqueKey(); +} +``` + +For the `PreviewLink`, append the `DataObjectPreview` GET parameter to the +page's frontend URL. +```php +public function PreviewLink($action = null) +{ + // Let the page know it's being previewed from a DataObject edit form (see Page::MetaTags()) + $action = $action . '?DataObjectPreview=' . mt_rand(); + // Scroll the preview straight to where the object sits on the page. + if ($page = $this->ProductPage()) { + $link = $page->Link($action) . '#' . $this->getAnchor(); + return $link; + } + return null; +} +``` + +The CMSEditLink doesn't matter so much for this implementation. It is required +by the `CMSPreviewable` interface so some implementation must be provided, but +you can safely return `null` or an empty string with no repercussions in this +situation. + +#### The Page template +In your page template, make sure the anchor is used where you render the objects. +This allows the preview panel to be scrolled automatically to where the object +being edited sits on the page. + +```ss +<%-- ... --%> +<% loop $Products %> +
+ <%-- ... --%> +
+<% end_loop %> +``` + + +## Javascript + +### Configuration and Defaults We use `ss.preview` entwine namespace for all preview-related entwines. -[notice] -Caveat: `SilverStripeNavigator` and `CMSPreviewable` interface currently only -support SiteTree objects that are _Versioned_. They are not general enough for -using on any other DataObject. That pretty much limits the extendability of the -feature. -[/notice] - -## Configuration and Defaults - Like most of the CMS, the preview UI is powered by [jQuery entwine](https://github.com/hafriedlander/jquery.entwine). This means its defaults are configured through JavaScript, by setting entwine properties. @@ -96,7 +452,7 @@ in the `silverstripe/admin` module. To understand how layouts are handled in the CMS UI, have a look at the [CMS Architecture](cms_architecture) guide. -## Enabling preview +### Enabling preview The frontend decides on the preview being enabled or disabled based on the presence of the `.cms-previewable` class. If this class is not found the preview @@ -113,7 +469,7 @@ The preview can be affected by calling `enablePreview` and `disablePreview`. You can check if the preview is active by inspecting the `IsPreviewEnabled` entwine property. -## Preview states +### Preview states States are the site stages: _live_, _stage_ etc. Preview states are picked up from the `SilverStripeNavigator`. You can invoke the state change by calling: @@ -133,7 +489,7 @@ You can get the current state by calling: $('.cms-preview').entwine('.ss.preview').getCurrentStateName(); ``` -## Preview sizes +### Preview sizes This selector defines how the preview iframe is rendered, and try to emulate different device sizes. The options are hardcoded. The option names map directly @@ -158,7 +514,7 @@ You can find out current size by calling: $('.cms-preview').entwine('.ss.preview').getCurrentSizeName(); ``` -## Preview modes +### Preview modes Preview modes map to the modes supported by the _threeColumnCompressor_ layout algorithm, see [layout reference](cms_layout) for more details. You @@ -183,7 +539,7 @@ preview is not visible. Currently CMS Actions are a separate area to the preview option selectors, even if they try to appear as one horizontal bar. [/notice] -## Preview API +### Preview API Namespace `ss.preview`, selector `.cms-preview`: @@ -197,6 +553,6 @@ Namespace `ss.preview`, selector `.cms-preview`: * **disablePreview**: deactivate the preview and switch to the _content_ mode. Preview will re-enable itself when new previewable content is loaded. -## Related +### Related * [Reference: Layout](cms_layout)