diff --git a/client/src/boot/registerComponents.js b/client/src/boot/registerComponents.js index d01ab3ca5..1ef5d089e 100644 --- a/client/src/boot/registerComponents.js +++ b/client/src/boot/registerComponents.js @@ -1,6 +1,7 @@ import { Field as ReduxFormField } from 'redux-form'; import Injector from 'lib/Injector'; import ActionMenu from 'components/ActionMenu/ActionMenu'; +import CmsSiteName from 'components/CmsSiteName/CmsSiteName'; import Badge from 'components/Badge/Badge'; import Button from 'components/Button/Button'; import BackButton from 'components/Button/BackButton'; @@ -100,5 +101,6 @@ export default () => { NumberField, PopoverOptionSet, ToastsContainer, + CmsSiteName }); }; diff --git a/client/src/bundles/bundle.js b/client/src/bundles/bundle.js index b62fea0c1..1fb85a535 100644 --- a/client/src/bundles/bundle.js +++ b/client/src/bundles/bundle.js @@ -100,6 +100,7 @@ require('../legacy/AddToCampaignForm'); require('../legacy/SecurityAdmin'); require('../legacy/ModelAdmin'); require('../legacy/ToastsContainer'); +require('../legacy/BootstrapComponent'); // Legacy form fields // Fields used by core legacy UIs, or available to users diff --git a/client/src/components/CmsSiteName/CmsSiteName.js b/client/src/components/CmsSiteName/CmsSiteName.js new file mode 100644 index 000000000..8df8ff2ae --- /dev/null +++ b/client/src/components/CmsSiteName/CmsSiteName.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +const CmsSiteName = ({ title, baseHref }) => ( +
+ + + {title} +
+); + +CmsSiteName.propTypes = { + title: PropTypes.string, + baseHref: PropTypes.string, +}; + +CmsSiteName.defaultProps = { +}; + +export default CmsSiteName; diff --git a/client/src/legacy/BootstrapComponent.js b/client/src/legacy/BootstrapComponent.js new file mode 100644 index 000000000..757eef371 --- /dev/null +++ b/client/src/legacy/BootstrapComponent.js @@ -0,0 +1,48 @@ +/* global ss */ +import jQuery from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { loadComponent } from 'lib/Injector'; + +jQuery.entwine('ss', ($) => { + $('.js-injector-boot .bootstrap-component').entwine({ + + Component: null, + + onmatch() { + const cmsContent = this.closest('.cms-content').attr('id'); + const context = (cmsContent) + ? { context: cmsContent } + : {}; + + const componentName = this.data('component'); + const Component = loadComponent(componentName, context); + + this.setComponent(Component); + this._super(); + this.refresh(); + }, + + refresh() { + const props = this.getProps(); + const Component = this.getComponent(); + ReactDOM.render(, this[0]); + }, + + /** + * Find the selected node and get attributes associated to attach the data to the form + * + * @returns {Object} + */ + getProps() { + return $(this).data('props') || {}; + }, + + /** + * Remove the component when unmatching + */ + onunmatch() { + ReactDOM.unmountComponentAtNode(this[0]); + }, + }); +}); diff --git a/code/LeftAndMain.php b/code/LeftAndMain.php index 1ea9a376a..ab5db43e7 100644 --- a/code/LeftAndMain.php +++ b/code/LeftAndMain.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use LogicException; use ReflectionClass; +use SilverStripe\Admin\React\SiteName; use SilverStripe\CMS\Controllers\SilverStripeNavigator; use SilverStripe\Control\ContentNegotiator; use SilverStripe\Control\Controller; @@ -1110,6 +1111,11 @@ public function Menu() return $this->renderWith($this->getTemplatesWithSuffix('_Menu')); } + public function SiteName() + { + return new SiteName(); + } + /** * @todo Wrap in CMSMenu instance accessor * @return ArrayData A single menu entry (see {@link MainMenu}) @@ -1658,11 +1664,11 @@ public function currentPageID() if (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) { return $this->urlParams['ID']; } - + if (is_numeric($this->getRequest()->param('ID'))) { return $this->getRequest()->param('ID'); } - + /** @deprecated */ $session = $this->getRequest()->getSession(); return $session->get($this->sessionNamespace() . ".currentPage") ?: null; diff --git a/code/React/BootstrapComponent.php b/code/React/BootstrapComponent.php new file mode 100644 index 000000000..145437956 --- /dev/null +++ b/code/React/BootstrapComponent.php @@ -0,0 +1,152 @@ + 'HTMLFragment' + ]; + + private $attributes = []; + + protected $extraClasses = []; + + public function forTemplate() + { + $return = $this->renderWith($this->getTemplates()); + return $return; + } + + public function getTemplates(): array + { + return [self::class, 'SilverStripe\\Admin\\React\\BootstrapComponent']; + } + + public function getAttributesHTML($attrs = null) + { + $exclude = (is_string($attrs)) ? func_get_args() : null; + + $attrs = $this->getAttributes(); + + // Remove empty + $attrs = array_filter((array)$attrs, function ($value) { + return ($value || $value === 0); + }); + + // Remove excluded + if ($exclude) { + $attrs = array_diff_key($attrs, array_flip($exclude)); + } + + // Prepare HTML-friendly 'method' attribute (lower-case) + if (isset($attrs['method'])) { + $attrs['method'] = strtolower($attrs['method']); + } + + // Create markup + $parts = []; + foreach ($attrs as $name => $value) { + if ($value === true) { + $value = $name; + } + + $parts[] = sprintf('%s="%s"', Convert::raw2att($name), Convert::raw2att($value)); + } + + return implode(' ', $parts); + } + + /** + * @param string $name + * @param string $value + * @return $this + */ + public function setAttribute($name, $value) + { + $this->attributes[$name] = $value; + return $this; + } + + /** + * @param string $name + * @return string + */ + public function getAttribute($name) + { + if (isset($this->attributes[$name])) { + return $this->attributes[$name]; + } + return null; + } + + /** + * @return array + */ + public function getAttributes() + { + $attrs = [ + 'class' => $this->extraClass(), + 'data-component' => $this->getComponent(), + 'data-props' => json_encode($this->getProps()), + ]; + + $attrs = array_merge($attrs, $this->attributes); + + return $attrs; + } + + /** + * Compiles all CSS-classes. + * + * @return string + */ + public function extraClass() + { + return 'bootstrap-component ' . implode(' ', array_unique($this->extraClasses)); + } + + /** + * Add a CSS-class to the form-container. If needed, multiple classes can + * be added by delimiting a string with spaces. + * + * @param string $class A string containing a classname or several class + * names delimited by a single space. + * @return $this + */ + public function addExtraClass($class) + { + //split at white space + $classes = preg_split('/\s+/', $class); + foreach ($classes as $class) { + //add classes one by one + $this->extraClasses[$class] = $class; + } + return $this; + } + + /** + * Remove a CSS-class from the form-container. Multiple class names can + * be passed through as a space delimited string + * + * @param string $class + * @return $this + */ + public function removeExtraClass($class) + { + //split at white space + $classes = preg_split('/\s+/', $class); + foreach ($classes as $class) { + //unset one by one + unset($this->extraClasses[$class]); + } + return $this; + } + + abstract public function getProps(): array; + + abstract public function getComponent(): string; +} diff --git a/code/React/SiteName.php b/code/React/SiteName.php new file mode 100644 index 000000000..03ea639ed --- /dev/null +++ b/code/React/SiteName.php @@ -0,0 +1,27 @@ + $config->Title ?? LeftAndMain::config()->get('application_name'), + 'baseHref' => Director::absoluteBaseURL() + ]; + } + + public function getComponent(): string + { + return 'CmsSiteName'; + } +} diff --git a/templates/SilverStripe/Admin/Includes/LeftAndMain_Menu.ss b/templates/SilverStripe/Admin/Includes/LeftAndMain_Menu.ss index 29a3c9b20..a49408068 100644 --- a/templates/SilverStripe/Admin/Includes/LeftAndMain_Menu.ss +++ b/templates/SilverStripe/Admin/Includes/LeftAndMain_Menu.ss @@ -2,7 +2,7 @@