diff --git a/CHANGELOG.md b/CHANGELOG.md index 4efa8332d..4f6c31a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +* **Build 22x** (2015-03-xx) + - Form Tabs now support specifying a default tab using the `defaultTab` option (see Backend > Forms docs). + - Improved the Theme management features: Edit properties, download, duplicate and delete. + * **Build 222** (2015-03-11) - Form fields can now use a simpler interface for using the Input preset converter (see Backend > Forms docs). - The event `cms.page.init` no longer passes the URL as the third parameter, `$controller->getRouter()->getUrl()` should be used instead. diff --git a/modules/backend/assets/js/october-min.js b/modules/backend/assets/js/october-min.js index 4b6ed9a32..17b58ce59 100644 --- a/modules/backend/assets/js/october-min.js +++ b/modules/backend/assets/js/october-min.js @@ -87,7 +87,7 @@ $el.on('oc.triggerOn.update',function(e){e.stopPropagation() self.onConditionChanged()}) self.onConditionChanged()} TriggerOn.prototype.onConditionChanged=function(){if(this.triggerCondition=='checked'){this.updateTarget($(this.options.trigger+':checked',this.triggerParent).length>0)} -else if(this.triggerCondition=='value'){this.updateTarget($(this.options.trigger,this.triggerParent).val()==this.triggerConditionValue)}} +else if(this.triggerCondition=='value'){var trigger=$(this.options.trigger+':checked',this.triggerParent);if(trigger.length){this.updateTarget(trigger.val()==this.triggerConditionValue)}else{this.updateTarget($(this.options.trigger,this.triggerParent).val()==this.triggerConditionValue)}}} TriggerOn.prototype.updateTarget=function(status){if(this.options.triggerAction=='show') this.$el.toggleClass('hide',!status).trigger('hide',[!status]) else if(this.options.triggerAction=='hide') diff --git a/modules/backend/classes/Controller.php b/modules/backend/classes/Controller.php index f5cda1633..3fba8270a 100644 --- a/modules/backend/classes/Controller.php +++ b/modules/backend/classes/Controller.php @@ -496,7 +496,7 @@ class Controller extends Extendable $this->suppressView = true; $this->execPageAction($this->action, $this->params); - foreach ($this->widget as $widget) { + foreach ((array) $this->widget as $widget) { if (method_exists($widget, $handler)) { $result = call_user_func_array([$widget, $handler], $this->params); return ($result) ?: true; diff --git a/modules/cms/assets/css/october.theme-selector.css b/modules/cms/assets/css/october.theme-selector.css index 92d8cca90..6cf246c36 100644 --- a/modules/cms/assets/css/october.theme-selector.css +++ b/modules/cms/assets/css/october.theme-selector.css @@ -7,7 +7,7 @@ .theme-selector-layout .theme-thumbnail { width: 288px; background: #ecf0f1; - border-bottom: 1px solid #e3e7e9; + border-top: 1px solid #e3e7e9; } .theme-selector-layout .theme-thumbnail img { opacity: 0.6; @@ -15,7 +15,7 @@ width: 240px; } .theme-selector-layout .theme-description { - border-bottom: 1px solid #f2f3f4; + border-top: 1px solid #f2f3f4; } .theme-selector-layout .theme-description h3, .theme-selector-layout .theme-description p { @@ -39,15 +39,21 @@ line-height: 180%; margin-bottom: 30px; } -.theme-selector-layout .theme-description .controls button i.icon-star { +.theme-selector-layout .theme-description .controls .btn > i { margin-right: 5px; - color: #f1a84e; font-size: 16px; - vertical-align: middle; + position: relative; + top: 1px; +} +.theme-selector-layout .theme-description .controls .btn > i.icon-star { + color: #f1a84e; +} +.theme-selector-layout .theme-description .controls .dropdown { + display: inline-block; } .theme-selector-layout .layout-row.active .theme-thumbnail { background: #bdc3c7; - border-bottom-color: #bdc3c7; + border-top-color: #bdc3c7; } .theme-selector-layout .layout-row.active .thumbnail-container { position: relative; @@ -77,9 +83,17 @@ opacity: 1; filter: alpha(opacity=100); } -.theme-selector-layout .layout-row.last .theme-description, -.theme-selector-layout .layout-row.last .theme-thumbnail { - border-bottom: none!important; +.theme-selector-layout .layout-row:first-child .theme-description, +.theme-selector-layout .layout-row.links .theme-description, +.theme-selector-layout .layout-row:first-child .theme-thumbnail, +.theme-selector-layout .layout-row.links .theme-thumbnail { + border-top: none; +} +.theme-selector-layout .layout-row.links .theme-thumbnail { + border-bottom: 1px solid #e3e7e9; +} +.theme-selector-layout .layout-row.links .theme-description { + border-bottom: 1px solid #f2f3f4; } .theme-selector-layout .find-more-themes { background: #ecf0f1; diff --git a/modules/cms/assets/less/css/october.theme-selector.css b/modules/cms/assets/less/css/october.theme-selector.css deleted file mode 100644 index 92d8cca90..000000000 --- a/modules/cms/assets/less/css/october.theme-selector.css +++ /dev/null @@ -1,112 +0,0 @@ -.theme-selector-layout .layout-cell { - padding: 24px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.theme-selector-layout .theme-thumbnail { - width: 288px; - background: #ecf0f1; - border-bottom: 1px solid #e3e7e9; -} -.theme-selector-layout .theme-thumbnail img { - opacity: 0.6; - filter: alpha(opacity=60); - width: 240px; -} -.theme-selector-layout .theme-description { - border-bottom: 1px solid #f2f3f4; -} -.theme-selector-layout .theme-description h3, -.theme-selector-layout .theme-description p { - opacity: 0.6; - filter: alpha(opacity=60); -} -.theme-selector-layout .theme-description h3 { - margin: 0 0 25px 0; - font-size: 28px; - color: #2b3e50; - display: inline-block; -} -.theme-selector-layout .theme-description p.author { - font-size: 13px; - display: inline-block; - color: #808c8d; -} -.theme-selector-layout .theme-description p.description { - color: #2b3e50; - font-size: 14px; - line-height: 180%; - margin-bottom: 30px; -} -.theme-selector-layout .theme-description .controls button i.icon-star { - margin-right: 5px; - color: #f1a84e; - font-size: 16px; - vertical-align: middle; -} -.theme-selector-layout .layout-row.active .theme-thumbnail { - background: #bdc3c7; - border-bottom-color: #bdc3c7; -} -.theme-selector-layout .layout-row.active .thumbnail-container { - position: relative; -} -.theme-selector-layout .layout-row.active .thumbnail-container:after { - content: ''; - display: block; - width: 0; - height: 0; - border-top: 14px solid transparent; - border-bottom: 14px solid transparent; - border-left: 15px solid #bdc3c7; - position: absolute; - right: -35px; - top: 50%; - margin-top: -14px; -} -.theme-selector-layout .layout-row.active .theme-description h3, -.theme-selector-layout .layout-row:hover .theme-description h3, -.theme-selector-layout .layout-row.active .theme-description p, -.theme-selector-layout .layout-row:hover .theme-description p { - opacity: 1; - filter: alpha(opacity=100); -} -.theme-selector-layout .layout-row.active .theme-thumbnail img, -.theme-selector-layout .layout-row:hover .theme-thumbnail img { - opacity: 1; - filter: alpha(opacity=100); -} -.theme-selector-layout .layout-row.last .theme-description, -.theme-selector-layout .layout-row.last .theme-thumbnail { - border-bottom: none!important; -} -.theme-selector-layout .find-more-themes { - background: #ecf0f1; - color: #2b3e50; - text-decoration: none; - display: block; - padding: 20px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.theme-selector-layout .find-more-themes:hover { - background: #1795f1; - color: white; -} -@media (max-width: 768px) { - .theme-selector-layout .layout-cell, - .theme-selector-layout .layout-row { - display: block!important; - width: auto!important; - height: auto!important; - } - .theme-selector-layout .theme-thumbnail img { - width: 100%; - } - .theme-selector-layout .layout-row.links .theme-thumbnail { - background: transparent; - padding: 0; - } -} diff --git a/modules/cms/assets/less/october.theme-selector.less b/modules/cms/assets/less/october.theme-selector.less index 1da56c885..ee4bc042b 100644 --- a/modules/cms/assets/less/october.theme-selector.less +++ b/modules/cms/assets/less/october.theme-selector.less @@ -9,7 +9,7 @@ .theme-thumbnail { width: 288px; background: #ecf0f1; - border-bottom: 1px solid #e3e7e9; + border-top: 1px solid #e3e7e9; img { .opacity(0.6); @@ -18,7 +18,7 @@ } .theme-description { - border-bottom: 1px solid #f2f3f4; + border-top: 1px solid #f2f3f4; h3, p { .opacity(0.6); @@ -45,11 +45,19 @@ } .controls { - button i.icon-star { + .btn > i { margin-right: 5px; - color: #f1a84e; font-size: 16px; - vertical-align: middle; + position: relative; + top: 1px; + + &.icon-star { + color: #f1a84e; + } + } + + .dropdown { + display: inline-block; } } } @@ -57,8 +65,7 @@ .layout-row.active { .theme-thumbnail { background: #bdc3c7; - border-bottom-color: #bdc3c7; - + border-top-color: #bdc3c7; } .thumbnail-container { @@ -74,7 +81,7 @@ } } - .layout-row { + .layout-row { &.active, &:hover { .theme-description { h3, p { @@ -89,9 +96,18 @@ } } - &.last { + &:first-child, &.links { .theme-description, .theme-thumbnail { - border-bottom: none!important; + border-top: none; + } + } + + &.links { + .theme-thumbnail { + border-bottom: 1px solid #e3e7e9; + } + .theme-description { + border-bottom: 1px solid #f2f3f4; } } } diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index ec8c50356..b66c65cc5 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -8,8 +8,9 @@ use Cache; use Event; use Config; use DbDongle; -use System\Models\Parameters; use SystemException; +use ApplicationException; +use System\Models\Parameters; use Cms\Models\ThemeData; use DirectoryIterator; @@ -89,6 +90,16 @@ class Theme return $this->dirName; } + /** + * Helper for {{ theme.id }} twig vars + * Returns a unique string for this theme. + * @return string + */ + public function getId() + { + return snake_case(str_replace('/', '-', $this->getDirName())); + } + /** * Determines if a theme with given directory name exists * @param string $dirName The theme directory @@ -113,6 +124,15 @@ class Theme return Page::listInTheme($this, $skipCache); } + /** + * Returns true if this theme is the chosen active theme. + */ + public function isActiveTheme() + { + $activeTheme = self::getActiveTheme(); + return $activeTheme && $activeTheme->getDirName() == $this->getDirName(); + } + /** * Returns the active theme. * By default the active theme is loaded from the cms.activeTheme parameter, @@ -258,6 +278,28 @@ class Theme return array_get($this->getConfig(), $name, $default); } + /** + * Writes to the theme.yaml file with the supplied array values. + * @param array $values Data to write + * @param array $overwrite If true, undefined values are removed. + * @return void + */ + public function writeConfig($values = [], $overwrite = false) + { + if (!$overwrite) { + $values = $values + $this->getConfig(); + } + + $path = $this->getPath().'/theme.yaml'; + if (!File::exists($path)) { + throw new ApplicationException('Path does not exist: '.$path); + } + + $contents = Yaml::render($values); + File::put($path, $contents); + $this->configCache = $values; + } + /** * Returns the theme preview image URL. * If the image file doesn't exist returns the placeholder image URL. diff --git a/modules/cms/classes/theme/fields.yaml b/modules/cms/classes/theme/fields.yaml new file mode 100644 index 000000000..408ee75fd --- /dev/null +++ b/modules/cms/classes/theme/fields.yaml @@ -0,0 +1,39 @@ +# =================================== +# Form Field Definitions +# =================================== + +tabs: + defaultTab: Properties + fields: + + name: + label: Name + placeholder: New theme name + span: auto + attributes: + default-focus: 1 + + directory_name: + label: Directory name + disabled: true + span: auto + + description: + label: Description + placeholder: Theme description + type: textarea + size: tiny + + author: + label: Author + placeholder: Person or company name + span: auto + + homepage: + label: Homepage + placeholder: Website URL + span: auto + + code: + label: Code + placeholder: A unique code for this theme used for distribution diff --git a/modules/cms/controllers/Themes.php b/modules/cms/controllers/Themes.php index 401242c24..7baf9f946 100644 --- a/modules/cms/controllers/Themes.php +++ b/modules/cms/controllers/Themes.php @@ -1,15 +1,15 @@ $this->makePartial('theme_list') ]; } + // + // Theme properties + // + + public function index_onLoadThemeFieldsForm() + { + $theme = $this->findThemeObject(); + $this->vars['widget'] = $this->makeThemeFieldsFormWidget($theme); + $this->vars['themeDir'] = $theme->getDirName(); + + return $this->makePartial('theme_fields_form'); + } + + public function index_onSaveThemeFields() + { + $theme = $this->findThemeObject(); + $widget = $this->makeThemeFieldsFormWidget($theme); + $theme->writeConfig($widget->getSaveData()); + + return ['#themeListItem-'.$theme->getId() => $this->makePartial('theme_list_item', ['theme' => $theme])]; + } + + protected function makeThemeFieldsFormWidget($theme) + { + $widgetConfig = $this->makeConfig('~/modules/cms/classes/theme/fields.yaml'); + $widgetConfig->alias = 'form'.studly_case($theme->getDirName()); + $widgetConfig->model = $theme; + $widgetConfig->data = $theme->getConfig(); + $widgetConfig->data['directory_name'] = $theme->getDirName(); + $widgetConfig->arrayName = 'Theme'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); + return $widget; + } + // // Theme customization // @@ -88,9 +123,7 @@ class Themes extends Controller protected function getThemeData($dirName) { - if (!$theme = CmsTheme::load($dirName)) - throw new Exception(Lang::get('Unable to find theme with name :name', $dirName)); - + $theme = $this->findThemeObject($dirName); $model = ThemeData::forTheme($theme); return $model; } @@ -101,9 +134,7 @@ class Themes extends Controller protected function formExtendFields($form) { $model = $form->model; - - if (!$theme = CmsTheme::load($model->theme)) - throw new Exception(Lang::get('Unable to find theme with name :name', $this->theme)); + $theme = $this->findThemeObject($model->theme); if ($fields = $theme->getConfigValue('form.fields')) { $form->addFields($fields); @@ -118,4 +149,21 @@ class Themes extends Controller } } + // + // Helpers + // + + protected function findThemeObject($name = null) + { + if ($name === null) { + $name = post('theme'); + } + + if (!$name || (!$theme = CmsTheme::load($name))) { + throw new ApplicationException(trans('cms::lang.theme.not_found_name', ['name' => $name])); + } + + return $theme; + } + } diff --git a/modules/cms/controllers/themes/_theme_fields_form.htm b/modules/cms/controllers/themes/_theme_fields_form.htm new file mode 100644 index 000000000..39586b2a9 --- /dev/null +++ b/modules/cms/controllers/themes/_theme_fields_form.htm @@ -0,0 +1,56 @@ + 'themeFieldsForm', + 'data-popup-load-indicator' => true +]) ?> + + + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/modules/cms/controllers/themes/_theme_list.htm b/modules/cms/controllers/themes/_theme_list.htm index 8a2805bb7..a221e4b92 100644 --- a/modules/cms/controllers/themes/_theme_list.htm +++ b/modules/cms/controllers/themes/_theme_list.htm @@ -1,55 +1,12 @@ $theme): ?> - getDirName() == $theme->getDirName(); - $author = $theme->getConfigValue('author'); - ?> -
-
-
-
-
-

getConfigValue('name', $theme->getDirName())) ?>

- -

by

- -

getConfigValue('description', 'The theme description is not provided.')) ?>

-
- - - - - - hasCustomData()): ?> - - - - - -
-
+ +
+ makePartial('theme_list_item', ['theme' => $theme]) ?>
+
- + + +
\ No newline at end of file diff --git a/modules/cms/controllers/themes/_theme_list_item.htm b/modules/cms/controllers/themes/_theme_list_item.htm new file mode 100644 index 000000000..05a282498 --- /dev/null +++ b/modules/cms/controllers/themes/_theme_list_item.htm @@ -0,0 +1,99 @@ +getConfigValue('author'); +?> + +
+
+
+
+

getConfigValue('name', $theme->getDirName())) ?>

+ +

by

+ +

+ getConfigValue('description', 'The theme description is not provided.')) ?> +

+
+ + isActiveTheme()): ?> + + + + + hasCustomData()): ?> + + + + + + +
+
diff --git a/modules/cms/lang/en/lang.php b/modules/cms/lang/en/lang.php index 6c0b791b5..db4ee938d 100644 --- a/modules/cms/lang/en/lang.php +++ b/modules/cms/lang/en/lang.php @@ -13,6 +13,7 @@ return [ 'file_name_required' => 'The File Name field is required.' ], 'theme' => [ + 'not_found_name' => "The theme ':name' is not found.", 'active' => [ 'not_set' => 'The active theme is not set.', 'not_found' => 'The active theme is not found.'