Improve theme list to allow editing properties

This commit is contained in:
Samuel Georges 2015-03-14 18:09:54 +11:00
parent e479ccbda6
commit 9b8e1ce3c1
13 changed files with 362 additions and 193 deletions

View File

@ -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.

View File

@ -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')

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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.

View File

@ -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

View File

@ -1,15 +1,15 @@
<?php namespace Cms\Controllers;
use Lang;
use Input;
use Yaml;
use Config;
use Backend;
use Redirect;
use BackendMenu;
use Backend\Classes\Controller;
use System\Classes\SettingsManager;
use ApplicationException;
use Cms\Models\ThemeData;
use Backend\Classes\Controller;
use Cms\Classes\Theme as CmsTheme;
use System\Classes\SettingsManager;
use Exception;
/**
@ -50,13 +50,48 @@ class Themes extends Controller
public function index_onSetActiveTheme()
{
CmsTheme::setActiveTheme(Input::get('theme'));
CmsTheme::setActiveTheme(post('theme'));
return [
'#theme-list' => $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;
}
}

View File

@ -0,0 +1,56 @@
<?= Form::ajax('onSaveThemeFields', [
'id' => 'themeFieldsForm',
'data-popup-load-indicator' => true
]) ?>
<input type="hidden" name="theme" value="<?= $themeDir ?>" />
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title">Theme: <?= $themeDir ?></h4>
</div>
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<?= $widget->render() ?>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary">
Save properties
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.close')) ?>
</button>
</div>
<?php endif ?>
<script>
setTimeout(
function(){ $('#themeFieldsForm input.form-control:first').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@ -1,55 +1,12 @@
<?php
$themes = Cms\Classes\Theme::all();
$activeTheme = Cms\Classes\Theme::getActiveTheme();
$cnt = count($themes);
?>
<?php foreach ($themes as $index => $theme): ?>
<?php
$isLast = $index == $cnt-1;
$isActive = $activeTheme && $activeTheme->getDirName() == $theme->getDirName();
$author = $theme->getConfigValue('author');
?>
<div class="layout-row <?= $isLast ? 'last' : null ?> min-size <?= $isActive ? 'active' : null ?>">
<div class="layout-cell min-height theme-thumbnail">
<div class="thumbnail-container"><img src="<?= $theme->getPreviewImageUrl() ?>"/></div>
</div>
<div class="layout-cell min-height theme-description">
<h3><?= e($theme->getConfigValue('name', $theme->getDirName())) ?></h3>
<?php if (strlen($author)): ?>
<p class="author">by <a href="<?= e($theme->getConfigValue('homepage', '#')) ?>"><?= e($author) ?></a></p>
<?php endif ?>
<p class="description"><?= e($theme->getConfigValue('description', 'The theme description is not provided.')) ?></p>
<div class="controls">
<?php if ($isActive): ?>
<button
type="submit"
disabled
class="btn btn-disabled">
<i class="icon-star"></i>
<?= e(trans('cms::lang.theme.active_button')) ?>
</button>
<?php else: ?>
<button
type="submit"
data-request="onSetActiveTheme"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
data-stripe-load-indicator
class="btn btn-primary">
<i class="icon-check"></i>
<?= e(trans('cms::lang.theme.activate_button')) ?>
</button>
<?php endif ?>
<?php if ($theme->hasCustomData()): ?>
<a
href="<?= Backend::url('cms/themes/update/'.$theme->getDirName()) ?>"
class="btn btn-default">
<i class="icon-pencil"></i>
<?= e(trans('cms::lang.theme.customize_button')) ?>
</a>
<?php endif ?>
</div>
</div>
<div id="themeListItem-<?= $theme->getId() ?>" class="layout-row min-size <?= $theme->isActiveTheme() ? 'active' : null ?>">
<?= $this->makePartial('theme_list_item', ['theme' => $theme]) ?>
</div>
<?php endforeach ?>
<div class="layout-row links">
@ -57,6 +14,11 @@
<!-- Spacer -->
</div>
<div class="layout-cell theme-description">
<a class="find-more-themes" href="http://octobercms.com/themes"><?= e(trans('cms::lang.theme.find_more_themes')) ?></a>
<a
class="find-more-themes"
href="http://octobercms.com/themes"
target="_blank">
<?= e(trans('cms::lang.theme.find_more_themes')) ?>
</a>
</div>
</div>

View File

@ -0,0 +1,99 @@
<?php
$author = $theme->getConfigValue('author');
?>
<div class="layout-cell min-height theme-thumbnail">
<div class="thumbnail-container"><img src="<?= $theme->getPreviewImageUrl() ?>"/></div>
</div>
<div class="layout-cell min-height theme-description">
<h3><?= e($theme->getConfigValue('name', $theme->getDirName())) ?></h3>
<?php if (strlen($author)): ?>
<p class="author">by <a href="<?= e($theme->getConfigValue('homepage', '#')) ?>"><?= e($author) ?></a></p>
<?php endif ?>
<p class="description">
<?= e($theme->getConfigValue('description', 'The theme description is not provided.')) ?>
</p>
<div class="controls">
<?php if ($theme->isActiveTheme()): ?>
<button
type="submit"
disabled
class="btn btn-disabled">
<i class="icon-star"></i>
<?= e(trans('cms::lang.theme.active_button')) ?>
</button>
<?php else: ?>
<button
type="submit"
data-request="onSetActiveTheme"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
data-stripe-load-indicator
class="btn btn-primary">
<i class="icon-check"></i>
<?= e(trans('cms::lang.theme.activate_button')) ?>
</button>
<?php endif ?>
<?php if ($theme->hasCustomData()): ?>
<a
href="<?= Backend::url('cms/themes/update/'.$theme->getDirName()) ?>"
class="btn btn-default">
<i class="icon-paint-brush"></i>
<?= e(trans('cms::lang.theme.customize_button')) ?>
</a>
<?php endif ?>
<div class="dropdown">
<button
data-toggle="dropdown"
class="btn btn-default">
<i class="icon-wrench"></i>
Manage
</button>
<ul class="dropdown-menu" role="menu" data-dropdown-title="Manage theme">
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
data-control="popup"
data-size="huge"
data-handler="onLoadThemeFieldsForm"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
href="javascript:;"
class="oc-icon-pencil">
Edit properties
</a>
</li>
<!--
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
href="javascript:;"
class="oc-icon-download">
Download
</a>
</li>
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
href="javascript:;"
class="oc-icon-copy">
Duplicate
</a>
</li>
<li role="presentation" class="divider"></li>
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
href="javascript:;"
class="oc-icon-trash">
Delete
</a>
</li>
-->
</ul>
</div>
</div>
</div>

View File

@ -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.'