Merge pull request #4 from octobercms/develop

上游合并
This commit is contained in:
Liu Xing 2015-04-08 18:48:53 +08:00
commit fde5a16da1
173 changed files with 6883 additions and 3143 deletions

View File

@ -1,3 +1,9 @@
* **Build 239** (2015-04-06)
- Installing plugins has a new interface and themes can now be installed using the back-end.
* **Build 236** (2015-03-28)
- Default context of `manage` and `pivot` forms is now *create* and *update* respectively, instead of the old value *relation*. Use the `context` option to set it back to the old value (see Backend > Relations docs).
* **Build 229** (2015-03-19)
- Belongs-to-many model relations now support defining a custom pivot model with the `pivotModel` option (see Database > Model docs).
- The config definitions for behavior `RelationController` have been refactored. When using `pivot` mode all columns and fields should now reside in a `pivot[]` array (see Backend > Relations docs).

View File

@ -8,7 +8,7 @@ October's mission is to show the world that web development is not rocket scienc
### Learning October
The best place to learn October is by [reading the documentation](http://octobercms.com/docs).
The best place to learn October is by [reading the documentation](http://octobercms.com/docs) or [the resources page](http://octobercms.com/resources).
### Installing October
@ -50,4 +50,4 @@ The OctoberCMS platform is open-sourced software licensed under the [MIT license
### Contributing
Before sending a Pull Request, be sure to review the [Contributing Guidelines](CONTRIBUTING.md) first.
Before sending a Pull Request, be sure to review the [Contributing Guidelines](CONTRIBUTING.md) first.

View File

@ -98,6 +98,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => SettingsManager::CATEGORY_SYSTEM,
'icon' => 'icon-paint-brush',
'class' => 'Backend\Models\BrandSettings',
'permissions' => ['backend.manage_branding'],
'order' => 500
],
'editor' => [
@ -134,7 +135,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => SettingsManager::CATEGORY_LOGS,
'icon' => 'icon-lock',
'url' => Backend::url('backend/accesslogs'),
'permissions' => ['backend.access_admin_logs'],
'permissions' => ['system.access_logs'],
'order' => 800
]
]);
@ -152,6 +153,10 @@ class ServiceProvider extends ModuleServiceProvider
'backend.manage_users' => [
'label' => 'system::lang.permissions.manage_other_administrators',
'tab' => 'system::lang.permissions.name'
],
'backend.manage_branding' => [
'label' => 'system::lang.permissions.manage_branding',
'tab' => 'system::lang.permissions.name'
]
]);
});

View File

@ -181,6 +181,7 @@ table.table.data tbody td.column-slim{padding-left:0;padding-right:0}
table.table.data tbody td.column-compact{padding:0}
table.table.data tfoot a{color:#666666;text-decoration:none}
table.table.data tfoot td,table.table.data tfoot th{border-color:#e2e2e2;padding:10px 15px}
table.table.data th.list-cell-type-switch,table.table.data td.list-cell-type-switch{text-align:center}
table.table.data .list-checkbox{padding-left:16px;padding-right:8px;width:52px;vertical-align:top;border-right:1px solid #eeeeee}
table.table.data .list-checkbox .checkbox{margin:0}
table.table.data .list-checkbox .custom-checkbox{position:relative;top:5px;left:-2px}
@ -774,8 +775,7 @@ ul.status-list li span.status.info{background:#5bc0de}
.control-breadcrumb li a:hover{color:#ecf0f1}
.control-breadcrumb li:after{font-size:14px;line-height:14px;display:inline-block;margin-left:6px;margin-right:2px;vertical-align:baseline;color:#9da3a7;font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em;content:"\f105"}
.control-breadcrumb li:last-child:after{content:''}
.control-breadcrumb + .content-tabs,.control-breadcrumb + .padded-container{margin-top:-20px}
.control-breadcrumb.breadcrumb-flush{margin-bottom:0}
body.breadcrumb-flush .control-breadcrumb,.control-breadcrumb.breadcrumb-flush{margin-bottom:0}
body.slim-container .control-breadcrumb{margin-left:0;margin-right:0}
body.compact-container .control-breadcrumb{margin-top:0;margin-left:0;margin-right:0}
div.control-popover{position:absolute;background-clip:content-box;left:0;top:0;z-index:570;visibility:hidden}

View File

@ -87,17 +87,18 @@ $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'){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)}}}
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])
this.$el.toggleClass('hide',!status).trigger('hide.oc.triggerapi',[!status])
else if(this.options.triggerAction=='hide')
this.$el.toggleClass('hide',status).trigger('hide',[status])
this.$el.toggleClass('hide',status).trigger('hide.oc.triggerapi',[status])
else if(this.options.triggerAction=='enable')
this.$el.prop('disabled',!status).trigger('disable',[!status]).toggleClass('control-disabled',!status)
this.$el.prop('disabled',!status).trigger('disable.oc.triggerapi',[!status]).toggleClass('control-disabled',!status)
else if(this.options.triggerAction=='disable')
this.$el.prop('disabled',status).trigger('disable',[status]).toggleClass('control-disabled',status)
this.$el.prop('disabled',status).trigger('disable.oc.triggerapi',[status]).toggleClass('control-disabled',status)
else if(this.options.triggerAction=='empty'&&status)
this.$el.trigger('empty').val('')
this.$el.trigger('empty.oc.triggerapi').val('')
if(this.options.triggerAction=='show'||this.options.triggerAction=='hide')
this.fixButtonClasses()
$(window).trigger('resize')}
@ -120,7 +121,7 @@ this.scrollClassContainer=this.options.scrollClassContainer?$(this.options.scrol
if(this.options.scrollMarkerContainer)
$(this.options.scrollMarkerContainer).append($('<span class="before scroll-marker"></span><span class="after scroll-marker"></span>'))
$el.mousewheel(function(event){if(!self.options.allowScroll)
return;var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):((event.deltaFactor*event.deltaX)*-1)
return;var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):(event.deltaFactor*event.deltaX)
return!scrollWheel(offset)})
$el.on('mousedown',function(event){startDrag(event)
return false})
@ -394,7 +395,9 @@ this.$el.on('modified.oc.tab',function(ev){ev.preventDefault()
self.modifyTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'))})
this.$el.on('unmodified.oc.tab',function(ev){ev.preventDefault()
self.unmodifyTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'))})
this.$tabsContainer.on('shown.bs.tab','li',function(){$(window).trigger('oc.updateUi')})
this.$tabsContainer.on('shown.bs.tab','li',function(){$(window).trigger('oc.updateUi')
var tabUrl=$('> a',this).data('tabUrl')
if(tabUrl){window.history.replaceState({},'Tab link reference',tabUrl)}})
if(this.options.slidable){this.$pagesContainer.touchwipe({wipeRight:function(){self.prev();},wipeLeft:function(){self.next();},preventDefaultEvents:false,min_move_x:60});}
this.$tabsContainer.toolbar({scrollClassContainer:this.$el})
this.updateClasses()}
@ -788,7 +791,7 @@ if(isTouch){this.$el.on('touchstart',function(event){var touchEvent=event.origin
event.stopPropagation()}})}
else{this.$thumb.on('mousedown',function(event){startDrag(event)})
this.$track.on('mouseup',function(event){moveDrag(event)})}
$el.mousewheel(function(event){var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):((event.deltaFactor*event.deltaX)*-1)
$el.mousewheel(function(event){var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):(event.deltaFactor*event.deltaX)
return!scrollWheel(offset*self.options.scrollSpeed)})
$el.on('oc.scrollbar.gotoStart',function(event){self.options.vertical?$el.scrollTop(0):$el.scrollLeft(0)
self.update()

View File

@ -60,7 +60,7 @@
var offset = self.options.vertical
? ((event.deltaFactor * event.deltaY) * -1)
: ((event.deltaFactor * event.deltaX) * -1)
: (event.deltaFactor * event.deltaX)
return !scrollWheel(offset)
})

View File

@ -68,7 +68,7 @@
$el.mousewheel(function (event){
var offset = self.options.vertical
? ((event.deltaFactor * event.deltaY) * -1)
: ((event.deltaFactor * event.deltaX) * -1)
: (event.deltaFactor * event.deltaX)
return !scrollWheel(offset * self.options.scrollSpeed)
})

View File

@ -107,8 +107,13 @@
})
this.$tabsContainer.on('shown.bs.tab', 'li', function(){
// self.$tabsContainer.dragScroll('fixScrollClasses')
$(window).trigger('oc.updateUi')
// self.$tabsContainer.dragScroll('fixScrollClasses')
$(window).trigger('oc.updateUi')
var tabUrl = $('> a', this).data('tabUrl')
if (tabUrl) {
window.history.replaceState({}, 'Tab link reference', tabUrl)
}
})
if (this.options.slidable) {

View File

@ -81,7 +81,8 @@
var trigger = $(this.options.trigger + ':checked', this.triggerParent);
if (trigger.length) {
this.updateTarget(trigger.val() == this.triggerConditionValue)
} else {
}
else {
this.updateTarget($(this.options.trigger, this.triggerParent).val() == this.triggerConditionValue)
}
}
@ -89,15 +90,15 @@
TriggerOn.prototype.updateTarget = function(status) {
if (this.options.triggerAction == 'show')
this.$el.toggleClass('hide', !status).trigger('hide', [!status])
this.$el.toggleClass('hide', !status).trigger('hide.oc.triggerapi', [!status])
else if (this.options.triggerAction == 'hide')
this.$el.toggleClass('hide', status).trigger('hide', [status])
this.$el.toggleClass('hide', status).trigger('hide.oc.triggerapi', [status])
else if (this.options.triggerAction == 'enable')
this.$el.prop('disabled', !status).trigger('disable', [!status]).toggleClass('control-disabled', !status)
this.$el.prop('disabled', !status).trigger('disable.oc.triggerapi', [!status]).toggleClass('control-disabled', !status)
else if (this.options.triggerAction == 'disable')
this.$el.prop('disabled', status).trigger('disable', [status]).toggleClass('control-disabled', status)
this.$el.prop('disabled', status).trigger('disable.oc.triggerapi', [status]).toggleClass('control-disabled', status)
else if (this.options.triggerAction == 'empty' && status)
this.$el.trigger('empty').val('')
this.$el.trigger('empty.oc.triggerapi').val('')
if (this.options.triggerAction == 'show' || this.options.triggerAction == 'hide')
this.fixButtonClasses()

View File

@ -39,15 +39,12 @@
content:'';
}
}
}
+ .content-tabs, + .padded-container {
margin-top: -20px;
}
// Breadcrumb to sit flush to the element below
&.breadcrumb-flush {
margin-bottom: 0;
}
// Breadcrumb to sit flush to the element below
body.breadcrumb-flush .control-breadcrumb,
.control-breadcrumb.breadcrumb-flush {
margin-bottom: 0;
}
body.slim-container {

View File

@ -256,6 +256,11 @@ table.table.data {
}
}
th.list-cell-type-switch,
td.list-cell-type-switch {
text-align: center;
}
.list-checkbox {
padding-left: 16px;
padding-right: 8px;

View File

@ -1,3 +1,15 @@
//
// Z-Index frequencies:
//
// 0-200 - Base layer (content)
// 200-400 - Base menus / dropdowns
// 400-600 - Secondary layer (full screen)
// 600-800 - Secondary menus / dropdowns
// 800-1000 - Tertiary layer (popups)
// 1000-1200 - Tertiary menus / dropdowns
// 1200-9000 - Reserved for frequency manager
//
//
// Combines layout and vendor styles
//

View File

@ -154,6 +154,11 @@ class RelationController extends ControllerBehavior
*/
protected $manageId;
/**
* @var int Foeign id of a selected pivot record.
*/
protected $foreignId;
/**
* @var string Active session key, used for deferred bindings.
*/
@ -187,6 +192,69 @@ class RelationController extends ControllerBehavior
$this->config = $this->originalConfig = $this->makeConfig($controller->relationConfig, $this->requiredConfig);
}
/**
* Validates the supplied field and initializes the relation manager.
* @param string $field The relationship field.
* @return string The active field name.
*/
protected function validateField($field = null)
{
$field = $field ?: post(self::PARAM_FIELD);
if ($field && $field != $this->field) {
$this->initRelation($this->model, $field);
}
if (!$field && !$this->field) {
throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field')));
}
return $field ?: $this->field;
}
/**
* Prepares the view data.
* @return void
*/
public function prepareVars()
{
$this->vars['relationManageId'] = $this->manageId;
$this->vars['relationLabel'] = $this->config->label ?: $this->field;
$this->vars['relationField'] = $this->field;
$this->vars['relationType'] = $this->relationType;
$this->vars['relationSearchWidget'] = $this->searchWidget;
$this->vars['relationToolbarWidget'] = $this->toolbarWidget;
$this->vars['relationManageMode'] = $this->manageMode;
$this->vars['relationManageWidget'] = $this->manageWidget;
$this->vars['relationToolbarButtons'] = $this->toolbarButtons;
$this->vars['relationViewMode'] = $this->viewMode;
$this->vars['relationViewWidget'] = $this->viewWidget;
$this->vars['relationViewModel'] = $this->viewModel;
$this->vars['relationPivotWidget'] = $this->pivotWidget;
$this->vars['relationSessionKey'] = $this->relationGetSessionKey();
}
/**
* The controller action is responsible for supplying the parent model
* so it's action must be fired. Additionally, each AJAX request must
* supply the relation's field name (_relation_field).
*/
protected function beforeAjax()
{
if ($this->initialized) {
return;
}
$this->controller->pageAction();
$this->validateField();
$this->prepareVars();
$this->initialized = true;
}
//
// Interface
//
/**
* Prepare the widgets used by this behavior
* @param Model $model
@ -242,6 +310,7 @@ class RelationController extends ControllerBehavior
$this->viewMode = $this->evalViewMode();
$this->manageMode = $this->evalManageMode();
$this->manageId = post('manage_id');
$this->foreignId = post('foreign_id');
/*
* Toolbar widget
@ -354,65 +423,6 @@ class RelationController extends ControllerBehavior
return $this->relationRender($field, ['section' => 'view']);
}
/**
* Validates the supplied field and initializes the relation manager.
* @param string $field The relationship field.
* @return string The active field name.
*/
protected function validateField($field = null)
{
$field = $field ?: post(self::PARAM_FIELD);
if ($field && $field != $this->field) {
$this->initRelation($this->model, $field);
}
if (!$field && !$this->field) {
throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field')));
}
return $field ?: $this->field;
}
/**
* Prepares the view data.
* @return void
*/
public function prepareVars()
{
$this->vars['relationManageId'] = $this->manageId;
$this->vars['relationLabel'] = $this->config->label ?: $this->field;
$this->vars['relationField'] = $this->field;
$this->vars['relationType'] = $this->relationType;
$this->vars['relationSearchWidget'] = $this->searchWidget;
$this->vars['relationToolbarWidget'] = $this->toolbarWidget;
$this->vars['relationManageMode'] = $this->manageMode;
$this->vars['relationManageWidget'] = $this->manageWidget;
$this->vars['relationToolbarButtons'] = $this->toolbarButtons;
$this->vars['relationViewMode'] = $this->viewMode;
$this->vars['relationViewWidget'] = $this->viewWidget;
$this->vars['relationViewModel'] = $this->viewModel;
$this->vars['relationPivotWidget'] = $this->pivotWidget;
$this->vars['relationSessionKey'] = $this->relationGetSessionKey();
}
/**
* The controller action is responsible for supplying the parent model
* so it's action must be fired. Additionally, each AJAX request must
* supply the relation's field name (_relation_field).
*/
protected function beforeAjax()
{
if ($this->initialized) {
return;
}
$this->controller->pageAction();
$this->validateField();
$this->prepareVars();
$this->initialized = true;
}
/**
* Controller accessor for making partials within this behavior.
* @param string $partial
@ -449,39 +459,319 @@ class RelationController extends ControllerBehavior
}
/**
* Returns the existing record IDs for the relation.
* Returns the active session key.
*/
protected function findExistingRelationIds($checkIds = null)
public function relationGetSessionKey($force = false)
{
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$results = $this->relationObject
->getBaseQuery()
->select($foreignKeyName);
if ($checkIds !== null && is_array($checkIds) && count($checkIds)) {
$results = $results->whereIn($foreignKeyName, $checkIds);
if ($this->sessionKey && !$force) {
return $this->sessionKey;
}
return $results->lists($foreignKeyName);
if (post('_relation_session_key')) {
return $this->sessionKey = post('_relation_session_key');
}
if (post('_session_key')) {
return $this->sessionKey = post('_session_key');
}
return $this->sessionKey = FormHelper::getSessionKey();
}
//
// Overrides
// Widgets
//
/**
* Controller override: Extend the query used for populating the list
* after the default query is processed.
* @param October\Rain\Database\Builder $query
* @param string $field
*/
public function relationExtendQuery($query, $field)
protected function makeSearchWidget()
{
$config = $this->makeConfig();
$config->alias = $this->alias . 'ManageSearch';
$config->growable = false;
$config->prompt = 'backend::lang.list.search_prompt';
$widget = $this->makeWidget('Backend\Widgets\Search', $config);
$widget->cssClasses[] = 'recordfinder-search';
return $widget;
}
public function relationExtendRefreshResults($field)
protected function makeToolbarWidget()
{
$defaultConfig = [];
/*
* Add buttons to toolbar
*/
$defaultButtons = null;
if (!$this->readOnly) {
$defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.htm';
}
$defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons);
/*
* Make config
*/
$toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig));
$toolbarConfig->alias = $this->alias . 'Toolbar';
/*
* Add search to toolbar
*/
$useSearch = $this->viewMode == 'multi' && $this->getConfig('view[showSearch]');
if ($useSearch) {
$toolbarConfig->search = [
'prompt' => 'backend::lang.list.search_prompt'
];
}
/*
* No buttons, no search should mean no toolbar
*/
if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons))
return;
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
$toolbarWidget->cssClasses[] = 'list-header';
return $toolbarWidget;
}
protected function makeViewWidget()
{
/*
* Multiple (has many, belongs to many)
*/
if ($this->viewMode == 'multi') {
$config = $this->makeConfigForMode('view', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ViewList';
$config->showSorting = $this->getConfig('view[showSorting]', true);
$config->defaultSort = $this->getConfig('view[defaultSort]');
$config->recordsPerPage = $this->getConfig('view[recordsPerPage]');
$config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly);
$config->recordUrl = $this->getConfig('view[recordUrl]', null);
$defaultOnClick = sprintf(
"$.oc.relationBehavior.clickViewListRecord(':id', '%s', '%s')",
$this->field,
$this->relationGetSessionKey()
);
if ($config->recordUrl) {
$defaultOnClick = null;
}
elseif (
!$this->makeConfigForMode('manage', 'form', false) &&
!$this->makeConfigForMode('pivot', 'form', false)
) {
$defaultOnClick = null;
}
$config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick);
if ($emptyMessage = $this->getConfig('emptyMessage')) {
$config->noRecordsMessage = $emptyMessage;
}
/*
* Constrain the query by the relationship and deferred items
*/
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
$widget->bindEvent('list.extendQuery', function ($query) {
$this->controller->relationExtendQuery($query, $this->field);
$this->relationObject->setQuery($query);
if ($sessionKey = $this->relationGetSessionKey()) {
$this->relationObject->withDeferred($sessionKey);
}
elseif ($this->model->exists) {
$this->relationObject->addConstraints();
}
/*
* Allows pivot data to enter the fray
*/
if ($this->relationType == 'belongsToMany') {
$this->relationObject->setQuery($query->getQuery());
return $this->relationObject;
}
});
/*
* Constrain the list by the search widget, if available
*/
if ($this->toolbarWidget && $this->getConfig('view[showSearch]')) {
if ($searchWidget = $this->toolbarWidget->getSearchWidget()) {
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
$widget->setSearchTerm($searchWidget->getActiveTerm());
return $widget->onRefresh();
});
$searchWidget->setActiveTerm(null);
}
}
}
/*
* Single (belongs to, has one)
*/
elseif ($this->viewMode == 'single') {
$query = $this->relationObject;
$this->controller->relationExtendQuery($query, $this->field);
$this->viewModel = $query->getResults() ?: $this->relationModel;
$config = $this->makeConfigForMode('view', 'form');
$config->model = $this->viewModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = 'relation';
$config->alias = $this->alias . 'ViewForm';
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
$widget->previewMode = true;
}
return $widget;
}
protected function makeManageWidget()
{
$widget = null;
/*
* List / Pivot
*/
if ($this->manageMode == 'list' || $this->manageMode == 'pivot') {
$isPivot = $this->manageMode == 'pivot';
$config = $this->makeConfigForMode('manage', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ManageList';
$config->showSetup = false;
$config->showCheckboxes = $this->getConfig('manage[showCheckboxes]', !$isPivot);
$config->showSorting = $this->getConfig('manage[showSorting]', !$isPivot);
$config->defaultSort = $this->getConfig('manage[defaultSort]');
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
if ($this->viewMode == 'single') {
$config->showCheckboxes = false;
$config->recordOnClick = sprintf(
"$.oc.relationBehavior.clickManageListRecord(:id, '%s', '%s')",
$this->field,
$this->relationGetSessionKey()
);
}
elseif ($config->showCheckboxes) {
$config->recordOnClick = "$.oc.relationBehavior.toggleListCheckbox(this)";
}
elseif ($isPivot) {
$config->recordOnClick = sprintf(
"$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')",
$this->field,
$this->relationGetSessionKey()
);
}
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
/*
* Link the Search Widget to the List Widget
*/
if ($this->getConfig('manage[showSearch]')) {
$this->searchWidget = $this->makeSearchWidget();
$this->searchWidget->bindToController();
$this->searchWidget->bindEvent('search.submit', function () use ($widget) {
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
return $widget->onRefresh();
});
$this->searchWidget->setActiveTerm(null);
}
}
/*
* Form
*/
elseif ($this->manageMode == 'form') {
$config = $this->makeConfigForMode('manage', 'form');
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = $this->evalFormContext('manage', !!$this->manageId);
$config->alias = $this->alias . 'ManageForm';
/*
* Existing record
*/
if ($this->manageId) {
$config->model = $config->model->find($this->manageId);
if (!$config->model) {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($config->model), 'id' => $this->manageId
]));
}
}
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
}
if (!$widget) {
return null;
}
/*
* Exclude existing relationships
*/
if ($this->manageMode == 'pivot' || $this->manageMode == 'list') {
$widget->bindEvent('list.extendQuery', function ($query) {
$this->controller->relationExtendQuery($query, $this->field);
/*
* Where not in the current list of related records
*/
$existingIds = $this->findExistingRelationIds();
if (count($existingIds)) {
$query->whereNotIn($this->relationModel->getQualifiedKeyName(), $existingIds);
}
});
}
return $widget;
}
protected function makePivotWidget()
{
$config = $this->makeConfigForMode('pivot', 'form');
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = $this->evalFormContext('pivot', !!$this->manageId);
$config->alias = $this->alias . 'ManagePivotForm';
/*
* Existing record
*/
if ($this->manageId) {
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
$config->model = $hydratedModel;
if (!$config->model) {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($config->model), 'id' => $this->manageId
]));
}
}
else {
if ($this->foreignId && ($foreignModel = $this->relationModel->find($this->foreignId))) {
$foreignModel->exists = false;
$config->model = $foreignModel;
}
$pivotModel = $this->relationObject->newPivot();
$config->model->setRelation('pivot', $pivotModel);
}
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
return $widget;
}
//
@ -775,11 +1065,19 @@ class RelationController extends ControllerBehavior
return $this->relationRefresh();
}
/**
* Add multiple items using a single pivot form.
*/
public function onRelationManageAddPivot()
{
return $this->onRelationManagePivotForm();
}
public function onRelationManagePivotForm()
{
$this->beforeAjax();
$this->vars['foreignId'] = post('foreign_id');
$this->vars['foreignId'] = $this->foreignId ?: post('checked');
return $this->relationMakePartial('pivot_form');
}
@ -787,18 +1085,24 @@ class RelationController extends ControllerBehavior
{
$this->beforeAjax();
$foreignId = post('foreign_id');
$foreignModel = $this->relationModel->find($foreignId);
$saveData = $this->pivotWidget->getSaveData();
/*
* Add the checked IDs to the pivot table
*/
$foreignIds = (array) $this->foreignId;
$this->relationObject->sync($foreignIds, false);
/*
* Check for existing relation
* Save data to models
*/
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$existing = $this->relationObject->where($foreignKeyName, $foreignId)->count();
$hyrdatedModels = $this->relationObject->whereIn($foreignKeyName, $foreignIds)->get();
$saveData = $this->pivotWidget->getSaveData();
if (!$existing) {
$this->relationObject->add($foreignModel, null, $saveData);
foreach ($hyrdatedModels as $hydratedModel) {
$modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData);
foreach ($modelsToSave as $modelToSave) {
$modelToSave->save();
}
}
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
@ -821,339 +1125,44 @@ class RelationController extends ControllerBehavior
}
//
// Widgets
// Overrides
//
protected function makeSearchWidget()
{
$config = $this->makeConfig();
$config->alias = $this->alias . 'ManageSearch';
$config->growable = false;
$config->prompt = 'backend::lang.list.search_prompt';
$widget = $this->makeWidget('Backend\Widgets\Search', $config);
$widget->cssClasses[] = 'recordfinder-search';
return $widget;
}
protected function makeToolbarWidget()
{
$defaultConfig = [];
/*
* Add buttons to toolbar
*/
$defaultButtons = null;
if (!$this->readOnly) {
$defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.htm';
}
$defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons);
/*
* Make config
*/
$toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig));
$toolbarConfig->alias = $this->alias . 'Toolbar';
/*
* Add search to toolbar
*/
$useSearch = $this->viewMode == 'multi' && $this->getConfig('view[showSearch]');
if ($useSearch) {
$toolbarConfig->search = [
'prompt' => 'backend::lang.list.search_prompt'
];
}
/*
* No buttons, no search should mean no toolbar
*/
if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons))
return;
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
$toolbarWidget->cssClasses[] = 'list-header';
return $toolbarWidget;
}
protected function makeViewWidget()
{
/*
* Multiple (has many, belongs to many)
*/
if ($this->viewMode == 'multi') {
$config = $this->makeConfigForMode('view', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ViewList';
$config->showSorting = $this->getConfig('view[showSorting]', true);
$config->defaultSort = $this->getConfig('view[defaultSort]');
$config->recordsPerPage = $this->getConfig('view[recordsPerPage]');
$config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly);
$defaultOnClick = sprintf(
"$.oc.relationBehavior.clickViewListRecord(':id', '%s', '%s')",
$this->field,
$this->relationGetSessionKey()
);
$config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick);
$config->recordUrl = $this->getConfig('view[recordUrl]', null);
if ($emptyMessage = $this->getConfig('emptyMessage')) {
$config->noRecordsMessage = $emptyMessage;
}
/*
* Constrain the query by the relationship and deferred items
*/
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
$widget->bindEvent('list.extendQuery', function ($query) {
$this->controller->relationExtendQuery($query, $this->field);
$this->relationObject->setQuery($query);
if ($sessionKey = $this->relationGetSessionKey()) {
$this->relationObject->withDeferred($sessionKey);
}
elseif ($this->model->exists) {
$this->relationObject->addConstraints();
}
/*
* Allows pivot data to enter the fray
*/
if ($this->relationType == 'belongsToMany') {
$this->relationObject->setQuery($query->getQuery());
return $this->relationObject;
}
});
/*
* Constrain the list by the search widget, if available
*/
if ($this->toolbarWidget && $this->getConfig('view[showSearch]')) {
if ($searchWidget = $this->toolbarWidget->getSearchWidget()) {
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
$widget->setSearchTerm($searchWidget->getActiveTerm());
return $widget->onRefresh();
});
$searchWidget->setActiveTerm(null);
}
}
}
/*
* Single (belongs to, has one)
*/
elseif ($this->viewMode == 'single') {
$query = $this->relationObject;
$this->controller->relationExtendQuery($query, $this->field);
$this->viewModel = $query->getResults() ?: $this->relationModel;
$config = $this->makeConfigForMode('view', 'form');
$config->model = $this->viewModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = 'relation';
$config->alias = $this->alias . 'ViewForm';
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
$widget->previewMode = true;
}
return $widget;
}
protected function makeManageWidget()
{
$widget = null;
/*
* Pivot
*/
if ($this->manageMode == 'pivot' && isset($this->config->pivot)) {
$config = $this->makeConfigForMode('manage', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ManagePivotList';
$config->showSetup = false;
$config->showSorting = $this->getConfig('manage[showSorting]', false);
$config->defaultSort = $this->getConfig('manage[defaultSort]');
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
$config->recordOnClick = sprintf(
"$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')",
$this->field,
$this->relationGetSessionKey()
);
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
/*
* Link the Search Widget to the List Widget
*/
if ($this->getConfig('pivot[showSearch]')) {
$this->searchWidget = $this->makeSearchWidget();
$this->searchWidget->bindToController();
$this->searchWidget->bindEvent('search.submit', function () use ($widget) {
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
return $widget->onRefresh();
});
$this->searchWidget->setActiveTerm(null);
}
}
/*
* List
*/
elseif ($this->manageMode == 'list' && isset($this->config->list)) {
$config = $this->makeConfigForMode('manage', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ManageList';
$config->showSetup = false;
$config->showCheckboxes = true;
$config->showSorting = $this->getConfig('manage[showSorting]', true);
$config->defaultSort = $this->getConfig('manage[defaultSort]');
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
if ($this->viewMode == 'single') {
$config->showCheckboxes = false;
$config->recordOnClick = sprintf(
"$.oc.relationBehavior.clickManageListRecord(:id, '%s', '%s')",
$this->field,
$this->relationGetSessionKey()
);
}
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
/*
* Link the Search Widget to the List Widget
*/
if ($this->getConfig('manage[showSearch]')) {
$this->searchWidget = $this->makeSearchWidget();
$this->searchWidget->bindToController();
$this->searchWidget->bindEvent('search.submit', function () use ($widget) {
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
return $widget->onRefresh();
});
$this->searchWidget->setActiveTerm(null);
}
}
/*
* Form
*/
elseif ($this->manageMode == 'form' && isset($this->config->form)) {
/*
* Determine supplied form context
*/
$manageConfig = isset($this->config->manage) ? $this->config->manage : [];
if ($context = array_get($manageConfig, 'context')) {
if (is_array($context)) {
$context = $this->manageId
? array_get($context, 'update')
: array_get($context, 'create');
}
}
$config = $this->makeConfigForMode('manage', 'form');
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = $context ?: 'relation';
$config->alias = $this->alias . 'ManageForm';
/*
* Existing record
*/
if ($this->manageId) {
$config->model = $config->model->find($this->manageId);
if (!$config->model) {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($config->model), 'id' => $this->manageId
]));
}
}
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
}
if (!$widget) {
return null;
}
/*
* Exclude existing relationships
*/
if ($this->manageMode == 'pivot' || $this->manageMode == 'list') {
$widget->bindEvent('list.extendQuery', function ($query) {
$this->controller->relationExtendQuery($query, $this->field);
/*
* Where not in the current list of related records
*/
$existingIds = $this->findExistingRelationIds();
if (count($existingIds)) {
$query->whereNotIn($this->relationModel->getQualifiedKeyName(), $existingIds);
}
});
}
return $widget;
}
protected function makePivotWidget()
{
$config = $this->makeConfigForMode('pivot', 'form');
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = 'relation';
$config->alias = $this->alias . 'ManagePivotForm';
/*
* Existing record
*/
if ($this->manageId) {
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
$config->model = $hydratedModel;
if (!$config->model) {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($config->model), 'id' => $this->manageId
]));
}
}
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
return $widget;
}
/**
* Returns the active session key.
* Controller override: Extend the query used for populating the list
* after the default query is processed.
* @param October\Rain\Database\Builder $query
* @param string $field
*/
public function relationGetSessionKey($force = false)
public function relationExtendQuery($query, $field)
{
if ($this->sessionKey && !$force) {
return $this->sessionKey;
}
}
if (post('_relation_session_key')) {
return $this->sessionKey = post('_relation_session_key');
}
if (post('_session_key')) {
return $this->sessionKey = post('_session_key');
}
return $this->sessionKey = FormHelper::getSessionKey();
public function relationExtendRefreshResults($field)
{
}
//
// Helpers
//
/**
* Returns the existing record IDs for the relation.
*/
protected function findExistingRelationIds($checkIds = null)
{
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$results = $this->relationObject
->getBaseQuery()
->select($foreignKeyName);
if ($checkIds !== null && is_array($checkIds) && count($checkIds)) {
$results = $results->whereIn($foreignKeyName, $checkIds);
}
return $results->lists($foreignKeyName);
}
/**
* Determine the default buttons based on the model relationship type.
@ -1238,11 +1247,33 @@ class RelationController extends ControllerBehavior
}
}
/**
* Determine supplied form context.
*/
protected function evalFormContext($mode = 'manage', $exists = false)
{
$config = isset($this->config->{$mode}) ? $this->config->{$mode} : [];
if ($context = array_get($config, 'context')) {
if (is_array($context)) {
$context = $exists
? array_get($context, 'update')
: array_get($context, 'create');
}
}
if (!$context) {
$context = $exists ? 'update' : 'create';
}
return $context;
}
/**
* Returns the configuration for a mode (view, manage, pivot) for an
* expected type (list, form). Uses fallback configuration.
*/
protected function makeConfigForMode($mode = 'view', $type = 'list')
protected function makeConfigForMode($mode = 'view', $type = 'list', $throwException = true)
{
$config = null;
@ -1268,12 +1299,16 @@ class RelationController extends ControllerBehavior
* - view.list => manage.list
*/
if (!$config) {
if ($mode == 'manage' && $type == 'list') {
return $this->makeConfigForMode('view', $type);
}
throw new ApplicationException('Missing configuration for '.$mode.'.'.$type.' in RelationController definition '.$this->field);
if ($throwException) {
throw new ApplicationException('Missing configuration for '.$mode.'.'.$type.' in RelationController definition '.$this->field);
}
else {
return false;
}
}
return $this->makeConfig($config);

View File

@ -5,18 +5,8 @@
var RelationBehavior = function() {
this.clickManageListRecord = function(recordId, relationField, sessionKey) {
var oldPopup = $('#relationManagePopup')
$.request('onRelationClickManageList', {
data: {
'record_id': recordId,
'_relation_field': relationField,
'_session_key': sessionKey
}
})
oldPopup.popup('hide')
this.toggleListCheckbox = function(el) {
$(el).closest('.control-list').listWidget('toggleChecked', [el])
}
this.clickViewListRecord = function(recordId, relationField, sessionKey) {
@ -33,6 +23,20 @@
})
}
this.clickManageListRecord = function(recordId, relationField, sessionKey) {
var oldPopup = $('#relationManagePopup')
$.request('onRelationClickManageList', {
data: {
'record_id': recordId,
'_relation_field': relationField,
'_session_key': sessionKey
}
})
oldPopup.popup('hide')
}
this.clickManagePivotListRecord = function(foreignId, relationField, sessionKey) {
var oldPopup = $('#relationManagePivotPopup'),
newPopup = $('<a />')

View File

@ -17,6 +17,18 @@
</div>
<div class="modal-footer">
<?php if ($relationManageWidget->showCheckboxes): ?>
<button
type="button"
class="btn btn-primary"
data-control="popup"
data-handler="onRelationManageAddPivot"
data-size="huge"
data-dismiss="popup"
data-stripe-load-indicator>
<?= e(trans('backend::lang.relation.add_selected')) ?>
</button>
<?php endif ?>
<button
type="button"
class="btn btn-default"

View File

@ -1,7 +1,7 @@
<?php namespace Backend\Classes;
use Html;
use Model;
use October\Rain\Database\Model;
use October\Rain\Html\Helper as HtmlHelper;
/**
@ -49,6 +49,16 @@ class FormField
*/
public $valueFrom;
/**
* @var string Specifies a default value for supported fields.
*/
public $defaults;
/**
* @var string Model attribute to use for the default value.
*/
public $defaultFrom;
/**
* @var string Specifies if this field belongs to a tab.
*/
@ -114,11 +124,6 @@ class FormField
*/
public $commentHtml = false;
/**
* @var string Specifies a default value for supported fields.
*/
public $defaults;
/**
* @var string Specifies a message to display when there is no value supplied (placeholder).
*/
@ -296,6 +301,9 @@ class FormField
if (isset($config['default'])) {
$this->defaults = $config['default'];
}
if (isset($config['defaultFrom'])) {
$this->defaultFrom = $config['defaultFrom'];
}
if (isset($config['attributes'])) {
$this->attributes($config['attributes']);
}
@ -487,6 +495,8 @@ class FormField
/**
* Returns a value suitable for the field id property.
* @param string $suffix Specify a suffix string
* @return string
*/
public function getId($suffix = null)
{
@ -516,7 +526,7 @@ class FormField
*/
public function getValueFromData($data, $default = null)
{
$fieldName = $this->fieldName;
$fieldName = $this->valueFrom ?: $this->fieldName;
/*
* Array field name, eg: field[key][key2][key3]

View File

@ -1,5 +1,7 @@
<?php namespace Backend\Classes;
use October\Rain\Html\Helper as HtmlHelper;
/**
* List Columns definition
* A translation of the list column configuration
@ -147,4 +149,31 @@ class ListColumn
return $config;
}
/**
* Returns a HTML valid name for the column name.
* @return string
*/
public function getName()
{
return HtmlHelper::nameToId($this->columnName);
}
/**
* Returns a value suitable for the column id property.
* @param string $suffix Specify a suffix string
* @return string
*/
public function getId($suffix = null)
{
$id = 'column';
$id .= '-'.$this->columnName;
if ($suffix) {
$id .= '-'.$suffix;
}
return HtmlHelper::nameToId($id);
}
}

View File

@ -25,7 +25,7 @@ class AccessLogs extends Controller
'Backend.Behaviors.ListController'
];
public $requiredPermissions = ['system.access_access_logs'];
public $requiredPermissions = ['system.access_logs'];
public $listConfig = 'config_list.yaml';

View File

@ -1,4 +1,4 @@
<div class="padded-container list-header">
<div class="padded-container container-flush">
<?= $this->makeHintPartial('backend_accesslogs_hint', 'hint') ?>
</div>

View File

@ -12,12 +12,12 @@ class DbBackendUsers extends Migration
$table->increments('id');
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->string('login')->unique()->index();
$table->string('email')->unique();
$table->string('login')->unique('login_unique')->index('login_index');
$table->string('email')->unique('email_unique');
$table->string('password');
$table->string('activation_code')->nullable()->index();
$table->string('activation_code')->nullable()->index('act_code_index');
$table->string('persist_code')->nullable();
$table->string('reset_password_code')->nullable()->index();
$table->string('reset_password_code')->nullable()->index('reset_code_index');
$table->text('permissions')->nullable();
$table->boolean('is_activated')->default(0);
$table->timestamp('activated_at')->nullable();

View File

@ -10,7 +10,7 @@ class DbBackendUserGroups extends Migration
Schema::create('backend_user_groups', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->unique();
$table->string('name')->unique('name_unique');
$table->text('permissions')->nullable();
$table->timestamps();
});

View File

@ -11,7 +11,7 @@ class DbBackendUsersGroups extends Migration
$table->engine = 'InnoDB';
$table->integer('user_id')->unsigned();
$table->integer('user_group_id')->unsigned();
$table->primary(array('user_id', 'user_group_id'));
$table->primary(['user_id', 'user_group_id'], 'user_group');
});
}

View File

@ -8,7 +8,7 @@ class DbBackendAddDescriptionField extends Migration
public function up()
{
Schema::table('backend_user_groups', function (Blueprint $table) {
$table->string('code')->nullable()->index();
$table->string('code')->nullable()->index('code_index');
$table->text('description')->nullable();
$table->boolean('is_new_user_default')->default(false);
});
@ -16,10 +16,10 @@ class DbBackendAddDescriptionField extends Migration
public function down()
{
Schema::table('backend_user_groups', function (Blueprint $table) {
$table->dropColumn('code');
$table->dropColumn('description');
$table->dropColumn('is_new_user_default');
});
// Schema::table('backend_user_groups', function (Blueprint $table) {
// $table->dropColumn('code');
// $table->dropColumn('description');
// $table->dropColumn('is_new_user_default');
// });
}
}

View File

@ -14,15 +14,29 @@ use ApplicationException;
*/
class DataTable extends FormWidgetBase
{
/**
* {@inheritDoc}
*/
protected $defaultAlias = 'datatable';
//
// Configurable properties
//
/**
* @var string Table size
*/
protected $size = 'large';
public $size = 'large';
/**
* @var bool Allow rows to be sorted
* @todo Not implemented...
*/
public $rowSorting = false;
//
// Object properties
//
/**
* {@inheritDoc}
*/
protected $defaultAlias = 'datatable';
/**
* @var Backend\Widgets\Table Table widget
@ -34,7 +48,11 @@ class DataTable extends FormWidgetBase
*/
public function init()
{
$this->size = $this->getConfig('size', $this->size);
$this->fillFromConfig([
'size',
'rowSorting',
]);
$this->table = $this->makeTableWidget();
$this->table->bindToController();
}
@ -64,6 +82,23 @@ class DataTable extends FormWidgetBase
$this->populateTableWidget();
$this->vars['table'] = $this->table;
$this->vars['size'] = $this->size;
$this->vars['rowSorting'] = $this->rowSorting;
}
/**
* {@inheritDoc}
*/
public function getLoadValue()
{
$value = (array) parent::getLoadValue();
// Sync the array keys as the ID to make the
// table widget happy!
foreach ($value as $key => $_value) {
$value[$key] = ['id' => $key] + (array) $_value;
}
return $value;
}
/**
@ -79,7 +114,13 @@ class DataTable extends FormWidgetBase
$result = [];
while ($records = $dataSource->readRecords()) {
$result += $records;
$result = array_merge($result, $records);
}
// We should be dealing with a simple array, so
// strip out the id columns in the final array.
foreach ($result as $key => $_result) {
unset($result[$key]['id']);
}
return $result;
@ -97,6 +138,8 @@ class DataTable extends FormWidgetBase
// all records at once. -ab
$records = $this->getLoadValue() ?: [];
$dataSource->purge();
$dataSource->initRecords((array) $records);
}

View File

@ -9,6 +9,7 @@ use Backend\Classes\FormField;
use Backend\Classes\FormWidgetBase;
use ValidationException;
use Exception;
use Lang;
/**
* File upload field

View File

@ -101,11 +101,11 @@ class RecordFinder extends FormWidgetBase
}
/**
* Returns the value as a relation object from the model,
* Returns the model of a relation type,
* supports nesting via HTML array.
* @return Relation
*/
protected function getRelationObject()
protected function getRelationModel()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
@ -116,17 +116,6 @@ class RecordFinder extends FormWidgetBase
]));
}
return $model->{$attribute}();
}
/**
* Returns the model of a relation type,
* supports nesting via HTML array.
* @return Relation
*/
protected function getRelationModel()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
return $model->makeRelation($attribute);
}

View File

@ -2,6 +2,7 @@
use Lang;
use Backend\Classes\FormWidgetBase;
use ApplicationException;
use SystemException;
use Illuminate\Database\Eloquent\Relations\Relation as RelationBase;

View File

@ -11,7 +11,7 @@
aria-label="Remove"
data-request="<?= $this->getEventHandler('onRemoveItem') ?>"
data-request-data="'index': '<?= $indexValue ?>'"
data-request-success="$el.closest('.field-repeater-item').remove()"
data-request-success="$(this).closest('.field-repeater-item').remove()"
data-request-confirm="Are you sure?">
<span aria-hidden="true">&times;</span>
</button>

View File

@ -6,18 +6,18 @@ return [
],
'field' => [
'invalid_type' => 'Использован неверный тип поля: :type.',
'options_method_not_exists' => 'Класс модели :model должен содержать метод :method(), возвращающий опции для поля формы ":field".',
'options_method_not_exists' => "Класс модели :model должен содержать метод :method(), возвращающий опции для поля формы ':field'.",
],
'widget' => [
'not_registered' => "Класс виджета ':name' не зарегистрирован.",
'not_bound' => "Виджет с именем класса ':name' не связан с контроллером.",
],
'page' => [
'untitled' => "Без названия",
'untitled' => 'Без названия',
'access_denied' => [
'label' => "Доступ запрещен",
'label' => 'Доступ запрещен',
'help' => "У вас нет необходимых прав для просмотра этой страницы.",
'cms_link' => "Перейти к CMS",
'cms_link' => 'Перейти к CMS',
],
],
'partial' => [
@ -30,17 +30,17 @@ return [
'restore' => 'Восстановить',
'login_placeholder' => 'пользователь',
'password_placeholder' => 'пароль',
'forgot_password' => "Забыли пароль?",
'forgot_password' => 'Забыли пароль?',
'enter_email' => 'Введите вашу почту',
'enter_login' => "Введите ваш Логин",
'email_placeholder' => "почта",
'enter_login' => 'Введите ваш Логин',
'email_placeholder' => 'почта',
'enter_new_password' => 'Введите новый пароль',
'password_reset' => "Сбросить пароль",
'restore_success' => "На вашу электронную почту отправлено сообщение с инструкциями для восстановления пароля.",
'password_reset' => 'Сбросить пароль',
'restore_success' => 'На вашу электронную почту отправлено сообщение с инструкциями для восстановления пароля.',
'restore_error' => "Пользователь с логином ':login' не найден.",
'reset_success' => "Ваш пароль был успешно изменен. Теперь вы можете войти на сайт.",
'reset_error' => "Недействительные данные для изменения пароля. Пожалуйста, попробуйте еще раз!",
'reset_fail' => "Невозможно изменить пароль!",
'reset_success' => 'Ваш пароль был успешно изменен. Теперь вы можете войти на сайт.',
'reset_error' => 'Недействительные данные для изменения пароля. Пожалуйста, попробуйте еще раз!',
'reset_fail' => 'Невозможно изменить пароль!',
'apply' => 'Применить',
'cancel' => 'Отменить',
'delete' => 'Удалить',
@ -75,18 +75,19 @@ return [
'menu_description' => 'Управление группой администраторов, создание групп и разрешений.',
'list_title' => 'Управление администраторами',
'new' => 'Добавить администратора',
'login' => "Логин",
'first_name' => "Имя",
'last_name' => "Фамилия",
'full_name' => "Полное имя",
'email' => "Почта",
'groups' => "Группы",
'groups_comment' => "Укажите к какой группе принадлежит этот пользователь.",
'avatar' => "Аватар",
'password' => "Пароль",
'password_confirmation' => "Подтверждение пароля",
'superuser' => "Суперпользователь",
'superuser_comment' => "Установите этот флажок, чтобы позволить пользователю получать доступ ко всем областям.",
'login' => 'Логин',
'first_name' => 'Имя',
'last_name' => 'Фамилия',
'full_name' => 'Полное имя',
'email' => 'Почта',
'groups' => 'Группы',
'groups_comment' => 'Укажите к какой группе принадлежит этот пользователь.',
'avatar' => 'Аватар',
'password' => 'Пароль',
'password_confirmation' => 'Подтверждение пароля',
'permissions' => 'Полномочия',
'superuser' => 'Суперпользователь',
'superuser_comment' => 'Установите этот флажок, чтобы позволить пользователю получать доступ ко всем областям.',
'send_invite' => 'Отправить приглашение по электронной почте',
'send_invite_comment' => 'Используйте эту опцию, чтобы отправить приглашение пользователю по электронной почте',
'delete_confirm' => 'Вы действительно хотите удалить этого администратора?',
@ -97,6 +98,10 @@ return [
'group' => [
'name' => 'Группы',
'name_field' => 'Название',
'description_field' => 'Описание',
'is_new_user_default_field' => 'Добавлять новых администраторов в эту группу по умолчанию',
'code_field' => 'Уникальный код',
'code_comment' => 'Введите уникальный код, если вы хотите открыть доступ к нему с помощью API.',
'menu_label' => 'Группы',
'list_title' => 'Управление группами',
'new' => 'Добавить группу',
@ -124,7 +129,11 @@ return [
'setup_title' => 'Настройка списка',
'setup_help' => 'Используйте флажки для выбора колонок, которые вы хотите видеть в списке. Вы можете изменить положение столбцов, перетаскивая их вверх или вниз.',
'records_per_page' => 'Записей на странице',
'records_per_page_help' => 'Выберите количество записей на странице для отображения. Обратите внимание, что большое количество записей на одной странице может привести к снижению производительности.'
'records_per_page_help' => 'Выберите количество записей на странице для отображения. Обратите внимание, что большое количество записей на одной странице может привести к снижению производительности.',
'delete_selected' => 'Удалить выбранное',
'delete_selected_empty' => 'Нет выбранных записей для удаления.',
'delete_selected_confirm' => 'Удалить выбранные записи?',
'delete_selected_success' => 'Выбранные записи успешно удалены.',
],
'fileupload' => [
'attachment' => 'Приложение',
@ -133,17 +142,17 @@ return [
'description_label' => 'Описание'
],
'form' => [
'create_title' => "Создание :name",
'update_title' => "Редактирование :name",
'preview_title' => "Предпросмотр :name",
'create_title' => 'Создание :name',
'update_title' => 'Редактирование :name',
'preview_title' => 'Предпросмотр :name',
'create_success' => ':name был успешно создан',
'update_success' => ':name был успешно сохранен',
'delete_success' => ':name был успешно удален',
'missing_id' => "Идентификатор формы записи не указан.",
'missing_id' => 'Идентификатор формы записи не указан.',
'missing_model' => 'Для формы используемой в :class не определена модель.',
'missing_definition' => "Поведение формы не содержит поле для':field'.",
'not_found' => 'Форма записи с идентификатором :ID не найдена.',
'action_confirm' => "Вы уверены, что хотите сделать это?",
'action_confirm' => 'Вы уверены, что хотите сделать это?',
'create' => 'Создать',
'create_and_close' => 'Создать и закрыть',
'creating' => 'Создание...',
@ -178,10 +187,11 @@ return [
'select_placeholder' => 'Пожалуйста, выберите',
'insert_row' => 'Вставить строку',
'delete_row' => 'Удалить строку',
'concurrency_file_changed_title' => "Файл был изменен",
'concurrency_file_changed_title' => 'Файл был изменен',
'concurrency_file_changed_description' => "Файл, который вы редактируете был изменен другим пользователем. Вы можете либо перезагрузить файл и потерять ваши изменения или перезаписать его",
],
'relation' => [
'missing_config' => "Поведение отношения не имеет конфигурации для ':config'.",
'missing_definition' => "Поведение отношения не содержит определения для ':field'.",
'missing_model' => "Для поведения отношения, используемого в :class не определена модель.",
'invalid_action_single' => "Это действие не может быть выполнено для особого отношения.",
@ -191,23 +201,34 @@ return [
'add' => "Добавить",
'add_selected' => "Добавить выбранные",
'add_a_new' => "Добавить новый :name",
'link_selected' => "Связать выбранное",
'link_a_new' => "Новая ссылка :name",
'cancel' => "Отмена",
'add_name' => "Добавить :name",
'close' => "Закрыть",
'add_name' => "Добавление :name",
'create' => "Создать",
'create_name' => "Создание :name",
'update' => "Update",
'update_name' => "Update :name",
'update' => "Обновить",
'update_name' => "Обновление :name",
'preview' => "Предпросмотр",
'preview_name' => "Предпросмотр :name",
'remove' => "Удалить",
'remove_name' => "Удаление :name",
'delete' => "Удалить",
'delete_name' => "Удаление :name",
'delete_confirm' => "Вы уверены?",
'link' => "Ссылка",
'link_name' => "Соединение :name",
'unlink' => "Отвязать",
'unlink_name' => "Разъединение :name",
'unlink_confirm' => "Вы уверены?",
],
'model' => [
'name' => "Модель",
'name' => 'Модель',
'not_found' => "Модель ':class' с идентификатором :id не найдена",
'missing_id' => "Нет идентификатора для поиска модели записи.",
'missing_id' => 'Нет идентификатора для поиска модели записи.',
'missing_relation' => "Модель ':class' не содержит определения для ':relation'",
'missing_method' => "Модель ':class' не содержит метод ':method'.",
'invalid_class' => "Модель :model используемая в :class не допустима, она должна наследовать класс \Model.",
'mass_assignment_failed' => "Массовое заполнение недоступно для атрибута модели ':attribute'.",
],

View File

@ -1,6 +1,9 @@
<?php
return [
'auth' => [
'title' => 'Administrations område'
],
'field' => [
'invalid_type' => 'Felaktig fälttyp använd :type.',
'options_method_not_exists' => 'Modelklassen :model måste definera en metod :method() som returnerar villkor för formfältet ":field"',
@ -45,10 +48,31 @@ return [
],
'dashboard' => [
'menu_label' => 'Kontrollpanelen',
'widget_label' => 'Widget',
'widget_width' => 'Bredd',
'full_width' => 'max bredd',
'add_widget' => 'Lägg till widget',
'widget_inspector_title' => 'Widget inställningar',
'widget_inspector_description' => 'Widget informations inställningar',
'widget_columns_label' => 'Bredd :columns',
'widget_columns_description' => 'Bredden på widgeten ska vara ett nummer mellan 1 och 10.',
'widget_columns_error' => 'Vänligen ange widgetens bredden som ett nummer mellan 1 och 10.',
'columns' => '{1} column|[2,Inf] kolonner',
'widget_new_row_label' => 'Forcera en ny rad',
'widget_new_row_description' => 'Lägg widgeten på en ny rad.',
'widget_title_label' => 'Widget titel',
'widget_title_error' => 'En widgets titel är tvingande.',
'status' => [
'widget_title_default' => 'System status',
'online' => 'online',
'maintenance' => 'i underhåll',
'update_available' => '{0} uppdateringar tillgängliga!|{1} uppdatering tillgänglig!|[2,Inf] uppdateringar tillgängliga!'
]
],
'user' => [
'name' => 'Administratör',
'menu_label' => 'Administratörer',
'menu_description' => 'Hantera administratörs användare, grupper och behörigheter.',
'list_title' => 'Hantera administratörer',
'new' => 'Ny Administratör',
'login' => "Användarnamn",
@ -61,18 +85,26 @@ return [
'avatar' => "Avatar",
'password' => "Lösenord",
'password_confirmation' => "Bekräfta lösenord",
'permissions' => 'Rättigheter',
'superuser' => "Superanvändare",
'superuser_comment' => "Markera denna checkbox för att ge denna person tillgång till alla områden",
'send_invite' => 'Inbjudan är sänd via e-post',
'send_invite_comment' => 'Markera denna checkbox för att skicka en inbjudan till användaren via e-post',
'delete_confirm' => 'Vill du verkligen radera denna administratör?',
'return' => 'Återgå till administratörlistan',
'allow' => 'Tillåt',
'inherit' => 'Ärv',
'deny' => 'Förbjud',
'group' => [
'name' => 'Grupp',
'name_field' => 'Namn',
'description_field' => 'Beskriving',
'is_new_user_default_field' => 'Lägg till nya administratörer till gruppen som standard',
'code_field' => 'Kod',
'code_comment' => 'Ange en unik kod om du vill komma åt det med API.',
'menu_label' => 'Grupper',
'list_title' => 'Hantera grupper',
'new' => 'New administratörgrupp',
'new' => 'Ny administratörsgrupp',
'delete_confirm' => 'Vill du verkligen radera denna administratörgrupp?',
'return' => 'Återgå till grupplistan',
],
@ -89,7 +121,25 @@ return [
'missing_columns' => 'Listan som används i :class har inga listkolumner definerade',
'missing_definition' => "Listegenskapen saknar en kolumn för ':field'",
'behavior_not_ready' => 'Listegenskapen har inte blivit initierad, kontrollera att du har anropat makeLists() i din controller',
'invalid_column_datetime' => "Column value ':column' is not a DateTime object, are you missing a \$dates reference in the Model?",
'invalid_column_datetime' => "Kolumns värde ':column' är inte ett DateTime objekt, saknar du en \$dates referens i Model?",
'pagination' => 'Visade poster: :from-:to av :total',
'prev_page' => 'Föregående sida',
'next_page' => 'Nästa sida',
'loading' => 'Laddar...',
'setup_title' => 'List inställningar',
'setup_help' => 'Använd kryssrutorna för att välja kolumner du vill se i listan. Du kan ändra positionen på kolumnerna genom att dra dem upp eller ner.',
'records_per_page' => 'Poster per sida',
'records_per_page_help' => 'Välj antalet poster som ska visas per sida. Observera att högt antal poster per sida kan påverka prestandan.',
'delete_selected' => 'Radera utvald',
'delete_selected_empty' => 'Det finns inga markerad post att radera.',
'delete_selected_confirm' => 'Ta bort de markerade posterna?',
'delete_selected_success' => 'De markerade posterna är raderade.',
],
'fileupload' => [
'attachment' => 'Bilaga',
'help' => 'Lägg till en och beskriving för denna bilagan.',
'title_label' => 'Titel',
'description_label' => 'Beskriving'
],
'form' => [
'create_title' => "Ny :name",
@ -102,66 +152,153 @@ return [
'missing_model' => 'Formuläregenskapen som används i :class har ingen modell definierad',
'missing_definition' => "Formuläregenskapen saknar ett fält för ':field'",
'not_found' => 'Record ID :id för formuläret kunde ej hittas',
'action_confirm' => 'Är du säker?',
'create' => 'Skapa',
'create_and_close' => 'Skapa och stäng',
'creating' => 'Skapar...',
'creating_name' => 'Skapar :name...',
'save' => 'Spara',
'save_and_close' => 'Spara och stäng',
'saving' => 'Sparar...',
'saving_name' => 'Sparar :name...',
'delete' => 'Radera',
'deleting' => 'Raderar...',
'deleting_name' => 'Raderar :name...',
'reset_default' => 'Äterställ till utgångsläge',
'resetting' => 'Återställer',
'resetting_name' => 'Återställer :name',
'undefined_tab' => 'Övrigt',
'field_off' => 'Av',
'field_on' => 'På',
'add' => 'Lägg till',
'apply' => 'Spara',
'cancel' => 'Avbryt',
'close' => 'Stäng',
'confirm' => 'Bekräfta',
'reload' => 'Ladda om',
'ok' => 'OK',
'or' => 'eller',
'confirm_tab_close' => 'Vill du verkligen stänga fliken? Ej sparade ändringar kommer gå förlorade',
'behavior_not_ready' => 'Formuläregenskap har ej blivit initierad, kontrollera att du anropat initForm() i din controller',
'preview_no_files_message' => 'Filen är inte uppladdad',
'select' => 'Välj',
'select_all' => 'alla',
'select_none' => 'ingen',
'select_placeholder' => 'Vänligen välj',
'insert_row' => 'Lägg till rad',
'delete_row' => 'Radera rad',
'concurrency_file_changed_title' => 'Filen var ändrad',
'concurrency_file_changed_description' => "Filen du redigerar har ändrats av en annan användare. Du kan antingen ladda om sidan och förlora dina ändringar eller skriva över filen med dina ändringar."
],
'relation' => [
'missing_config' => "Relations beteendet har ingen konfiguration för ': config '.",
'missing_definition' => "Relationen saknar en definintion för ':field'",
'missing_model' => "Relationen som används i :class har ingen modell definierad",
'invalid_action_single' => "Den här åtgärden kan inte appliceras på en enskild relation",
'invalid_action_multi' => "Denna åtgärd kan inte appliceras på flera relationer",
'help' => "Klicka på en post för att lägga till",
'related_data' => "Relaterad :name data",
'add' => "Lägg till",
'add_selected' => "Lägg till vald",
'add_a_new' => "Lägg till en ny :name",
'link_selected' => "Länka vald",
'link_a_new' => "Länka en ny :name",
'cancel' => "Avbryt",
'close' => "Stäng",
'add_name' => "Lägg till :name",
'create' => "Skapa",
'create_name' => "Skapa :name",
'update' => "Update",
'update_name' => "Update :name",
'update' => "Uppdatera",
'update_name' => "Uppdatera :name",
'preview' => "Förhandsgranska",
'preview_name' => "Förhandsgranska :name",
'remove' => "Ta bort",
'remove_name' => "Ta bort :name",
'delete' => "Radera",
'delete_name' => "Radera :name",
'delete_confirm' => "Är du säker?",
'link' => "Länka",
'link_name' => "Länka :name",
'unlink' => "Avlänka",
'unlink_name' => "Avlänka :name",
'unlink_confirm' => "Är du säker?",
],
'model' => [
'name' => "Modell",
'not_found' => "Modellen ':class' med ID :id kunde ej hittas",
'missing_id' => "Det finns inget ID anviget för modellen",
'missing_relation' => "Modellen ':class' saknar en definition för ':relation'",
'missing_method' => "Modellen ':class' innehåller inte metoden ':method'.",
'invalid_class' => "Modellen :model i klass :class är ej giltig. Den måste ärva från \Model-klassen",
'mass_assignment_failed' => "Mass assignment failed for Model attribute ':attribute'.",
'mass_assignment_failed' => "Mass uppdraget misslyckades för Modell-attributet ':attribute'.",
],
'warnings' => [
'tips' => 'System configuration tips',
'tips_description' => 'There are issues you need to pay attention to in order to configure the system properly.',
'permissions' => 'Directory :name or its subdirectories is not writable for PHP. Please set corresponding permissions for the webserver on this directory.',
'extension' => 'The PHP extension :name is not installed. Please install this library and activate the extension.'
'tips' => 'Systemkonfigurationstips',
'tips_description' => 'Det finns problem som du behöver åtgärda för att konfigurera systemet ordentligt.',
'permissions' => 'Katalogen :name eller dess underkataloger är inte skrivbara av PHP. Väligen ändra dess motsvarande behörigheter för web-servern i denna katalogen.',
'extension' => 'PHP-tillägget: Namnet är inte installerat. Vänligen installera och aktivera det biblioteket.'
],
'editor' => [
'menu_label' => 'Editor Configuration',
'menu_description' => 'Manage editor configuration.',
'font_size' => 'Font size',
'tab_size' => 'Tab size',
'use_hard_tabs' => 'Indent using tabs',
'code_folding' => 'Code folding',
'word_wrap' => 'Word wrap',
'highlight_active_line' => 'Highlight active line',
'show_invisibles' => 'Show invisible characters',
'show_gutter' => 'Show gutter',
'theme' => 'Color scheme',
'menu_label' => 'Kodnings preferenser',
'menu_description' => 'Anpassa dina preferenser för kodredigering, så som typsnitt och färgschema.',
'font_size' => 'Teckenstorlek',
'tab_size' => 'Tab längd',
'use_hard_tabs' => 'Indentera med tab',
'code_folding' => 'Dölj kod',
'word_wrap' => 'Radbryting',
'highlight_active_line' => 'Markera aktiv rad',
'show_invisibles' => 'Visa dolda tecken',
'show_gutter' => 'Visa ränna',
'theme' => 'Färgschema'
],
'tooltips' => [
'preview_website' => 'Förhandsgranska websidan'
],
'mysettings' => [
'menu_label' => 'Mina inställningar',
'menu_description' => 'Inställningar rörande ditt administrationskonto'
],
'myaccount' => [
'menu_label' => 'Mitt konto',
'menu_description' => 'Uppdatera dina kontouppgifter såsom namn, e-postadress och lösenord.',
'menu_keywords' => 'säkerhets inloggning'
],
'branding' => [
'menu_label' => 'Anpassa back-end',
'menu_description' => 'Anpassa administrations området såsom namn, färger och logotyp.',
'brand' => 'Varumärke',
'logo' => 'Logga',
'logo_description' => 'Ladda upp en egen logotyp för att använda i back-end.',
'app_name' => 'Applikationsnamn',
'app_name_description' => 'Detta namn visas i titelområdet back-end.',
'app_tagline' => 'Applikationstaggning',
'app_tagline_description' => 'Detta namn visas på inloggningsskärmen för back-end.',
'colors' => 'Färger',
'primary_light' => 'Primär (Ljus)',
'primary_dark' => 'Primär (Mörk)',
'secondary_light' => 'Sekundär (Ljus)',
'secondary_dark' => 'Sekundär (Mörk)',
'styles' => 'Formatmallar',
'custom_stylesheet' => 'Anpassad formatmall'
],
'backend_preferences' => [
'menu_label' => 'Back-end preferenser',
'menu_description' => 'Hantera dina kontoinställningar såsom önskat språk.',
'locale' => 'Språk',
'locale_comment' => 'Välj önskat språk.'
],
'access_log' => [
'hint' => 'Denna logg visar en lista över lyckade inloggningsförsök till administratrationen. Registret behålls i :days dagar.',
'menu_label' => 'Åtkomst logg',
'menu_description' => 'Visa en lista över framgångsrika inloggningar av back-end användare.',
'created_at' => 'Dataum och tid',
'login' => 'Inlogging',
'ip_address' => 'IP adress',
'first_name' => 'Förnamn',
'last_name' => 'Efternamn',
'email' => 'E-post'
],
'filter' => [
'all' => 'alla'
]
];

View File

@ -26,17 +26,18 @@
<?php endif ?>
<!-- Content Body -->
<div class="layout-cell layout-container" id="layout-body" >
<div class="layout-cell layout-container" id="layout-body">
<div class="layout-relative">
<div class="layout">
<!-- Breadcrumb -->
<?php if ($breadcrumbContent = Block::placeholder('breadcrumb')): ?>
<!-- Breadcrumb -->
<div class="control-breadcrumb">
<?= $breadcrumbContent ?>
</div>
<?php endif ?>
<!-- Content -->
<div class="layout-row">
<?= Block::placeholder('body') ?>
</div>

View File

@ -2,13 +2,14 @@
<div class="layout-cell">
<div class="layout">
<!-- Breadcrumb -->
<?php if ($breadcrumbContent = Block::placeholder('breadcrumb')): ?>
<!-- Breadcrumb -->
<div class="control-breadcrumb breadcrumb-flush">
<?= $breadcrumbContent ?>
</div>
<?php endif ?>
<!-- Content -->
<div class="layout-row">
<div class="padded-container layout">
<?= Block::placeholder('form-contents') ?>

View File

@ -775,9 +775,17 @@ class Form extends WidgetBase
$field = $this->allFields[$field];
}
$defaultValue = (!$this->model->exists && $field->defaults !== '')
? $field->defaults
: null;
$defaultValue = null;
if (!$this->model->exists) {
if ($field->defaultFrom) {
list($model, $attribute) = $field->resolveModelAttribute($this->model, $field->defaultFrom);
$defaultValue = $model->{$attribute};
}
elseif ($field->defaults !== '') {
$defaultValue = $field->defaults;
}
}
return $field->getValueFromData($this->data, $defaultValue);
}
@ -863,34 +871,35 @@ class Form extends WidgetBase
/*
* Handle fields that differ by fieldName and valueFrom
* @todo @deprecated / Not needed? Remove if year >= 2016
*/
$remappedFields = [];
foreach ($this->allFields as $field) {
if ($field->fieldName == $field->valueFrom) {
continue;
}
// $remappedFields = [];
// foreach ($this->allFields as $field) {
// if ($field->fieldName == $field->valueFrom) {
// continue;
// }
/*
* Get the value, remove it from the data collection
*/
$parts = HtmlHelper::nameToArray($field->fieldName);
$dotted = implode('.', $parts);
$value = array_get($data, $dotted);
array_forget($data, $dotted);
// /*
// * Get the value, remove it from the data collection
// */
// $parts = HtmlHelper::nameToArray($field->fieldName);
// $dotted = implode('.', $parts);
// $value = array_get($data, $dotted);
// array_forget($data, $dotted);
/*
* Set the new value to the data collection
*/
$parts = HtmlHelper::nameToArray($field->valueFrom);
$dotted = implode('.', $parts);
array_set($remappedFields, $dotted, $value);
}
// /*
// * Set the new value to the data collection
// */
// $parts = HtmlHelper::nameToArray($field->valueFrom);
// $dotted = implode('.', $parts);
// array_set($remappedFields, $dotted, $value);
// }
if (count($remappedFields) > 0) {
$data = array_merge($remappedFields, $data);
// Could be useful one day for field name collisions
// $data['X_OCTOBER_REMAPPED_FIELDS'] = $remappedFields;
}
// if (count($remappedFields) > 0) {
// $data = array_merge($remappedFields, $data);
// // Could be useful one day for field name collisions
// // $data['X_OCTOBER_REMAPPED_FIELDS'] = $remappedFields;
// }
return $data;
}

View File

@ -361,25 +361,6 @@ class Lists extends WidgetBase
$joins[] = $column->relation;
}
/*
* Include any relation constraints
*/
if ($joins) {
foreach (array_unique($joins) as $join) {
/*
* Apply a supplied search term for relation columns and
* constrain the query only if there is something to search for
*/
$columnsToSearch = array_get($relationSearchable, $join, []);
if (count($columnsToSearch) > 0) {
$query->whereHas($join, function ($_query) use ($columnsToSearch) {
$_query->searchWhere($this->searchTerm, $columnsToSearch);
});
}
}
}
/*
* Add eager loads to the query
*/
@ -387,6 +368,39 @@ class Lists extends WidgetBase
$query->with(array_unique($withs));
}
/*
* Apply search term
*/
$query->where(function ($innerQuery) use ($primarySearchable, $relationSearchable, $joins) {
/*
* Search primary columns
*/
if (count($primarySearchable) > 0) {
$innerQuery->orSearchWhere($this->searchTerm, $primarySearchable);
}
/*
* Search relation columns
*/
if ($joins) {
foreach (array_unique($joins) as $join) {
/*
* Apply a supplied search term for relation columns and
* constrain the query only if there is something to search for
*/
$columnsToSearch = array_get($relationSearchable, $join, []);
if (count($columnsToSearch) > 0) {
$innerQuery->orWhereHas($join, function ($_query) use ($columnsToSearch) {
$_query->searchWhere($this->searchTerm, $columnsToSearch);
});
}
}
}
});
/*
* Custom select queries
*/
@ -428,15 +442,6 @@ class Lists extends WidgetBase
}
}
/*
* Apply a supplied search term for primary columns
*/
if (count($primarySearchable) > 0) {
$query->orWhere(function ($innerQuery) use ($primarySearchable) {
$innerQuery->searchWhere($this->searchTerm, $primarySearchable);
});
}
/*
* Apply sorting
*/

View File

@ -71,21 +71,25 @@
}).get();
}
ListWidget.prototype.toggleChecked = function(el) {
var $checkbox = $('.list-checkbox input[type="checkbox"]', $(el).closest('tr'))
$checkbox.prop('checked', !$checkbox.is(':checked')).trigger('change')
}
// LIST WIDGET PLUGIN DEFINITION
// ============================
var old = $.fn.listWidget
$.fn.listWidget = function (option) {
var args = arguments,
result
var args = Array.prototype.slice.call(arguments, 1), result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.listwidget')
var options = $.extend({}, ListWidget.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.listwidget', (data = new ListWidget(this, options)))
if (typeof option == 'string') result = data[option].call($this)
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})

View File

@ -19,7 +19,7 @@
<?php $index = 0; foreach ($columns as $key => $column): ?>
<?php $index++; ?>
<td data-title="<?= e(trans($column->label)) ?>" class="list-cell-index-<?= $index ?> list-cell-name-<?= $column->columnName ?> list-cell-type-<?= $column->type ?> <?= $column->cssClass ?>">
<td data-title="<?= e(trans($column->label)) ?>" class="list-cell-index-<?= $index ?> list-cell-name-<?= $column->getName() ?> list-cell-type-<?= $column->type ?> <?= $column->cssClass ?>">
<?php if ($index == 1 && ($url = $this->getRecordUrl($record))): ?>
<a <?= $this->getRecordOnClick($record) ?> href="<?= $url ?>">
<?= $this->getColumnValue($record, $column) ?>

View File

@ -18,7 +18,7 @@
<?php if ($showSorting && $column->sortable): ?>
<th
<?php if ($column->width): ?>style="width: <?= $column->width ?>"<?php endif ?>
class="<?= $this->sortColumn==$column->columnName?'sort-'.$this->sortDirection.' active':'sort-desc' ?> list-cell-name-<?= $column->columnName ?>"
class="<?= $this->sortColumn==$column->columnName?'sort-'.$this->sortDirection.' active':'sort-desc' ?> list-cell-name-<?= $column->getName() ?> list-cell-type-<?= $column->type ?>"
>
<a
href="javascript:;"
@ -31,7 +31,7 @@
<?php else: ?>
<th
<?php if ($column->width): ?>style="width: <?= $column->width ?>"<?php endif ?>
class="list-cell-name-<?= $column->columnName ?>"
class="list-cell-name-<?= $column->getName() ?> list-cell-type-<?= $column->type ?>"
>
<span><?= $this->getHeaderValue($column) ?></span>
</th>

View File

@ -1,6 +1,6 @@
<?= Form::open([
'data-request' => $this->getEventHandler('onAddWidget'),
'data-request-success' => "\$el.trigger('close.oc.popup'); \$(window).trigger('oc.report-widget-added')",
'data-request-success' => "\$(this).trigger('close.oc.popup'); \$(window).trigger('oc.report-widget-added')",
'data-popup-load-indicator' => 1
]) ?>
<div class="modal-header">

View File

@ -353,9 +353,9 @@
dataContainer.setAttribute('type', 'hidden')
dataContainer.setAttribute('data-container', 'data-container')
dataContainer.value = records[i][columnName] !== undefined ?
records[i][columnName] :
""
dataContainer.value = records[i][columnName] !== undefined
? records[i][columnName]
: ""
cellContentContainer.setAttribute('class', 'content-container')
@ -391,9 +391,10 @@
Table.prototype.fetchRecords = function(onSuccess) {
this.dataSource.getRecords(
this.navigation.getPageFirstRowOffset(),
this.navigation.getPageFirstRowOffset(),
this.options.recordsPerPage,
onSuccess)
onSuccess
)
}
Table.prototype.updateScrollbar = function() {

View File

@ -138,6 +138,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => SettingsManager::CATEGORY_CMS,
'icon' => 'icon-picture-o',
'url' => Backend::URL('cms/themes'),
'permissions' => ['system.manage_themes'],
'order' => 200
],
'maintenance_settings' => [
@ -146,6 +147,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => SettingsManager::CATEGORY_CMS,
'icon' => 'icon-plug',
'class' => 'Cms\Models\MaintenanceSettings',
'permissions' => ['system.manage_themes'],
'order' => 400
],
]);

View File

@ -1,7 +1,7 @@
<?php namespace Cms\Classes;
use Illuminate\Support\Collection as CollectionBase;
use ApplicationException;
use Illuminate\Support\Collection as CollectionBase;
/**
* This class represents a collection of Cms Objects.
@ -11,4 +11,24 @@ use ApplicationException;
*/
class CmsObjectCollection extends CollectionBase
{
/**
* Returns objects that use the supplied component.
* @param string|array $components
* @return static
*/
public function withComponent($components)
{
return $this->filter(function($object) use ($components) {
$hasComponent = false;
foreach ((array) $components as $component) {
if ($object->hasComponent($component)) {
$hasComponent = true;
}
}
return $hasComponent;
});
}
}

View File

@ -5,6 +5,18 @@ use ApplicationException;
/**
* This class provides helper methods to make the CmsObject behave like a Model
*
* Some examples:
*
* Page::find('blog/post');
*
* Page::all();
*
* Page::inEditTheme()->useCache()->all();
*
* Page::withComponent('blogPost')
* ->sortBy('baseFileName')
* ->lists('baseFileName', 'baseFileName');
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/

View File

@ -114,10 +114,7 @@ class Page extends CmsCompoundObject
* assuming that the method is called not during the front-end
* request processing.
*/
$controller = Controller::getController();
if (!$controller) {
$controller = new Controller;
}
$controller = Controller::getController() ?: new Controller;
return $controller->pageUrl($page, $params, true);
}
@ -188,7 +185,8 @@ class Page extends CmsCompoundObject
}
$page = self::loadCached($theme, $item->reference);
$pageUrl = self::url($item->reference);
$controller = Controller::getController() ?: new Controller;
$pageUrl = $controller->pageUrl($item->reference, [], false);
$result = [];
$result['url'] = $pageUrl;

View File

@ -0,0 +1,126 @@
<?php namespace Cms\Classes;
use File;
use ApplicationException;
use System\Models\Parameters;
use Cms\Classes\Theme as CmsTheme;
/**
* Theme manager
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class ThemeManager
{
use \October\Rain\Support\Traits\Singleton;
//
// Gateway spawned
//
/**
* Returns a collection of themes installed via the update gateway
* @return array
*/
public function getInstalled()
{
return Parameters::get('system::theme.history', []);
}
/**
* Checks if a theme has ever been installed before.
* @param string $name Theme code
* @return boolean
*/
public function isInstalled($name)
{
return array_key_exists($name, Parameters::get('system::theme.history', []));
}
/**
* Flags a theme as being installed, so it is not downloaded twice.
* @param string $name Theme code
*/
public function setInstalled($code, $dirName = null)
{
if (!$dirName) {
$dirName = strtolower(str_replace('.', '-', $code));
}
$history = Parameters::get('system::theme.history', []);
$history[$code] = $dirName;
Parameters::set('system::theme.history', $history);
}
/**
* Flags a theme as being uninstalled.
* @param string $name Theme code
*/
public function setUninstalled($code)
{
$history = Parameters::get('system::theme.history', []);
if (array_key_exists($code, $history)) {
unset($history[$code]);
}
Parameters::set('system::theme.history', $history);
}
/**
* Returns an installed theme's code from it's dirname.
* @return string
*/
public function findByDirName($dirName)
{
$installed = $this->getInstalled();
foreach ($installed as $code => $name) {
if ($dirName == $name) {
return $code;
}
}
return null;
}
//
// Management
//
/**
* Completely delete a theme from the system.
* @param string $id Theme code/namespace
* @return void
*/
public function deleteTheme($theme)
{
if (!$theme) {
return false;
}
if (is_string($theme)) {
$theme = CmsTheme::load($theme);
}
if ($theme->isActiveTheme()) {
throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed'));
}
/*
* Delete from file system
*/
$themePath = $theme->getPath();
if (File::isDirectory($themePath)) {
File::deleteDirectory($themePath);
}
/*
* Set uninstalled
*/
if ($themeCode = $this->findByDirName($theme->getDirName())) {
$this->setUninstalled($themeCode);
}
}
}

View File

@ -13,6 +13,7 @@ use Cms\Models\ThemeData;
use Cms\Models\ThemeExport;
use Cms\Models\ThemeImport;
use Cms\Classes\Theme as CmsTheme;
use Cms\Classes\ThemeManager;
use System\Classes\SettingsManager;
use Backend\Classes\Controller;
use Exception;
@ -71,16 +72,7 @@ class Themes extends Controller
public function index_onDelete()
{
$theme = $this->findThemeObject();
if ($theme->isActiveTheme()) {
throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed'));
}
$themePath = $theme->getPath();
if (File::isDirectory($themePath)) {
File::deleteDirectory($themePath);
}
ThemeManager::instance()->deleteTheme(post('theme'));
Flash::success(trans('cms::lang.theme.delete_theme_success'));
return Redirect::refresh();

View File

@ -13,6 +13,6 @@
class="btn btn-default empty oc-icon-trash-o <?php if (!$templatePath): ?>hide<?php endif ?>"
data-request="onDelete"
data-request-confirm="<?= e(trans('cms::lang.content.delete_confirm_single')) ?>"
data-request-success="$.oc.cmsPage.updateTemplateList('content'); $(element).trigger('close.oc.tab', [{force: true}])"
data-request-success="$.oc.cmsPage.updateTemplateList('content'); $(this).trigger('close.oc.tab', [{force: true}])"
data-control="delete-button"></button>
</div>

View File

@ -13,6 +13,6 @@
class="btn btn-default empty oc-icon-trash-o <?php if (!$templatePath): ?>hide<?php endif ?>"
data-request="onDelete"
data-request-confirm="<?= e(trans('cms::lang.layout.delete_confirm_single')) ?>"
data-request-success="$.oc.cmsPage.updateTemplateList('layout'); $(element).trigger('close.oc.tab', [{force: true}])"
data-request-success="$.oc.cmsPage.updateTemplateList('layout'); $(this).trigger('close.oc.tab', [{force: true}])"
data-control="delete-button"></button>
</div>

View File

@ -24,6 +24,6 @@
class="btn btn-default empty oc-icon-trash-o <?php if (!$templatePath): ?>hide<?php endif ?>"
data-request="onDelete"
data-request-confirm="<?= e(trans('cms::lang.page.delete_confirm_single')) ?>"
data-request-success="$.oc.cmsPage.updateTemplateList('page'); $(element).trigger('close.oc.tab', [{force: true}])"
data-request-success="$.oc.cmsPage.updateTemplateList('page'); $(this).trigger('close.oc.tab', [{force: true}])"
data-control="delete-button"></button>
</div>

View File

@ -13,6 +13,6 @@
class="btn btn-default empty oc-icon-trash-o <?php if (!$templatePath): ?>hide<?php endif ?>"
data-request="onDelete"
data-request-confirm="<?= e(trans('cms::lang.partial.delete_confirm_single')) ?>"
data-request-success="$.oc.cmsPage.updateTemplateList('partial'); $(element).trigger('close.oc.tab', [{force: true}])"
data-request-success="$.oc.cmsPage.updateTemplateList('partial'); $(this).trigger('close.oc.tab', [{force: true}])"
data-control="delete-button"></button>
</div>

View File

@ -19,14 +19,12 @@
data-control="popup"
data-handler="onLoadCreateForm"
data-size="huge"
href="javascript:;"
target="_blank">
href="javascript:;">
<?= e(trans('cms::lang.theme.create_new_blank_theme')) ?>
</a>
<a
class="find-more-themes"
href="http://octobercms.com/themes"
target="_blank">
href="<?= Backend::url('system/updates/install/themes') ?>">
<?= e(trans('cms::lang.theme.find_more_themes')) ?>
</a>
</div>

View File

@ -74,7 +74,7 @@ return [
'new_directory_name_comment' => 'Provide a new directory name for the duplicated theme.',
'dir_name_invalid' => 'Name can contain only digits, Latin letters and the following symbols: _-',
'dir_name_taken' => 'Desired theme directory already exists.',
'find_more_themes' => 'Find more themes on OctoberCMS Theme Marketplace',
'find_more_themes' => 'Find more themes',
'return' => 'Return to themes list',
],
'maintenance' => [

View File

@ -2,32 +2,80 @@
return [
'cms_object' => [
'invalid_file' => "Ошибка в имени файла: :name. Имена файлов могут содержать только латинские буквы, цифры, знаки подчеркивания и точки. Пример правильных имен файлов: page.htm, page, subdirectory/page",
'invalid_property' => 'Параметр ":name" нельзя изменить.',
'file_already_exists' => 'Файл ":name" уже существует.',
'error_saving' => 'Ошибка сохранения файла ":name". Пожалуйста, проверьте права на запись.',
'invalid_file' => 'Ошибка в имени файла: :name. Имена файлов могут содержать только латинские буквы, цифры, знаки подчеркивания и точки. Пример правильных имен файлов: page.htm, page, subdirectory/page',
'invalid_property' => "Параметр ':name' нельзя изменить.",
'file_already_exists' => "Файл ':name' уже существует.",
'error_saving' => "Ошибка сохранения файла ':name'. Пожалуйста, проверьте права на запись.",
'error_creating_directory' => 'Ошибка создания директории :name. Пожалуйста, проверьте права на запись.',
'invalid_file_extension'=>'Указано неправильное расширение файла: :invalid. Разрешенные расширения: :allowed.',
'error_deleting' => 'Невозможно удалить файл шаблона :name. Пожалуйста, проверьте права на запись.',
'error_deleting' => "Невозможно удалить файл шаблона ':name'. Пожалуйста, проверьте права на запись.",
'delete_success' => 'Шаблоны были успешно удалены: :count.',
'file_name_required' => 'Пожалуйста, укажите имя файла шаблона.'
],
'theme' => [
'not_found_name' => "Тема ':name' не найдена.",
'active' => [
'not_set' => "Активная тема не установлена.",
'not_found' => "Активная тема не найдена.",
'not_set' => 'Активная тема не установлена.',
'not_found' => 'Активная тема не найдена.',
],
'edit' => [
'not_set' => "Тема для редактирования не установлена.",
'not_found' => "Тема для редактирования не найдена.",
'not_set' => 'Тема для редактирования не установлена.',
'not_found' => 'Тема для редактирования не найдена.',
'not_match' => "Объект, который вы пытаетесь октрыть, не пренадлежит редактируемой теме. Пожалуйста, обновите страницу."
],
'settings_menu' => 'Фронтенд темы',
'settings_menu_description' => 'Просмотр списка установленных тем и выбор активной темы.',
'find_more_themes' => 'Найти еще темы на OctoberCMS Theme Marketplace',
'name_label' => 'Название',
'name_create_placeholder' => 'Новое название темы',
'author_label' => 'Автор',
'author_placeholder' => 'Человек или название компании',
'description_label' => 'Описание',
'description_placeholder' => 'Описание темы',
'homepage_label' => 'Домашняя страница',
'homepage_placeholder' => 'Адрес сайта',
'code_label' => 'Уникальный код',
'code_placeholder' => 'Уникальный код темы, который используются для её распространения',
'dir_name_create_label' => 'Директория темы',
'dir_name_label' => 'Название директории',
'theme_label' => 'Тема',
'activate_button' => 'Активировать',
'active_button' => 'Активировано',
'customize_button' => 'Настроить',
'duplicate_button' => 'Дублировать',
'duplicate_title' => 'Дублировать тему',
'duplicate_theme_success' => 'Дублирование успешно завершено!',
'manage_button' => 'Упрвление',
'manage_title' => 'Управление темой',
'edit_properties_title' => 'Тема',
'edit_properties_button' => 'Редактирование свойств',
'save_properties' => 'Сохранить свойства',
'import_button' => 'Импортировать',
'import_title' => 'Импортировать тему',
'import_theme_success' => 'Импортирование темы успешно завершено!',
'import_uploaded_file' => 'Файл архива темы',
'import_overwrite_label' => 'Перезаписывать существующие файлы',
'import_overwrite_comment' => 'Отключите эту опцию, чтобы импортировать только новые файлы',
'import_folders_label' => 'Директории',
'import_folders_comment' => 'Пожалуйста, выберите директории темы, которые вы хотели бы импортировать',
'export_button' => 'Экспортировать',
'export_title' => 'Экспортировать тему',
'export_folders_label' => 'Директории',
'export_folders_comment' => 'Пожалуйста, выберите директории темы, которые вы хотели бы экспортировать',
'delete_button' => 'Удалить',
'delete_confirm' => 'Вы уверены, что хотите удалить эту тему? Это действие необратимо!',
'delete_active_theme_failed' => 'Невозможно удалить активный тему, попробуйте сделать другую тему активной.',
'delete_theme_success' => 'Удаление темы успешно завершено!',
'create_title' => 'Создать тему',
'create_button' => 'Создать',
'create_new_blank_theme' => 'Создать новый бланк темы',
'create_theme_success' => 'Создание темы успешно завершено!',
'create_theme_required_name' => 'Пожалуйста, укажите имя для темы.',
'new_directory_name_label' => 'Директория темы',
'new_directory_name_comment' => 'Укажите новое имя каталога для дубликата темы.',
'dir_name_invalid' => 'Имя может содержать только цифры, латинские буквы и следующие символы: _ -',
'dir_name_taken' => 'Указанный каталог уже существует.',
'find_more_themes' => 'Найти еще темы на OctoberCMS Theme Marketplace',
'return' => 'Вернуться к списку тем',
],
'maintenance' => [
'settings_menu' => 'Режим обслуживания',
@ -36,12 +84,13 @@ return [
'is_enabled_comment' => 'При активации этого режима посетители сайта увидят страницу выбранную ниже.',
],
'page' => [
'not_found_name' => "Страница ':name' не найдена",
'not_found' => [
'label' => "Страница не найдена",
'help' => "Запрошенная страница не найдена.",
'label' => 'Страница не найдена',
'help' => 'Запрошенная страница не найдена.',
],
'custom_error' => [
'label' => "Ошибка на странице",
'label' => 'Ошибка на странице',
'help' => "К сожалению, страница не может быть отображена из-за ошибки.",
],
'menu_label' => 'Страницы',
@ -64,7 +113,7 @@ return [
],
'partial' => [
'not_found_name' => "Не удалось найти шаблон (partial) с именем :name.",
'invalid_name' => "Ошибка в имени шаблона (partial) :name.",
'invalid_name' => 'Ошибка в имени шаблона (partial) :name.',
'menu_label' => 'Фрагменты',
'unsaved_label' => 'Несохранённый(е) фрагмент(ы)',
'no_list_records' => 'Фрагменты не найдены',
@ -82,11 +131,11 @@ return [
'new' => 'Новый файл содержимого'
],
'ajax_handler' => [
'invalid_name' => "Ошибка в имени обработчика AJAX: :name.",
'invalid_name' => 'Ошибка в имени обработчика AJAX: :name.',
'not_found' => "Обработчик AJAX не найден: ':name'.",
],
'cms' => [
'menu_label' => "CMS"
'menu_label' => 'CMS'
],
'sidebar' => [
'add' => 'Добавить',
@ -113,7 +162,7 @@ return [
'exit_fullscreen' => 'Выйти из полноэкранного режима'
],
'asset' => [
'menu_label' => "Ресурсы",
'menu_label' => 'Ресурсы',
'unsaved_label' => 'Несохранённый(е) файл(ы)',
'drop_down_add_title' => 'Добавить...',
'drop_down_operation_title' => 'Действие...',
@ -141,7 +190,7 @@ return [
'too_large' => 'Загруженный файл слишком велик. Максимальный допустимый размер файла составляет :max_size',
'type_not_allowed' => 'Разрешены только файлы следующих типов: :allowed_types',
'file_not_valid' => 'Файл не может быть сохранен',
'error_uploading_file' => 'Ошибка загрузки файла ":name": :error',
'error_uploading_file' => "Ошибка загрузки файла ':name': :error",
'move_please_select' => 'пожалуйста, выберите директорию',
'move_destination' => 'Новая директория',
'move_popup_title' => 'Переместить файлы',
@ -155,23 +204,24 @@ return [
'path' => 'Путь'
],
'component' => [
'menu_label' => "Компоненты",
'unnamed' => "Безымянный",
'no_description' => "Без описания",
'alias' => "Псевдоним",
'alias_description' => "Псевдоним компонента определяет его имя, под которым он доступен в коде страницы или шаблона.",
'validation_message' => "Псевдонимы обязательны и могут содержать только латинские буквы, цифры и знаки подчеркивания. Псевдонимы должны начинаться с латинской буквы.",
'invalid_request' => "Шаблон не может быть сохранен, так как запрос содержит поврежденную информацию о компоненентах.",
'menu_label' => 'Компоненты',
'unnamed' => 'Безымянный',
'no_description' => 'Без описания',
'alias' => 'Псевдоним',
'alias_description' => 'Псевдоним компонента определяет его имя, под которым он доступен в коде страницы или шаблона.',
'validation_message' => 'Псевдонимы обязательны и могут содержать только латинские буквы, цифры и знаки подчеркивания. Псевдонимы должны начинаться с латинской буквы.',
'invalid_request' => 'Шаблон не может быть сохранен, так как запрос содержит поврежденную информацию о компоненентах.',
'no_records' => 'Компоненты не найдены',
'not_found' => "Компонент ':name' не найден.",
'method_not_found' => "Компонент ':name' не содержит метод ':method'.",
],
'template' => [
'invalid_type' => "Неизвестный тип шаблона.",
'not_found' => "Запрошенный шаблон не найден.",
'saved'=> "Шаблон был успешно сохранен."
'invalid_type' => 'Неизвестный тип шаблона.',
'not_found' => 'Запрошенный шаблон не найден.',
'saved'=> 'Шаблон был успешно сохранен.'
],
'permissions' => [
'name' => 'Управление CMS',
'manage_content' => 'Управление контентом',
'manage_assets' => 'Управление файлами',
'manage_pages' => 'Управление страницами',

View File

@ -13,16 +13,78 @@ return [
'file_name_required' => 'Filnamnsfältet är obligatoriskt.'
],
'theme' => [
'not_found_name' => "Kunde inte hitta temat ':name'.",
'active' => [
'not_set' => "Ett aktivt tema är ej valt",
'not_found' => 'Kunde inte hitta det aktiva temat.'
],
'edit' => [
'not_set' => "Redigeringstemat är ej valt",
'not_found' => "Redigeringstemat kunde ej hittas",
'not_match' => "Objektet du försöker komma åt tillhör inte det tema som för håller på att redigeras. Var god ladda om sidan",
]
],
'settings_menu' => 'Front-end tema',
'settings_menu_description' => 'Förhandsgranska listan av installerade teman och välj ett aktivt tema.',
'name_label' => 'Namn',
'name_create_placeholder' => 'Nytt tema namn',
'author_label' => 'Författare',
'author_placeholder' => 'Person eller företagsnamn',
'description_label' => 'Beskrivning',
'description_placeholder' => 'Tema beskrivning',
'homepage_label' => 'Hemsida',
'homepage_placeholder' => 'Webbadress',
'code_label' => 'Kod',
'code_placeholder' => 'En unik kod för detta tema som används för distribution',
'dir_name_label' => 'Katalognamn',
'dir_name_create_label' => 'Destinationen för temakatalogen',
'theme_label' => 'Tema',
'activate_button' => 'Aktivera',
'active_button' => 'Aktivera',
'customize_button' => 'Anpassa',
'duplicate_button' => 'Duplicera',
'duplicate_title' => 'Duplicera temat',
'duplicate_theme_success' => 'Lyckades duplicera temat!',
'manage_button' => 'Hantera',
'manage_title' => 'Hantera teman',
'edit_properties_title' => 'Tema',
'edit_properties_button' => 'Redigera egenskaper',
'save_properties' => 'Spara egenskaperna',
'import_button' => 'Importera',
'import_title' => 'Importera tema',
'import_theme_success' => 'Lyckades importera temat!',
'import_uploaded_file' => 'Tema akrivfil',
'import_overwrite_label' => 'Skriv över befintliga filer',
'import_overwrite_comment' => 'Avmarkera rutan för att endast importera nya filer',
'import_folders_label' => 'Mappar',
'import_folders_comment' => 'Vänligen ange temamappen som du vill importera',
'export_button' => 'Exportera',
'export_title' => 'Exportera tema',
'export_folders_label' => 'Mappar',
'export_folders_comment' => 'Vänligen välj temamappen du vill importera',
'delete_button' => 'Radera',
'delete_confirm' => 'Är du säker på att du vill readera detta tema?? Det kan inte bli ogjort!',
'delete_active_theme_failed' => 'Du kan inte att readera det akriva temat, försök markera ett annat tema som aktivt först.',
'delete_theme_success' => 'Lyckades radera temat!',
'create_title' => 'Skapa tema',
'create_button' => 'Skapa',
'create_new_blank_theme' => 'Skapa ett nytt blankt tema',
'create_theme_success' => 'Lyckades skapa temat!',
'create_theme_required_name' => 'Vänligen ange ett namn för temat.',
'new_directory_name_label' => 'Temamappen',
'new_directory_name_comment' => 'Ange ett nytt katalognamn för det duplicerade temat.',
'dir_name_invalid' => 'Namn kan bara innehålla siffror, latinska bokstäver och följande symboler: _-',
'dir_name_taken' => 'Den önskade temakatalogen finns redan.',
'find_more_themes' => 'Hitta fler teman på OctoberCMS Theme Marketplace',
'return' => 'Återvänd till temalistan',
],
'maintenance' => [
'settings_menu' => 'Underhållsläge',
'settings_menu_description' => 'Konfigurera underhållsläge-sidan och växla inställningen.',
'is_enabled' => 'Akrivera underhållsläge-läget',
'is_enabled_comment' => 'När den är aktiverad så kommer besökare att se sidan som väljs nedan.'
],
'page' => [
'not_found_name' => "The page ':name' is not found",
'not_found' => [
'label' => "Sidan kunde ej hittas",
'help' => "Den begärda sidan kunde ej hittas",
@ -32,6 +94,7 @@ return [
'help' => "Tyvärr kan inte sidan visas",
],
'menu_label' => 'Sidor',
'unsaved_label' => 'Osparade sidor',
'no_list_records' => 'Inga sidor funna',
'new' => 'Ny sida',
'invalid_url' => 'Felaktigt URL-format. URLen skall starta med ett / och kan innehålla siffror, bokstäver och följande tecken: ._-[]:?|/+*^$',
@ -42,6 +105,7 @@ return [
'layout' => [
'not_found_name' => "Layouten ':name' hittades ej",
'menu_label' => 'Layouter',
'unsaved_label' => 'Osparade layouter',
'no_list_records' => 'Inga layouter funna',
'new' => 'Ny layout',
'delete_confirm_multiple' => 'Vill du verkligen radera valda layouter?',
@ -51,6 +115,7 @@ return [
'not_found_name' => "En partial med namnet ':name' kunde ej hittas",
'invalid_name' => "Felaktigt partialnamn: :name",
'menu_label' => 'Partials',
'unsaved_label' => 'Osparade partials',
'no_list_records' => 'Inga partials funna',
'delete_confirm_multiple' => 'Vill du verkligen radera markerade partials?',
'delete_confirm_single' => 'Vill du verkligen radera denna partial?',
@ -59,6 +124,7 @@ return [
'content' => [
'not_found_name' => "Innehållet ':name' kunde ej hittas",
'menu_label' => 'Innehåll',
'unsaved_label' => 'Osparat innehåll',
'no_list_records' => 'Inga innehållsfiler funna',
'delete_confirm_multiple' => 'Vill du verkligen radera markerade filer eller mappar?',
'delete_confirm_single' => 'Vill du verkligen radera detta innehållsfil?',
@ -90,17 +156,25 @@ return [
'markup' => 'Markup',
'code' => 'Kod',
'content' => 'Innehåll',
'hidden' => 'Dold',
'hidden_comment' => 'Dolda sidor är endast tillgängliga genom inloggade back-end användare.',
'enter_fullscreen' => 'Starta helskärmsläge',
'exit_fullscreen' => 'Avsluta helskärmsläge'
],
'asset' => [
'menu_label' => "Filsystem",
'unsaved_label' => 'Osparade filer',
'drop_down_add_title' => 'Lägg till...',
'drop_down_operation_title' => 'Åtgärd...',
'upload_files' => 'Ladda upp fil(er)',
'create_file' => 'Skapa fil',
'create_directory' => 'Skapa mapp',
'directory_popup_title' => 'Ny mapp',
'directory_name' => 'Mappnamn',
'rename' => 'Döp om',
'delete' => 'Radera',
'move' => 'Flytta',
'select' => 'Välj',
'new' => 'Ny fil',
'rename_popup_title' => 'Byt namn',
'rename_new_name' => 'Nytt namn',
@ -145,5 +219,14 @@ return [
'invalid_type' => "Felaktig malltyp",
'not_found' => "Den angivna mallen kunde ej hittas",
'saved'=> "Mallen har sparats"
],
'permissions' => [
'name' => 'Cms',
'manage_content' => 'Hantera innehåll',
'manage_assets' => 'Hantera filer',
'manage_pages' => 'Hantera sidor',
'manage_layouts' => 'Hantera layouts',
'manage_partials' => 'Hantera partials',
'manage_themes' => 'Hantera teman'
]
];

View File

@ -1,6 +1,6 @@
<?= Form::open([
'data-request'=>$this->getEventHandler('onMove'),
'data-request-success'=>"\$el.trigger('close.oc.popup')",
'data-request-success'=>"\$(this).trigger('close.oc.popup')",
'data-stripe-load-indicator'=>1,
'id'=>'asset-move-popup-form'
]) ?>

View File

@ -1,6 +1,6 @@
<?= Form::open([
'data-request'=>$this->getEventHandler('onNewDirectory'),
'data-request-success'=>"\$el.trigger('close.oc.popup')",
'data-request-success'=>"\$(this).trigger('close.oc.popup')",
'data-stripe-load-indicator'=>1,
'id'=>'asset-new-dir-popup-form'
]) ?>

View File

@ -150,7 +150,7 @@ class ServiceProvider extends ModuleServiceProvider
'label' => 'system::lang.settings.menu_label',
'icon' => 'icon-cog',
'url' => Backend::url('system/settings'),
'permissions' => ['backend.manage_users', 'system.*'],
'permissions' => [],
'order' => 1000
]
]);
@ -175,6 +175,10 @@ class ServiceProvider extends ModuleServiceProvider
'label' => 'system::lang.permissions.manage_software_updates',
'tab' => 'system::lang.permissions.name'
],
'system.access_logs' => [
'label' => 'system::lang.permissions.access_logs',
'tab' => 'system::lang.permissions.name'
],
'system.manage_mail_settings' => [
'label' => 'system::lang.permissions.manage_mail_settings',
'tab' => 'system::lang.permissions.name'
@ -258,7 +262,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => SettingsManager::CATEGORY_LOGS,
'icon' => 'icon-exclamation-triangle',
'url' => Backend::url('system/eventlogs'),
'permissions' => ['system.access_event_logs'],
'permissions' => ['system.access_logs'],
'order' => 800
],
'request_logs' => [
@ -267,7 +271,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => SettingsManager::CATEGORY_LOGS,
'icon' => 'icon-file-o',
'url' => Backend::url('system/requestlogs'),
'permissions' => ['system.access_request_logs'],
'permissions' => ['system.access_logs'],
'order' => 800
],
'mail_settings' => [

View File

@ -0,0 +1,275 @@
.product-list-empty {
padding: 5px 0;
font-size: 16px;
color: #999;
}
.product-list {
margin: 0;
padding: 10px 0;
overflow: hidden;
}
.product-list li button {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
opacity: 0;
outline: none;
}
.product-list li button,
.product-list li .image,
.product-list li .details {
-webkit-transition: opacity .2s linear;
-moz-transition: opacity .2s linear;
transition: opacity .2s linear;
}
.product-list li:hover button {
opacity: .3;
}
.product-list li:hover button:hover {
opacity: .8;
}
.plugin-list li {
list-style: none;
position: relative;
border-bottom: 1px solid #E6E9E9;
margin-bottom: 10px;
padding-bottom: 10px;
overflow: hidden;
}
.plugin-list li:last-child {
border-bottom: none;
}
.plugin-list li .image {
float: left;
margin-right: 15px;
margin-left: 5px;
}
.plugin-list li .image img {
width: 50px;
height: 50px;
}
.plugin-list li .details p {
padding: 0;
margin: 3px 0 0 0;
color: #808C8D;
}
.plugin-list li h4 {
padding: 5px 0 0;
margin: 0;
color: #C03F31;
font-weight: 400;
}
.theme-list li {
float: left;
padding: 0;
margin: 0 10px 10px 0;
list-style: none;
border: 1px solid #E6E9E9;
background: #fff;
position: relative;
border-radius: 3px;
}
.theme-list li:hover {
border-color: transparent;
}
.theme-list li {
-webkit-transition: border .2s linear;
-moz-transition: border .2s linear;
transition: border .2s linear;
}
.theme-list li .image {
padding: 5px;
}
.theme-list li .image img {
width: 210px;
height: 140px;
}
.theme-list li:hover .image {
opacity: 0;
}
.theme-list li .details {
position: absolute;
bottom: 0;
left: 0;
opacity: 0;
padding: 10px;
overflow: hidden;
}
.theme-list li:hover .details {
opacity: 1;
}
.theme-list li h4 {
padding: 15px 0 0;
margin: 0;
}
.theme-list li p {
padding: 0;
margin: 0;
color: #999;
text-transform: uppercase;
font-size: 12px;
}
.suggested-products {
padding: 0;
}
.suggested-products .product {
padding: 0;
}
.suggested-products .image img {
width: 40px;
height: 40px;
margin-top: 10px;
}
.suggested-themes .image img {
width: 60px;
height: 40px;
}
.suggested-products .image {
float: left;
position: relative;
}
.suggested-products .details {
margin-left: 50px;
padding: 10px 0;
}
.suggested-themes .details {
margin-left: 70px;
}
.suggested-products .details h5 {
margin: 0 0 3px;
font-size: 14px;
color: #C03F31;
font-weight: 400;
}
.suggested-products .details p {
font-size: 12px;
}
.suggested-products a {
color: #777;
background: #fff;
padding: 5px;
text-decoration: none;
display: block;
overflow: hidden;
border-bottom: 1px solid #E6E9E9;
}
.suggested-products a:hover {
color: #333;
background: #f9f9f9;
}
.suggested-products a:hover .image:after {
content: "+";
color: #999;
font-size: 32px;
display: block;
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
position: absolute;
top: 7px;
left: 0;
}
.suggested-products a:hover .image img {
opacity: .5;
}
/*!
* Typeahead
*/
.product-search {
position: relative;
width: 100%;
margin: 0 auto 0 auto;
text-align: left;
padding-bottom: 15px;
}
.twitter-typeahead {
width: 100%;
}
.typeahead,
.tt-hint {
width: 100%;
height: 46px;
padding: 8px 12px;
font-size: 24px;
line-height: 30px;
border: 1px solid #024e6a;
border-radius: 3px;
outline: none;
}
.typeahead {
background-color: #fff;
border-color: #e0e0e0;
}
.tt-input {
font-weight: 200;
}
.tt-input:focus {
border-color: #E6E9E9;
}
.tt-hint {
color: #999;
font-weight: 200;
}
.tt-dropdown-menu {
width: 100%;
margin-top: 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}
.tt-suggestion {
font-size: 14px;
line-height: 18px;
}
.tt-suggestion + .tt-suggestion {
font-size: 14px;
border-top: 1px solid #ccc;
}
.tt-suggestions .product-details {
padding: 5px;
overflow: hidden;
position: relative;
}
.tt-suggestions .product-image {
float: left;
margin-right: 10px;
}
.tt-suggestions .product-image img {
height: 45px;
width: 45px;
}
.tt-suggestions .product-name {
font-size: 20px;
padding-top: 5px;
}
.tt-suggestion.tt-cursor {
cursor: pointer;
}
.tt-suggestion.tt-cursor .product-details {
color: #333;
background: #f9f9f9;
border-color: #f0f0f0;
}
.tt-suggestion.tt-cursor .product-details .product-image:after {
content: "+";
color: #999;
font-size: 38px;
display: block;
width: 45px;
height: 45px;
text-align: center;
line-height: 45px;
position: absolute;
top: 5px;
left: 5px;
}
.tt-suggestion.tt-cursor .product-details .product-image img {
opacity: .5;
}

View File

@ -95,7 +95,7 @@ if (window.jQuery === undefined)
* Halt here if beforeUpdate() or data-request-before-update returns false
*/
if (this.options.beforeUpdate.apply(this, [data, textStatus, jqXHR]) === false) return
if (options.evalBeforeUpdate && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalBeforeUpdate+'}($el, context, data, textStatus, jqXHR))') === false) return
if (options.evalBeforeUpdate && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalBeforeUpdate+'}.call($el.get(0), $el, context, data, textStatus, jqXHR))') === false) return
/*
* Trigger 'ajaxBeforeUpdate' on the form, halt if event.preventDefault() is called
@ -111,7 +111,7 @@ if (window.jQuery === undefined)
updatePromise.done(function(){
form.trigger('ajaxSuccess', [context, data, textStatus, jqXHR])
options.evalSuccess && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalSuccess+'}($el, context, data, textStatus, jqXHR))')
options.evalSuccess && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalSuccess+'}.call($el.get(0), $el, context, data, textStatus, jqXHR))')
})
return updatePromise
@ -158,7 +158,7 @@ if (window.jQuery === undefined)
/*
* Halt here if the data-request-error attribute returns false
*/
if (options.evalError && eval('(function($el, context, textStatus, jqXHR) {'+options.evalError+'}($el, context, textStatus, jqXHR))') === false)
if (options.evalError && eval('(function($el, context, textStatus, jqXHR) {'+options.evalError+'}.call($el.get(0), $el, context, textStatus, jqXHR))') === false)
return
requestOptions.handleErrorMessage(errorMsg)
@ -166,6 +166,10 @@ if (window.jQuery === undefined)
return updatePromise
},
complete: function(data, textStatus, jqXHR) {
form.trigger('ajaxComplete', [context, data, textStatus, jqXHR])
options.evalComplete && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalComplete+'}.call($el.get(0), $el, context, data, textStatus, jqXHR))')
},
/*
* Custom function, display an error message to the user
@ -189,7 +193,7 @@ if (window.jQuery === undefined)
var updatePromise = $.Deferred().done(function(){
for (var partial in data) {
/*
* If a partial has been supplied on the client side that matches the server supplied key, look up
* If a partial has been supplied on the client side that matches the server supplied key, look up
* it's selector and use that. If not, we assume it is an explicit selector reference.
*/
var selector = (options.update[partial]) ? options.update[partial] : partial
@ -262,6 +266,7 @@ if (window.jQuery === undefined)
*/
context.success = requestOptions.success
context.error = requestOptions.error
context.complete = requestOptions.complete
requestOptions = $.extend(requestOptions, options)
requestOptions.data = data.join('&')
@ -294,7 +299,8 @@ if (window.jQuery === undefined)
beforeUpdate: function(data, textStatus, jqXHR) {},
evalBeforeUpdate: null,
evalSuccess: null,
evalError: null
evalError: null,
evalComplete: null,
}
/*
@ -322,6 +328,7 @@ if (window.jQuery === undefined)
evalBeforeUpdate: $this.data('request-before-update'),
evalSuccess: $this.data('request-success'),
evalError: $this.data('request-error'),
evalComplete: $this.data('request-complete'),
confirm: $this.data('request-confirm'),
redirect: $this.data('request-redirect'),
loading: $this.data('request-loading'),
@ -411,7 +418,7 @@ if (window.jQuery === undefined)
/*
* Invent our own event that unifies document.ready with window.ajaxUpdateComplete
*
*
* $(document).render(function() { })
* $(document).on('render', function(){ })
*/

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
@import "../../../backend/assets/less/core/boot.less";
@import "../../../../backend/assets/less/core/boot.less";
.control-settings {

View File

@ -0,0 +1,301 @@
@import "../../../../backend/assets/less/core/boot.less";
.product-list-empty {
padding: 5px 0;
font-size: 16px;
color: #999;
}
.product-list {
margin: 0;
padding: 10px 0;
overflow: hidden; /* clearfix */
li {
button {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
opacity: 0;
outline: none;
}
button, .image, .details {
-webkit-transition: opacity .2s linear;
-moz-transition: opacity .2s linear;
transition: opacity .2s linear;
}
}
li:hover {
button {
opacity: .3;
}
button:hover {
opacity: .8;
}
}
}
.plugin-list {
li {
list-style: none;
position: relative;
border-bottom: 1px solid #E6E9E9;
margin-bottom: 10px;
padding-bottom: 10px;
overflow: hidden;
.image {
float: left;
margin-right: 15px;
margin-left: 5px;
img {
width: 50px;
height: 50px;
}
}
.details {
p {
padding: 0;
margin: 3px 0 0 0;
color: #808C8D;
}
}
h4 {
padding: 5px 0 0;
margin: 0;
color: #C03F31;
font-weight: 400;
}
}
li:last-child {
border-bottom: none;
}
}
.theme-list {
li {
float: left;
padding: 0;
margin: 0 10px 10px 0;
list-style: none;
border: 1px solid #E6E9E9;
background: #fff;
position: relative;
border-radius: 3px;
-webkit-transition: border .2s linear;
-moz-transition: border .2s linear;
transition: border .2s linear;
.image {
padding: 5px;
img {
width: 210px;
height: 140px;
}
}
.details {
position: absolute;
bottom: 0;
left: 0;
opacity: 0;
padding: 10px;
overflow: hidden;
}
h4 {
padding: 15px 0 0;
margin: 0;
}
p {
padding: 0;
margin: 0;
color: #999;
text-transform: uppercase;
font-size: 12px;
}
}
li:hover {
border-color: transparent;
.image {
opacity: 0;
}
.details {
opacity: 1;
}
}
}
.suggested-products {
padding: 0;
.product {
padding: 0;
}
.image {
float: left;
position: relative;
img {
width: 40px;
height: 40px;
margin-top: 10px;
}
}
.details {
margin-left: 50px;
padding: 10px 0;
h5 {
margin: 0 0 3px;
font-size: 14px;
color: #C03F31;
font-weight: 400;
}
p {
font-size: 12px;
}
}
a {
color: #777;
background: #fff;
padding: 5px;
text-decoration: none;
display: block;
overflow: hidden;
border-bottom: 1px solid #E6E9E9;
}
a:hover {
color: #333;
background: #f9f9f9;
.image:after {
content: "+";
color: #999;
font-size: 32px;
display: block;
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
position: absolute;
top: 7px;
left: 0;
}
.image {
img {
opacity: .5;
}
}
}
}
.suggested-themes {
.image {
img {
width: 60px;
height: 40px;
}
}
.details {
margin-left: 70px;
}
}
/*!
* Typeahead
*/
.product-search {
position: relative;
width: 100%;
margin: 0 auto 0 auto;
text-align: left;
padding-bottom: 15px;
}
.twitter-typeahead {
width: 100%;
}
.typeahead, .tt-hint {
width: 100%;
height: 46px;
padding: 8px 12px;
font-size: 24px;
line-height: 30px;
border: 1px solid #024e6a;
border-radius: 3px;
outline: none;
}
.typeahead {
background-color: #fff;
border-color: #e0e0e0;
}
.tt-input {
font-weight: 200;
}
.tt-input:focus {
border-color: #E6E9E9;
}
.tt-hint {
color: #999;
font-weight: 200;
}
.tt-dropdown-menu {
width: 100%;
margin-top: 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
}
.tt-suggestion {
font-size: 14px;
line-height: 18px;
+ {
.tt-suggestion {
font-size: 14px;
border-top: 1px solid #ccc;
}
}
}
.tt-suggestions {
.product-details {
padding: 5px;
overflow: hidden;
position: relative;
}
.product-image {
float: left;
margin-right: 10px;
img {
height: 45px;
width: 45px;
}
}
.product-name {
font-size: 20px;
padding-top: 5px;
}
}
.tt-suggestion.tt-cursor {
cursor: pointer;
.product-details {
color: #333;
background: #f9f9f9;
border-color: #f0f0f0;
.product-image:after {
content: "+";
color: #999;
font-size: 38px;
display: block;
width: 45px;
height: 45px;
text-align: center;
line-height: 45px;
position: absolute;
top: 5px;
left: 5px;
}
.product-image {
img {
opacity: .5;
}
}
}
}

View File

@ -1,4 +1,4 @@
@import "../../../backend/assets/less/core/boot.less";
@import "../../../../backend/assets/less/core/boot.less";
.control-updatelist {

View File

@ -645,4 +645,40 @@ class PluginManager
return $result;
}
//
// Management
//
/**
* Completely roll back and delete a plugin from the system.
* @param string $id Plugin code/namespace
* @return void
*/
public function deletePlugin($id)
{
/*
* Rollback plugin
*/
UpdateManager::instance()->rollbackPlugin($id);
/*
* Delete from file system
*/
if ($pluginPath = PluginManager::instance()->getPluginPath($id)) {
File::deleteDirectory($pluginPath);
}
}
/**
* Tears down a plugin's database tables and rebuilds them.
* @param string $id Plugin code/namespace
* @return void
*/
public function refreshPlugin($id)
{
$manager = UpdateManager::instance();
$manager->rollbackPlugin($id);
$manager->updatePlugin($id);
}
}

View File

@ -5,14 +5,16 @@ use URL;
use File;
use Lang;
use Http;
use Cache;
use Schema;
use Config;
use Carbon\Carbon;
use ApplicationException;
use Cms\Classes\ThemeManager;
use System\Models\Parameters;
use System\Models\PluginVersion;
use ApplicationException;
use System\Helpers\Cache as CacheHelper;
use October\Rain\Filesystem\Zip;
use Carbon\Carbon;
use Exception;
/**
@ -48,6 +50,11 @@ class UpdateManager
*/
protected $pluginManager;
/**
* @var Cms\Classes\ThemeManager
*/
protected $themeManager;
/**
* @var System\Classes\VersionManager
*/
@ -68,12 +75,18 @@ class UpdateManager
*/
protected $disableCoreUpdates = false;
/**
* @var array Cache of gateway products
*/
protected $productCache;
/**
* Initialize this singleton.
*/
protected function init()
{
$this->pluginManager = PluginManager::instance();
$this->themeManager = ThemeManager::instance();
$this->versionManager = VersionManager::instance();
$this->tempDirectory = temp_path();
$this->baseDirectory = base_path();
@ -235,7 +248,7 @@ class UpdateManager
*/
$themes = [];
foreach (array_get($result, 'themes', []) as $code => $info) {
if (!$this->isThemeInstalled($code)) {
if (!$this->themeManager->isInstalled($code)) {
$themes[$code] = $info;
}
}
@ -492,6 +505,17 @@ class UpdateManager
// Themes
//
/**
* Looks up a theme from the update server.
* @param string $name Theme name.
* @return array Details about the theme.
*/
public function requestThemeDetails($name)
{
$result = $this->requestServerData('theme/detail', ['name' => $name]);
return $result;
}
/**
* Downloads a theme from the update server.
* @param string $name Theme name.
@ -516,29 +540,118 @@ class UpdateManager
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
}
$this->setThemeInstalled($name);
$this->themeManager->setInstalled($name);
@unlink($filePath);
}
/**
* Checks if a theme has ever been installed before.
* @param string $name Theme code
* @return boolean
*/
public function isThemeInstalled($name)
//
// Products
//
public function requestProductDetails($codes, $type = null)
{
return array_key_exists($name, Parameters::get('system::theme.history', []));
if ($type != 'plugin' && $type != 'theme')
$type = 'plugin';
$codes = (array) $codes;
$this->loadProductDetailCache();
/*
* New products requested
*/
$newCodes = array_diff($codes, array_keys($this->productCache[$type]));
if (count($newCodes)) {
$dataCodes = [];
$data = $this->requestServerData($type.'/details', ['names' => $newCodes]);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
$dataCodes[] = $code;
}
/*
* Cache unknown products
*/
$unknownCodes = array_diff($newCodes, $dataCodes);
foreach ($unknownCodes as $code) {
$this->cacheProductDetail($type, $code, -1);
}
$this->saveProductDetailCache();
}
/*
* Build details from cache
*/
$result = [];
$requestedDetails = array_intersect_key($this->productCache[$type], array_flip($codes));
foreach ($requestedDetails as $detail) {
if ($detail === -1) continue;
$result[] = $detail;
}
return $result;
}
/**
* Flags a theme as being installed, so it is not downloaded twice.
* @param string $name Theme code
* Returns popular themes found on the marketplace.
*/
public function setThemeInstalled($name)
public function requestPopularProducts($type = null)
{
$history = Parameters::get('system::theme.history', []);
$history[$name] = Carbon::now()->timestamp;
Parameters::set('system::theme.history', $history);
if ($type != 'plugin' && $type != 'theme')
$type = 'plugin';
$cacheKey = 'system-updates-popular-'.$type;
if (Cache::has($cacheKey)) {
return @unserialize(Cache::get($cacheKey)) ?: [];
}
$data = $this->requestServerData($type.'/popular');
Cache::put($cacheKey, serialize($data), 60);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
}
$this->saveProductDetailCache();
return $data;
}
protected function loadProductDetailCache()
{
$defaultCache = ['theme' => [], 'plugin' => []];
$cacheKey = 'system-updates-product-details';
if (Cache::has($cacheKey)) {
$this->productCache = @unserialize(Cache::get($cacheKey)) ?: $defaultCache;
}
else {
$this->productCache = $defaultCache;
}
}
protected function saveProductDetailCache()
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
}
$cacheKey = 'system-updates-product-details';
$expiresAt = Carbon::now()->addDays(2);
Cache::put($cacheKey, serialize($this->productCache), $expiresAt);
}
protected function cacheProductDetail($type, $code, $data)
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
}
$this->productCache[$type][$code] = $data;
}
//

View File

@ -26,7 +26,7 @@ class EventLogs extends Controller
'Backend.Behaviors.ListController'
];
public $requiredPermissions = ['system.access_event_logs'];
public $requiredPermissions = ['system.access_logs'];
public $formConfig = 'config_form.yaml';

View File

@ -26,7 +26,7 @@ class RequestLogs extends Controller
'Backend.Behaviors.ListController'
];
public $requiredPermissions = ['system.access_request_logs'];
public $requiredPermissions = ['system.access_logs'];
public $formConfig = 'config_form.yaml';

View File

@ -34,7 +34,7 @@ class Settings extends Controller
$this->requiredPermissions = null;
}
$this->addCss('/modules/system/assets/css/settings.css', 'core');
$this->addCss('/modules/system/assets/css/settings/settings.css', 'core');
BackendMenu::setContext('October.System', 'system', 'settings');
}

View File

@ -7,7 +7,9 @@ use Flash;
use Config;
use Backend;
use Redirect;
use Response;
use BackendMenu;
use Cms\Classes\ThemeManager;
use Backend\Classes\Controller;
use System\Models\Parameters;
use System\Models\PluginVersion;
@ -37,7 +39,7 @@ class Updates extends Controller
parent::__construct();
$this->addJs('/modules/system/assets/js/updates/updates.js', 'core');
$this->addCss('/modules/system/assets/css/updates.css', 'core');
$this->addCss('/modules/system/assets/css/updates/updates.css', 'core');
BackendMenu::setContext('October.System', 'system', 'updates');
SettingsManager::setContext('October.System', 'updates');
@ -67,6 +69,31 @@ class Updates extends Controller
return $this->asExtension('ListController')->index();
}
/**
* Install new plugins / themes
*/
public function install($tab = null)
{
if (get('search')) {
return Response::make($this->onSearchProducts());
}
try {
$this->bodyClass = 'compact-container breadcrumb-flush';
$this->pageTitle = 'Install products';
$this->addJs('/modules/system/assets/js/updates/install.js', 'core');
$this->addCss('/modules/system/assets/css/updates/install.css', 'core');
$this->vars['activeTab'] = $tab ?: 'plugins';
$this->vars['installedPlugins'] = $this->getInstalledPlugins();
$this->vars['installedThemes'] = $this->getInstalledThemes();
}
catch (Exception $ex) {
$this->handleError($ex);
}
}
/**
* {@inheritDoc}
*/
@ -132,12 +159,12 @@ class Updates extends Controller
case 'completeUpdate':
$manager->update();
Flash::success(Lang::get('system::lang.updates.update_success'));
return Backend::redirect('system/updates');
return Redirect::refresh();
case 'completeInstall':
$manager->update();
Flash::success(Lang::get('system::lang.install.install_success'));
return Backend::redirect('system/updates');
return Redirect::refresh();
}
}
@ -442,7 +469,7 @@ class Updates extends Controller
}
/**
* Removes or purges plugins from the system.
* Rollback and remove plugins from the system.
* @return void
*/
public function onRemovePlugins()
@ -454,18 +481,7 @@ class Updates extends Controller
continue;
}
/*
* Rollback plugin
*/
$pluginCode = $object->code;
UpdateManager::instance()->rollbackPlugin($pluginCode);
/*
* Delete from file system
*/
if ($pluginPath = PluginManager::instance()->getPluginPath($pluginCode)) {
File::deleteDirectory($pluginPath);
}
PluginManager::instance()->deletePlugin($object->code);
}
Flash::success(Lang::get('system::lang.plugins.remove_success'));
@ -474,6 +490,22 @@ class Updates extends Controller
return $this->listRefresh('manage');
}
/**
* Rollback and remove a single plugin from the system.
* @return void
*/
public function onRemovePlugin()
{
if ($pluginCode = post('code')) {
PluginManager::instance()->deletePlugin($pluginCode);
Flash::success(Lang::get('system::lang.plugins.remove_success'));
}
return Redirect::refresh();
}
/**
* Rebuilds plugin database migrations.
* @return void
@ -482,19 +514,12 @@ class Updates extends Controller
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
$manager = UpdateManager::instance();
foreach ($checkedIds as $objectId) {
if (!$object = PluginVersion::find($objectId)) {
continue;
}
/*
* Refresh plugin
*/
$pluginCode = $object->code;
$manager->rollbackPlugin($pluginCode);
$manager->updatePlugin($pluginCode);
PluginManager::instance()->refreshPlugin($object->code);
}
Flash::success(Lang::get('system::lang.plugins.refresh_success'));
@ -548,4 +573,145 @@ class Updates extends Controller
return Backend::redirect('system/updates/manage');
}
//
// Theme management
//
/**
* Validate the theme code and execute the theme installation
*/
public function onInstallTheme()
{
try {
if (!$code = trim(post('code'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_theme_name'));
}
$manager = UpdateManager::instance();
$result = $manager->requestThemeDetails($code);
if (!isset($result['code']) || !isset($result['hash'])) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
$name = $result['code'];
$hash = $result['hash'];
$themes = [$name => $hash];
$plugins = [];
/*
* Update steps
*/
$updateSteps = $this->buildUpdateSteps(null, $plugins, $themes);
/*
* Finish up
*/
$updateSteps[] = [
'code' => 'completeInstall',
'label' => Lang::get('system::lang.install.install_completing'),
];
$this->vars['updateSteps'] = $updateSteps;
return $this->makePartial('execute');
}
catch (Exception $ex) {
$this->handleError($ex);
return $this->makePartial('theme_form');
}
}
/**
* Deletes a single theme from the system.
* @return void
*/
public function onRemoveTheme()
{
if ($themeCode = post('code')) {
ThemeManager::instance()->deleteTheme($themeCode);
Flash::success(trans('cms::lang.theme.delete_theme_success'));
}
return Redirect::refresh();
}
//
// Product install
//
public function onSearchProducts()
{
$searchType = get('search', 'plugins');
$serverUri = $searchType == 'plugins' ? 'plugin/search' : 'theme/search';
$manager = UpdateManager::instance();
return $manager->requestServerData($serverUri, ['query' => get('query')]);
}
public function onGetPopularPlugins()
{
$installed = $this->getInstalledPlugins();
$popular = UpdateManager::instance()->requestPopularProducts('plugin');
$popular = $this->filterPopularProducts($popular, $installed);
return ['result' => $popular];
}
public function onGetPopularThemes()
{
$installed = $this->getInstalledThemes();
$popular = UpdateManager::instance()->requestPopularProducts('theme');
$popular = $this->filterPopularProducts($popular, $installed);
return ['result' => $popular];
}
protected function getInstalledPlugins()
{
$installed = PluginVersion::lists('code');
$manager = UpdateManager::instance();
return $manager->requestProductDetails($installed, 'plugin');
}
protected function getInstalledThemes()
{
$history = Parameters::get('system::theme.history', []);
$manager = UpdateManager::instance();
$installed = $manager->requestProductDetails(array_keys($history), 'theme');
/*
* Splice in the directory names
*/
foreach ($installed as $key => $data) {
$code = array_get($data, 'code');
$installed[$key]['dirName'] = array_get($history, $code, $code);
}
return $installed;
}
/*
* Remove installed products from the collection
*/
protected function filterPopularProducts($popular, $installed)
{
$installedArray = [];
foreach ($installed as $product) {
$installedArray[] = array_get($product, 'code', -1);
}
foreach ($popular as $key => $product) {
$code = array_get($product, 'code');
if (in_array($code, $installedArray)) {
unset($popular[$key]);
}
}
return array_values($popular);
}
}

View File

@ -2,6 +2,7 @@
<a
href="javascript:;"
data-request="onEmptyLog"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-load-indicator="<?= e(trans('system::lang.event_log.empty_loading')) ?>"
class="btn btn-default oc-icon-eraser">
<?= e(trans('system::lang.event_log.empty_link')) ?>
@ -13,11 +14,10 @@
checked: $('.control-list').listWidget('getChecked')
})"
data-request="onDelete"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$el.prop('disabled', false)"
data-request-success="$(this).prop('disabled', false)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>

View File

@ -1,4 +1,4 @@
<div class="padded-container list-header">
<div class="padded-container container-flush">
<?= $this->makeHintPartial('system_eventlogs_hint', 'hint') ?>
</div>

View File

@ -2,6 +2,7 @@
<a
href="javascript:;"
data-request="onEmptyLog"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-load-indicator="<?= e(trans('system::lang.request_log.empty_loading')) ?>"
class="btn btn-default oc-icon-eraser">
<?= e(trans('system::lang.request_log.empty_link')) ?>
@ -13,11 +14,10 @@
checked: $('.control-list').listWidget('getChecked')
})"
data-request="onDelete"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$el.prop('disabled', false)"
data-request-success="$(this).prop('disabled', false)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>

View File

@ -1,4 +1,4 @@
<div class="padded-container list-header">
<div class="padded-container container-flush">
<?= $this->makeHintPartial('system_requestlogs_hint', 'hint') ?>
</div>

View File

@ -0,0 +1,102 @@
<div>
<!-- Search -->
<form
id="installPluginsForm"
data-handler="onInstallPlugin"
onsubmit="$.oc.installProcess.searchSubmit(this); return false">
<div class="product-search">
<input
name="code"
id="pluginSearchInput"
class="product-search-input search-input-lg typeahead"
placeholder="search plugins to install..."
data-search-type="plugins"
/>
</div>
</form>
<div class="row">
<div class="col-md-7">
<!-- Installed plugins -->
<div id="pluginList"
class="product-list-manager">
<h4 class="section-header">
<a href="<?= Backend::url('system/updates') ?>">Installed plugins</a>
<small>(<span class="product-counter"><?= count($installedPlugins) ?></span>)</small>
</h4>
<?php if (!count($installedPlugins)): ?>
<div class="product-list-empty">
<p>There are no plugins installed from the marketplace.</p>
</div>
<?php else: ?>
<ul class="product-list plugin-list">
<?php foreach ($installedPlugins as $plugin): ?>
<li data-code="<?= $plugin['code'] ?>">
<div class="image">
<img src="<?= $plugin['image'] ?>" alt="">
</div>
<div class="details">
<h4><?= $plugin['name'] ?></h4>
<p>by <?= $plugin['author'] ?></p>
</div>
<button
type="button"
class="close"
aria-hidden="true"
data-request="onRemovePlugin"
data-request-data="code: '<?= $plugin['code'] ?>'"
data-request-confirm="Are you sure you want to remove this?"
data-stripe-load-indicator>
&times;
</button>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</div>
</div>
<div class="col-md-5">
<!-- Recommended extras -->
<div class="suggested-products-container">
<h4 class="section-header">Recommended</h4>
<div class="scroll-panel">
<div
id="suggestedPlugins"
class="suggested-products suggested-plugins"
data-handler="onGetPopularPlugins"
data-view="plugin/suggestion"></div>
</div>
</div>
</div>
</div>
</div>
<script type="text/template" data-partial="plugin/suggestion">
<div class="product">
<a
data-control="popup"
data-handler="onInstallPlugin"
data-request-data="code: '{{code}}'"
href="javascript:;">
<div class="image"><img src="{{image}}" alt=""></div>
<div class="details">
<h5 class="text-overflow">{{code}}</h5>
<p>{{description}}</p>
</div>
</a>
</div>
</script>

View File

@ -0,0 +1,104 @@
<div>
<!-- Search -->
<form
id="installThemesForm"
data-handler="onInstallTheme"
onsubmit="$.oc.installProcess.searchSubmit(this); return false">
<div class="product-search">
<input
name="code"
id="themeSearchInput"
class="product-search-input search-input-lg typeahead"
placeholder="search themes to install..."
data-search-type="themes"
/>
</div>
</form>
<div class="row">
<div class="col-md-7">
<!-- Installed themes -->
<div id="themeList"
class="product-list-manager"
data-handler="onGetInstalledThemes"
data-view="product/theme">
<h4 class="section-header">
<a href="<?= Backend::url('cms/themes') ?>">Installed themes</a>
<small>(<span class="product-counter"><?= count($installedThemes) ?></span>)</small>
</h4>
<?php if (!count($installedThemes)): ?>
<div class="product-list-empty">
<p>There are no themes installed from the marketplace.</p>
</div>
<?php else: ?>
<ul class="product-list theme-list">
<?php foreach ($installedThemes as $theme): ?>
<li data-code="<?= $theme['code'] ?>">
<div class="image">
<img src="<?= $theme['image'] ?>" alt="">
</div>
<div class="details">
<h4><?= $theme['name'] ?></h4>
<p>by <?= $theme['author'] ?></p>
</div>
<button
type="button"
class="close"
aria-hidden="true"
data-request="onRemoveTheme"
data-request-data="code: '<?= $theme['dirName'] ?>'"
data-request-confirm="Are you sure you want to remove this?"
data-stripe-load-indicator>
&times;
</button>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</div>
</div>
<div class="col-md-5">
<!-- Recommended extras -->
<div class="suggested-products-container">
<h4 class="section-header">Recommended</h4>
<div class="scroll-panel">
<div
id="suggestedThemes"
class="suggested-products suggested-themes"
data-handler="onGetPopularThemes"
data-view="theme/suggestion"></div>
</div>
</div>
</div>
</div>
</div>
<script type="text/template" data-partial="theme/suggestion">
<div class="product">
<a
data-control="popup"
data-handler="onInstallTheme"
data-request-data="code: '{{code}}'"
href="javascript:;">
<div class="image"><img src="{{image}}" alt=""></div>
<div class="details">
<h5 class="text-overflow">{{code}}</h5>
<p>{{description}}</p>
</div>
</a>
</div>
</script>

View File

@ -1,24 +0,0 @@
<?= Form::open() ?>
<div class="input-group" style="width: 207px">
<input
placeholder="<?= e(trans('backend::lang.list.search_prompt')) ?>"
type="text"
name="code"
value=""
class="form-control icon plus growable pull-right"
autocomplete="off" />
<span class="input-group-btn">
<button
type="submit"
class="btn btn-success"
id="installPluginButton"
data-control="popup"
data-handler="onInstallPlugin">
<?= e(trans('system::lang.install.plugin_label')) ?>
</button>
</span>
</div>
<?= Form::close() ?>

View File

@ -11,4 +11,9 @@
class="btn btn-default oc-icon-puzzle-piece">
<?= e(trans('system::lang.plugins.manage')) ?>
</a>
<a
href="<?= Backend::url('system/updates/install') ?>"
class="btn btn-success oc-icon-plus">
Install plugins
</a>
</div>

View File

@ -0,0 +1,47 @@
<?= Form::open(['id' => 'themeForm']) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('system::lang.install.theme_label')) ?></h4>
</div>
<div class="modal-body">
<?php if ($this->fatalError): ?>
<p class="flash-message static error"><?= $fatalError ?></p>
<?php endif ?>
<div class="form-group">
<label for="themeCode"><?= e(trans('system::lang.theme.name.label')) ?></label>
<input
name="code"
type="text"
class="form-control"
id="themeCode"
value="<?= post('code') ?>" />
<p class="help-block"><?= e(trans('system::lang.theme.name.help')) ?></p>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary"
data-dismiss="popup"
data-control="popup"
data-handler="onInstallTheme">
<?= e(trans('system::lang.install.theme_label')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<script>
setTimeout(
function(){ $('#themeCode').select() },
310
)
</script>
<?= Form::close() ?>

View File

@ -9,4 +9,3 @@ noRecordsMessage: backend::lang.list.no_records
toolbar:
buttons: list_toolbar
search: list_search

View File

@ -0,0 +1,49 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('system/updates') ?>"><?= e(trans('system::lang.updates.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="control-tabs content-tabs tabs-flush" data-control="tab">
<ul class="nav nav-tabs">
<li class="<?= $activeTab == 'plugins' ? 'active' : '' ?>">
<a
href="#tabPlugins"
data-tab-url="<?= Backend::url('system/updates/install/plugins') ?>">
Plugins
</a>
</li>
<li class="<?= $activeTab == 'themes' ? 'active' : '' ?>">
<a
href="#tabThemes"
data-tab-url="<?= Backend::url('system/updates/install/themes') ?>">
Themes
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane <?= $activeTab == 'plugins' ? 'active' : '' ?>">
<div class="padded-container">
<?= $this->makePartial('install_plugins') ?>
</div>
</div>
<div class="tab-pane <?= $activeTab == 'themes' ? 'active' : '' ?>">
<div class="padded-container">
<?= $this->makePartial('install_themes') ?>
</div>
</div>
</div>
</div>
<?php else: ?>
<div class="padded-container">
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<p><a href="<?= Backend::url('system/updates') ?>" class="btn btn-default"><?= e(trans('system::lang.settings.return')) ?></a></p>
<p><a href="<?= Backend::url('cms/themes') ?>" class="btn btn-default"><?= e(trans('cms::lang.theme.return')) ?></a></p>
</div>
<?php endif ?>

View File

@ -52,6 +52,12 @@ return [
'my_settings' => 'My Settings'
]
],
'theme' => [
'name' => [
'label' => 'Theme Name',
'help' => 'Name the theme by its unique code. For example, RainLab.Vanilla'
]
],
'plugin' => [
'unnamed' => 'Unnamed plugin',
'name' => [
@ -156,7 +162,9 @@ return [
'install' => [
'project_label' => 'Attach to Project',
'plugin_label' => 'Install Plugin',
'theme_label' => 'Install Theme',
'missing_plugin_name' => 'Please specify a Plugin name to install.',
'missing_theme_name' => 'Please specify a Theme name to install.',
'install_completing' => 'Finishing installation process',
'install_success' => 'The plugin has been installed successfully.'
],
@ -254,9 +262,11 @@ return [
'name' => 'System',
'manage_system_settings' => 'Manage system settings',
'manage_software_updates' => 'Manage software updates',
'access_logs' => 'View system logs',
'manage_mail_templates' => 'Manage mail templates',
'manage_mail_settings' => 'Manage mail settings',
'manage_other_administrators' => 'Manage other administrators',
'view_the_dashboard' => 'View the dashboard'
'view_the_dashboard' => 'View the dashboard',
'manage_branding' => 'Customize the back-end'
]
];

View File

@ -13,48 +13,48 @@ return array(
|
*/
"accepted" => "O :attribute deve ser aceito.",
"active_url" => "O :attribute não é uma URL válida.",
"after" => "O :attribute deve ser uma data após :date.",
"alpha" => "O :attribute só pode conter letras.",
"alpha_dash" => "O :attribute só pode conter letras, números e traços.",
"alpha_num" => "O :attribute só pode conter letras e números.",
"array" => "O :attribute deve ser uma matriz.",
"before" => "O :attribute deve ser uma data antes :date.",
"accepted" => ":attribute deve ser aceito.",
"active_url" => ":attribute não é uma URL válida.",
"after" => ":attribute deve ser uma data após :date.",
"alpha" => ":attribute só pode conter letras.",
"alpha_dash" => ":attribute só pode conter letras, números e traços.",
"alpha_num" => ":attribute só pode conter letras e números.",
"array" => ":attribute deve ser uma matriz.",
"before" => ":attribute deve ser uma data antes de :date.",
"between" => array(
"numeric" => "O :attribute deve situar-se entre :min - :max.",
"file" => "O :attribute deve situar-se entre :min - :max kilobytes.",
"string" => "O :attribute deve situar-se entre :min - :max carácteres.",
"array" => "O :attribute tem de ter entre :min - :max itens.",
"numeric" => ":attribute deve possuir entre :min - :max.",
"file" => ":attribute deve possuir entre :min - :max kilobytes.",
"string" => ":attribute deve possuir entre :min - :max carácteres.",
"array" => ":attribute tem de ter entre :min - :max itens.",
),
"confirmed" => "O :attribute confirmação não corresponde.",
"date" => "O :attribute não é uma data válida.",
"date_format" => "O :attribute não coincide com o formato :format.",
"different" => "O :attribute e :other deve ser diferente.",
"digits" => "O :attribute deve ser :digits dígitos.",
"digits_between" => "O :attribute deve situar-se entre :min e :max dígitos.",
"email" => "O :attribute formato é inválido.",
"exists" => "O :attribute selecioado é inválido.",
"image" => "O :attribute deve ser uma imagem.",
"in" => "O :attribute selecioado é inválido.",
"integer" => "O :attribute deve ser um número inteiro.",
"ip" => "O :attribute deve ser um endereço IP válido.",
"confirmed" => "A confirmação de :attribute não corresponde.",
"date" => ":attribute não é uma data válida.",
"date_format" => ":attribute não coincide com o formato :format.",
"different" => ":attribute e :other devem ser diferentes.",
"digits" => ":attribute deve ter :digits dígitos.",
"digits_between" => ":attribute deve possuir entre :min e :max dígitos.",
"email" => "O formato de :attribute é inválido.",
"exists" => ":attribute selecioado é inválido.",
"image" => ":attribute deve ser uma imagem.",
"in" => ":attribute selecioado é inválido.",
"integer" => ":attribute deve ser um número inteiro.",
"ip" => ":attribute deve ser um endereço IP válido.",
"max" => array(
"numeric" => "O :attribute não pode ser maior do que :max.",
"file" => "O :attribute não pode ser maior do que :max kilobytes.",
"string" => "O :attribute não pode ser maior do que :max carácteres.",
"array" => "O :attribute não podem ter mais do que :max itens.",
"numeric" => ":attribute não pode ser maior do que :max.",
"file" => ":attribute não pode ser maior do que :max kilobytes.",
"string" => ":attribute não pode ser maior do que :max carácteres.",
"array" => ":attribute não podem ter mais do que :max itens.",
),
"mimes" => "O :attribute deve ser um arquivo do tipo: :values.",
"min" => array(
"numeric" => "O :attribute deve ser de pelo menos :min.",
"file" => "O :attribute deve ser de pelo menos :min kilobytes.",
"string" => "O :attribute deve ser de pelo menos :min carácteres.",
"array" => "O :attribute deve ter pelo menos :min itens.",
"numeric" => ":attribute deve ser de pelo menos :min.",
"file" => ":attribute deve ser de pelo menos :min kilobytes.",
"string" => ":attribute deve ser de pelo menos :min carácteres.",
"array" => ":attribute deve ter pelo menos :min itens.",
),
"not_in" => "O :attribute selecionado é inválido.",
"numeric" => "O :attribute deve ser um número.",
"regex" => "O :attribute formato é inválido.",
"not_in" => ":attribute selecionado é inválido.",
"numeric" => ":attribute deve ser um número.",
"regex" => ":attribute formato é inválido.",
"required" => "O campo :attribute é obrigatório.",
"required_if" => "O campo :attribute é obrigatório quando :other é :value.",
"required_with" => "O campo :attribute é obrigatório quando :values está presente.",

View File

@ -13,20 +13,23 @@ return [
'fa' => 'Persian',
'fr' => 'French',
'hu' => 'Hungarian',
'id' => 'Bahasa Indonesia',
'it' => 'Italian',
'ja' => 'Japanese',
'nl' => 'Dutch',
'pl' => 'Polish',
'pt-br' => 'Brazilian Portuguese',
'ro' => 'Romanian',
'ru' => 'Russian',
'se' => 'Swedish',
'tr' => 'Turkish',
'sk' => 'Slovak (Slovakia)',
'tr' => 'Turkish'
],
'directory' => [
'create_fail' => "Невозможно создать директорию: :name",
'create_fail' => 'Невозможно создать директорию: :name',
],
'file' => [
'create_fail' => "Невозможно создать файл: :name",
'create_fail' => 'Невозможно создать файл: :name',
],
'combiner' => [
'not_found' => "Сборщик ресурсов не может найти файл ':name'.",
@ -66,13 +69,13 @@ return [
'disabled_help' => 'Отключенные плагины будут игнорироваться.',
'selected_amount' => 'Выбрано плагинов: :amount',
'remove_confirm' => 'Вы уверены?',
'remove_success' => "Выбранные плагины успешно удалены.",
'remove_success' => 'Выбранные плагины успешно удалены.',
'refresh_confirm' => 'Вы уверены?',
'refresh_success' => "Выбранные плагины успешно обновлены.",
'refresh_success' => 'Выбранные плагины успешно обновлены.',
'disable_confirm' => 'Вы уверены?',
'disable_success' => "Плагин успешно отключен.",
'enable_success' => "Плагин успешно включен.",
'unknown_plugin' => "Плагин был удален из файловой системы.",
'disable_success' => 'Плагин успешно отключен.',
'enable_success' => 'Плагин успешно включен.',
'unknown_plugin' => 'Плагин был удален из файловой системы.',
],
'project' => [
'name' => 'Проект',
@ -114,6 +117,7 @@ return [
'smtp_password' => 'SMTP пароль',
'smtp_port' => 'SMTP порт',
'smtp_ssl' => 'Использовать SSL',
'sendmail' => 'Sendmail',
'sendmail_path' => 'Sendmail Путь',
'sendmail_path_comment' => 'Пожалуйста, укажите путь к sendmail.',
'mailgun' => 'Mailgun',
@ -121,6 +125,9 @@ return [
'mailgun_domain_comment' => 'Пожалуйста, укажите Mailgun домен.',
'mailgun_secret' => 'Секретный API-ключ',
'mailgun_secret_comment' => 'Введите ваш Mailgun API-ключ.',
'mandrill' => 'Mandrill',
'mandrill_secret' => 'Секретный ключ Mandrill',
'mandrill_secret_comment' => 'Введите ваш Mandrill API-ключ.'
],
'mail_templates' => [
'menu_label' => 'Шаблоны почты',
@ -170,6 +177,8 @@ return [
'core_build_new_help' => 'Последняя доступная сборка.',
'core_downloading' => 'Загрузка файлов приложения',
'core_extracting' => 'Распаковка файлов приложения',
'plugins' => 'Плагины',
'disabled' => 'Отключено',
'plugin_downloading' => 'Загрузка плагина: :name',
'plugin_extracting' => 'Распаковка плагина: :name',
'plugin_version_none' => 'Новый плагин',
@ -207,7 +216,7 @@ return [
],
'config' => [
'not_found' => 'Не удалось найти конфигурационный файл :file, ожидаемый для :location.',
'required' => 'Для конфигурации, используемой в :location не указано свойство $:property.',
'required' => "Для конфигурации, используемой в :location не указано свойство ':property'.",
],
'zip' => [
'extract_failed' => "Невозможно извлечь файл ':file'.",
@ -242,9 +251,11 @@ return [
'status_code' => 'Статус',
],
'permissions' => [
'name' => 'Система',
'manage_system_settings' => 'Настройка системных параметров',
'manage_software_updates' => 'Управлять обновлениями',
'manage_mail_templates' => 'Управление почтовыми шаблонами',
'manage_mail_settings' => 'Управление настройками почты',
'manage_other_administrators' => 'Управление другими администраторами',
'view_the_dashboard' => 'Просмотр приборной панели'
]

View File

@ -7,17 +7,23 @@ return [
],
'locale' => [
'en' => 'English',
'nl' => 'Dutch',
'ja' => 'Japanese',
'se' => 'Swedish',
'tr' => 'Turkish',
'de' => 'German',
'ru' => 'Russian',
'fr' => 'French',
'it' => 'Italian',
'ro' => 'Romana',
'pt-br' => 'Brazilian Portuguese',
'es' => 'Spanish',
'es-ar' => 'Spanish (Argentina)',
'fa' => 'Persian',
'fr' => 'French',
'hu' => 'Hungarian',
'id' => 'Bahasa Indonesia',
'it' => 'Italian',
'ja' => 'Japanese',
'nl' => 'Dutch',
'pl' => 'Polish',
'pt-br' => 'Brazilian Portuguese',
'ro' => 'Romanian',
'ru' => 'Russian',
'se' => 'Swedish',
'sk' => 'Slovak (Slovakia)',
'tr' => 'Turkish'
],
'directory' => [
'create_fail' => "Kunde inte skapa mapp: :name",
@ -31,6 +37,20 @@ return [
'system' => [
'name' => 'System',
'menu_label' => 'System',
'categories' => [
'cms' => 'CMS',
'misc' => 'Övrigt',
'logs' => 'Loggar',
'mail' => 'Mail',
'shop' => 'Affär',
'team' => 'Lag',
'users' => 'Användare',
'system' => 'System',
'social' => 'Social',
'events' => 'Händelser',
'customers' => 'Kunder',
'my_settings' => 'Mina inställningar'
]
],
'plugin' => [
'unnamed' => 'Namnlöst plugin',
@ -39,6 +59,24 @@ return [
'help' => 'Namnge pluginet efter dess unika kod. Exempelvis RainLab.Blog',
],
],
'plugins' => [
'manage' => 'Hantera pluginerna',
'enable_or_disable' => 'Aktivera eller inaktivera',
'enable_or_disable_title' => 'Aktivera eller inaktivera pluginerna',
'remove' => 'Ta bort',
'refresh' => 'Uppdatera',
'disabled_label' => 'Avaktivera',
'disabled_help' => 'Pluginerna som är avaktiverade är igorerade av appliationen.',
'selected_amount' => 'Markerade plugins: :amount',
'remove_confirm' => 'Är du säker?',
'remove_success' => 'Lyckades bort dessa plugins från systemet.',
'refresh_confirm' => 'Är du säker?',
'refresh_success' => 'Lyckades uppdatera dessa plugins från systemet.',
'disable_confirm' => 'Är du säker?',
'disable_success' => 'Lyckades avaktivera dessa plugins från systemet.',
'enable_success' => 'Lyckades aktivera dessa plugins.',
'unknown_plugin' => 'Pluginen har blivit borttagen från systemet.'
],
'project' => [
'name' => 'Projekt',
'owner_label' => 'Ägare',
@ -55,17 +93,22 @@ return [
],
'settings' => [
'menu_label' => 'Inställningar',
'not_found' => 'Det går inte att hitta de angivna inställningarna.',
'missing_model' => 'Inställningssidan saknar en modell-definition',
'update_success' => 'Inställningar för :name har uppdaterats',
'return' => 'Återgå till systeminställningar',
'search' => 'Sök'
],
'email' => [
'mail' => [
'log_file' => 'Logfiler',
'menu_label' => 'Epost-konfiguration',
'menu_description' => 'Hantera epost-inställningar',
'general' => 'Generellt',
'method' => 'Email-metod',
'sender_name' => 'Avsändarnamn',
'sender_email' => 'Avsändarens epost-adress',
'php_mail' => 'PHP mail',
'sendmail' => 'Sendmail',
'smtp' => 'SMTP',
'smtp_address' => 'SMTP-adress',
'smtp_authorization' => 'SMTP-autentisering krävs',
@ -77,6 +120,38 @@ return [
'sendmail' => 'Sendmail',
'sendmail_path' => 'Sendmail-sökväg',
'sendmail_path_comment' => 'Var god ange sökvägen till sendmail',
'mailgun' => 'Mailgun',
'mailgun_domain' => 'Mailgun domän',
'mailgun_domain_comment' => 'Vänligen ange Mailgun domännamnet.',
'mailgun_secret' => 'Mailgun hemlighet',
'mailgun_secret_comment' => 'Ange din Mailgun API nyckel.',
'mandrill' => 'Mandrill',
'mandrill_secret' => 'Mandrill hemlighet',
'mandrill_secret_comment' => 'Ange din API nyckel.'
],
'mail_templates' => [
'menu_label' => 'Epost mall',
'menu_description' => 'Ändra epost-mallar som skickas till användare och administratörer, hantera epost-utseende.',
'new_template' => 'Ny mall',
'new_layout' => 'Ny utseende',
'template' => 'Mall',
'templates' => 'Mallar',
'menu_layouts_label' => 'Epost mallar',
'layout' => 'Utseende',
'layouts' => 'Utseenden',
'name' => 'Namn',
'name_comment' => 'Unikt namn för att hänvisa till den här mallen',
'code' => 'Kod',
'code_comment' => 'Unik kod som används för att hänvisa till den här mallen',
'subject' => 'Ärende',
'subject_comment' => 'Ärende till epost meddelandet',
'description' => 'Beskrivning',
'content_html' => 'HTML',
'content_css' => 'CSS',
'content_text' => 'Klartext',
'test_send' => 'Skicka ett testmeddelande',
'test_success' => 'Testmeddelandet har skickats.',
'return' => 'Återvänd till mall-listan'
],
'install' => [
'project_label' => 'Länka till Projekt',
@ -89,7 +164,8 @@ return [
'title' => 'Hantera uppdateringar',
'name' => 'Uppdatera mjukvara',
'menu_label' => 'Uppdateringar',
'check_label' => 'Kontrollera uppdateringar',
'menu_description' => 'Uppdatera systemet, hantera och installera plugins och teman.',
'check_label' => 'Sök efter uppdateringar',
'retry_label' => 'Försök igen',
'plugin_name' => 'Namn',
'plugin_description' => 'Beskrivning',
@ -101,11 +177,17 @@ return [
'core_build_new_help' => 'Senaste build är tillgänglig.',
'core_downloading' => 'Laddar ner applikationsfiler',
'core_extracting' => 'Packar upp applikationsfiler',
'plugins' => 'Plugins',
'disabled' => 'Avaktiverade',
'plugin_downloading' => 'Laddar ner plugin: :name',
'plugin_extracting' => 'Packar upp plugin: :name',
'plugin_version_none' => 'Nytt plugin',
'plugin_version_old' => 'Nuvarande v:version',
'plugin_version_new' => 'v:version',
'theme_label' => 'Tema',
'theme_new_install' => 'Installation av nytt tema.',
'theme_downloading' => 'Ladda ner temat: :name',
'theme_extracting' => 'Packar upp temat: :name',
'update_label' => 'Updatera mjukvara',
'update_completing' => 'Slutför uppdatering',
'update_loading' => 'Laddar tillgängliga uppdateringar...',
@ -114,12 +196,12 @@ return [
'force_label' => 'Tvinga uppdatering',
'found' => [
'label' => 'Hittade nya uppdateringar!',
'help' => 'Klicka på Uppdatera mjukvara för att påbörja uppdateringen.',
'help' => 'Klicka på Updatera mjukvara för att påbörja processen.'
],
'none' => [
'label' => 'Inga uppdateringar',
'help' => 'Inga nya uppdateringar hittades.',
],
'help' => 'Inga nya uppdateringar hittades.'
]
],
'server' => [
'connect_error' => 'Ett fel uppstod vid anslutning till servern.',
@ -139,4 +221,44 @@ return [
'zip' => [
'extract_failed' => "Kunde inte packa upp core-fil ':file'.",
],
'event_log' => [
'hint' => 'Denna logg visar en lista över potentiella fel som uppstår i applikationen, såsom undantag och felsökningsinformation.',
'menu_label' => 'Händelselog',
'menu_description' => 'Visa systemloggmeddelanden med respektive tid och detaljer.',
'empty_link' => 'Töm händelseloggen',
'empty_loading' => 'Tömmer händelselogg...',
'empty_success' => 'Lyckades tömma händelseloggen.',
'return_link' => 'Återvänd till händelseloggen',
'id' => 'ID',
'id_label' => 'Händelse ID',
'created_at' => 'Datum och tid',
'message' => 'Meddelande',
'level' => 'Nivå'
],
'request_log' => [
'hint' => 'Denna loggen visar en lista med förfrågningar från webbläsare som kan kräva uppmärksamhet. Till exempel, om en besökare öppnar en CMS sida som inte kan hittas så skapas en post med statuskoden 404.',
'menu_label' => 'Förgrådan-logg',
'menu_description' => 'Visa otillåtna eller omdirigerade förgrågningar, så som Sidan kunde inte hittas (404).',
'empty_link' => 'Töm förfrågningan-loggen',
'empty_loading' => 'Tömmer förfrågningan-loggen...',
'empty_success' => 'Lyckades tömma förfrågningan-loggen.',
'return_link' => 'Återvänd till förfrågningan-loggen',
'id' => 'ID',
'id_label' => 'Log ID',
'count' => 'Räknare',
'referer' => 'Refererare',
'url' => 'URL',
'status_code' => 'Status'
],
'permissions' => [
'name' => 'System',
'manage_system_settings' => 'Hantera system inställningar',
'manage_software_updates' => 'Hantera mjukvaruuppdateringar',
'access_logs' => 'Visa system loggen',
'manage_mail_templates' => 'Hantera epost-mallar',
'manage_mail_settings' => 'Hantera epost inställningar',
'manage_other_administrators' => 'Hantera andra administratörer',
'view_the_dashboard' => 'Visa the kontrollpanelen',
'manage_branding' => 'Anpassa back-end'
]
];

View File

@ -4,20 +4,20 @@
columns:
name:
label: system::lang.updates.plugin_name
sortable: false
name:
label: system::lang.updates.plugin_name
sortable: false
description:
label: system::lang.updates.plugin_description
sortable: false
description:
label: system::lang.updates.plugin_description
sortable: false
version:
label: system::lang.updates.plugin_version
sortable: false
version:
label: system::lang.updates.plugin_version
sortable: false
author:
label: system::lang.updates.plugin_author
sortable: false
type: partial
path: column_author
author:
label: system::lang.updates.plugin_author
sortable: false
type: partial
path: column_author

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: affix.js v3.1.0
* Bootstrap: affix.js v3.3.4
* http://getbootstrap.com/javascript/#affix
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -15,28 +15,54 @@
var Affix = function (element, options) {
this.options = $.extend({}, Affix.DEFAULTS, options)
this.$window = $(window)
this.$target = $(this.options.target)
.on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
.on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
this.$element = $(element)
this.affixed =
this.unpin =
this.affixed = null
this.unpin = null
this.pinnedOffset = null
this.checkPosition()
}
Affix.RESET = 'affix affix-top affix-bottom'
Affix.VERSION = '3.3.4'
Affix.RESET = 'affix affix-top affix-bottom'
Affix.DEFAULTS = {
offset: 0
offset: 0,
target: window
}
Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {
var scrollTop = this.$target.scrollTop()
var position = this.$element.offset()
var targetHeight = this.$target.height()
if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false
if (this.affixed == 'bottom') {
if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'
return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'
}
var initializing = this.affixed == null
var colliderTop = initializing ? scrollTop : position.top
var colliderHeight = initializing ? targetHeight : height
if (offsetTop != null && scrollTop <= offsetTop) return 'top'
if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'
return false
}
Affix.prototype.getPinnedOffset = function () {
if (this.pinnedOffset) return this.pinnedOffset
this.$element.removeClass(Affix.RESET).addClass('affix')
var scrollTop = this.$window.scrollTop()
var scrollTop = this.$target.scrollTop()
var position = this.$element.offset()
return (this.pinnedOffset = position.top - scrollTop)
}
@ -48,43 +74,41 @@
Affix.prototype.checkPosition = function () {
if (!this.$element.is(':visible')) return
var scrollHeight = $(document).height()
var scrollTop = this.$window.scrollTop()
var position = this.$element.offset()
var height = this.$element.height()
var offset = this.options.offset
var offsetTop = offset.top
var offsetBottom = offset.bottom
if (this.affixed == 'top') position.top += scrollTop
var scrollHeight = $(document.body).height()
if (typeof offset != 'object') offsetBottom = offsetTop = offset
if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)
if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false :
offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' :
offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false
var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)
if (this.affixed === affix) return
if (this.unpin) this.$element.css('top', '')
if (this.affixed != affix) {
if (this.unpin != null) this.$element.css('top', '')
var affixType = 'affix' + (affix ? '-' + affix : '')
var e = $.Event(affixType + '.bs.affix')
var affixType = 'affix' + (affix ? '-' + affix : '')
var e = $.Event(affixType + '.bs.affix')
this.$element.trigger(e)
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
if (e.isDefaultPrevented()) return
this.affixed = affix
this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
this.affixed = affix
this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
this.$element
.removeClass(Affix.RESET)
.addClass(affixType)
.trigger($.Event(affixType.replace('affix', 'affixed')))
this.$element
.removeClass(Affix.RESET)
.addClass(affixType)
.trigger(affixType.replace('affix', 'affixed') + '.bs.affix')
}
if (affix == 'bottom') {
this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() })
this.$element.offset({
top: scrollHeight - height - offsetBottom
})
}
}
@ -92,9 +116,7 @@
// AFFIX PLUGIN DEFINITION
// =======================
var old = $.fn.affix
$.fn.affix = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.affix')
@ -105,6 +127,9 @@
})
}
var old = $.fn.affix
$.fn.affix = Plugin
$.fn.affix.Constructor = Affix
@ -127,10 +152,10 @@
data.offset = data.offset || {}
if (data.offsetBottom) data.offset.bottom = data.offsetBottom
if (data.offsetTop) data.offset.top = data.offsetTop
if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom
if (data.offsetTop != null) data.offset.top = data.offsetTop
$spy.affix(data)
Plugin.call($spy, data)
})
})

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: alert.js v3.1.0
* Bootstrap: alert.js v3.3.4
* http://getbootstrap.com/javascript/#alerts
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -18,6 +18,10 @@
$(el).on('click', dismiss, this.close)
}
Alert.VERSION = '3.3.4'
Alert.TRANSITION_DURATION = 150
Alert.prototype.close = function (e) {
var $this = $(this)
var selector = $this.attr('data-target')
@ -32,7 +36,7 @@
if (e) e.preventDefault()
if (!$parent.length) {
$parent = $this.hasClass('alert') ? $this : $this.parent()
$parent = $this.closest('.alert')
}
$parent.trigger(e = $.Event('close.bs.alert'))
@ -42,13 +46,14 @@
$parent.removeClass('in')
function removeElement() {
$parent.trigger('closed.bs.alert').remove()
// detach from parent, fire event then clean up data
$parent.detach().trigger('closed.bs.alert').remove()
}
$.support.transition && $parent.hasClass('fade') ?
$parent
.one($.support.transition.end, removeElement)
.emulateTransitionEnd(150) :
.one('bsTransitionEnd', removeElement)
.emulateTransitionEnd(Alert.TRANSITION_DURATION) :
removeElement()
}
@ -56,9 +61,7 @@
// ALERT PLUGIN DEFINITION
// =======================
var old = $.fn.alert
$.fn.alert = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.alert')
@ -68,6 +71,9 @@
})
}
var old = $.fn.alert
$.fn.alert = Plugin
$.fn.alert.Constructor = Alert

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: button.js v3.1.0
* Bootstrap: button.js v3.3.4
* http://getbootstrap.com/javascript/#buttons
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -19,6 +19,8 @@
this.isLoading = false
}
Button.VERSION = '3.3.4'
Button.DEFAULTS = {
loadingText: 'loading...'
}
@ -31,12 +33,12 @@
state = state + 'Text'
if (!data.resetText) $el.data('resetText', $el[val]())
$el[val](data[state] || this.options[state])
if (data.resetText == null) $el.data('resetText', $el[val]())
// push to event loop to allow forms to submit
setTimeout($.proxy(function () {
$el[val](data[state] == null ? this.options[state] : data[state])
if (state == 'loadingText') {
this.isLoading = true
$el.addClass(d).attr(d, d)
@ -58,6 +60,8 @@
else $parent.find('.active').removeClass('active')
}
if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
} else {
this.$element.attr('aria-pressed', !this.$element.hasClass('active'))
}
if (changed) this.$element.toggleClass('active')
@ -67,9 +71,7 @@
// BUTTON PLUGIN DEFINITION
// ========================
var old = $.fn.button
$.fn.button = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.button')
@ -82,6 +84,9 @@
})
}
var old = $.fn.button
$.fn.button = Plugin
$.fn.button.Constructor = Button
@ -97,11 +102,15 @@
// BUTTON DATA-API
// ===============
$(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
var $btn = $(e.target)
if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
$btn.button('toggle')
e.preventDefault()
})
$(document)
.on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
var $btn = $(e.target)
if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
Plugin.call($btn, 'toggle')
e.preventDefault()
})
.on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
$(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
})
}(jQuery);

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: carousel.js v3.1.0
* Bootstrap: carousel.js v3.3.4
* http://getbootstrap.com/javascript/#carousel
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -17,24 +17,42 @@
this.$element = $(element)
this.$indicators = this.$element.find('.carousel-indicators')
this.options = options
this.paused =
this.sliding =
this.interval =
this.$active =
this.paused = null
this.sliding = null
this.interval = null
this.$active = null
this.$items = null
this.options.pause == 'hover' && this.$element
.on('mouseenter', $.proxy(this.pause, this))
.on('mouseleave', $.proxy(this.cycle, this))
this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))
this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element
.on('mouseenter.bs.carousel', $.proxy(this.pause, this))
.on('mouseleave.bs.carousel', $.proxy(this.cycle, this))
}
Carousel.VERSION = '3.3.4'
Carousel.TRANSITION_DURATION = 600
Carousel.DEFAULTS = {
interval: 5000,
pause: 'hover',
wrap: true
wrap: true,
keyboard: true
}
Carousel.prototype.cycle = function (e) {
Carousel.prototype.keydown = function (e) {
if (/input|textarea/i.test(e.target.tagName)) return
switch (e.which) {
case 37: this.prev(); break
case 39: this.next(); break
default: return
}
e.preventDefault()
}
Carousel.prototype.cycle = function (e) {
e || (this.paused = false)
this.interval && clearInterval(this.interval)
@ -46,23 +64,31 @@
return this
}
Carousel.prototype.getActiveIndex = function () {
this.$active = this.$element.find('.item.active')
this.$items = this.$active.parent().children()
Carousel.prototype.getItemIndex = function (item) {
this.$items = item.parent().children('.item')
return this.$items.index(item || this.$active)
}
return this.$items.index(this.$active)
Carousel.prototype.getItemForDirection = function (direction, active) {
var activeIndex = this.getItemIndex(active)
var willWrap = (direction == 'prev' && activeIndex === 0)
|| (direction == 'next' && activeIndex == (this.$items.length - 1))
if (willWrap && !this.options.wrap) return active
var delta = direction == 'prev' ? -1 : 1
var itemIndex = (activeIndex + delta) % this.$items.length
return this.$items.eq(itemIndex)
}
Carousel.prototype.to = function (pos) {
var that = this
var activeIndex = this.getActiveIndex()
var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active'))
if (pos > (this.$items.length - 1) || pos < 0) return
if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) })
if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid"
if (activeIndex == pos) return this.pause().cycle()
return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos))
}
Carousel.prototype.pause = function (e) {
@ -90,22 +116,20 @@
Carousel.prototype.slide = function (type, next) {
var $active = this.$element.find('.item.active')
var $next = next || $active[type]()
var $next = next || this.getItemForDirection(type, $active)
var isCycling = this.interval
var direction = type == 'next' ? 'left' : 'right'
var fallback = type == 'next' ? 'first' : 'last'
var that = this
if (!$next.length) {
if (!this.options.wrap) return
$next = this.$element.find('.item')[fallback]()
}
if ($next.hasClass('active')) return (this.sliding = false)
if ($next.hasClass('active')) return this.sliding = false
var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
var relatedTarget = $next[0]
var slideEvent = $.Event('slide.bs.carousel', {
relatedTarget: relatedTarget,
direction: direction
})
this.$element.trigger(slideEvent)
if (slideEvent.isDefaultPrevented()) return
this.sliding = true
@ -113,30 +137,31 @@
if (this.$indicators.length) {
this.$indicators.find('.active').removeClass('active')
this.$element.one('slid.bs.carousel', function () {
var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
$nextIndicator && $nextIndicator.addClass('active')
})
var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)])
$nextIndicator && $nextIndicator.addClass('active')
}
var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid"
if ($.support.transition && this.$element.hasClass('slide')) {
$next.addClass(type)
$next[0].offsetWidth // force reflow
$active.addClass(direction)
$next.addClass(direction)
$active
.one($.support.transition.end, function () {
.one('bsTransitionEnd', function () {
$next.removeClass([type, direction].join(' ')).addClass('active')
$active.removeClass(['active', direction].join(' '))
that.sliding = false
setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0)
setTimeout(function () {
that.$element.trigger(slidEvent)
}, 0)
})
.emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000)
.emulateTransitionEnd(Carousel.TRANSITION_DURATION)
} else {
$active.removeClass('active')
$next.addClass('active')
this.sliding = false
this.$element.trigger('slid.bs.carousel')
this.$element.trigger(slidEvent)
}
isCycling && this.cycle()
@ -148,9 +173,7 @@
// CAROUSEL PLUGIN DEFINITION
// ==========================
var old = $.fn.carousel
$.fn.carousel = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.carousel')
@ -164,6 +187,9 @@
})
}
var old = $.fn.carousel
$.fn.carousel = Plugin
$.fn.carousel.Constructor = Carousel
@ -179,26 +205,32 @@
// CAROUSEL DATA-API
// =================
$(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
var $this = $(this), href
var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
var clickHandler = function (e) {
var href
var $this = $(this)
var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
if (!$target.hasClass('carousel')) return
var options = $.extend({}, $target.data(), $this.data())
var slideIndex = $this.attr('data-slide-to')
if (slideIndex) options.interval = false
$target.carousel(options)
Plugin.call($target, options)
if (slideIndex = $this.attr('data-slide-to')) {
if (slideIndex) {
$target.data('bs.carousel').to(slideIndex)
}
e.preventDefault()
})
}
$(document)
.on('click.bs.carousel.data-api', '[data-slide]', clickHandler)
.on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler)
$(window).on('load', function () {
$('[data-ride="carousel"]').each(function () {
var $carousel = $(this)
$carousel.carousel($carousel.data())
Plugin.call($carousel, $carousel.data())
})
})

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: collapse.js v3.1.0
* Bootstrap: collapse.js v3.3.4
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -16,12 +16,23 @@
var Collapse = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Collapse.DEFAULTS, options)
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
this.transitioning = null
if (this.options.parent) this.$parent = $(this.options.parent)
if (this.options.parent) {
this.$parent = this.getParent()
} else {
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
}
if (this.options.toggle) this.toggle()
}
Collapse.VERSION = '3.3.4'
Collapse.TRANSITION_DURATION = 350
Collapse.DEFAULTS = {
toggle: true
}
@ -34,35 +45,43 @@
Collapse.prototype.show = function () {
if (this.transitioning || this.$element.hasClass('in')) return
var activesData
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
if (actives && actives.length) {
activesData = actives.data('bs.collapse')
if (activesData && activesData.transitioning) return
}
var startEvent = $.Event('show.bs.collapse')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) return
var actives = this.$parent && this.$parent.find('> .panel > .in')
if (actives && actives.length) {
var hasData = actives.data('bs.collapse')
if (hasData && hasData.transitioning) return
actives.collapse('hide')
hasData || actives.data('bs.collapse', null)
Plugin.call(actives, 'hide')
activesData || actives.data('bs.collapse', null)
}
var dimension = this.dimension()
this.$element
.removeClass('collapse')
.addClass('collapsing')
[dimension](0)
.addClass('collapsing')[dimension](0)
.attr('aria-expanded', true)
this.$trigger
.removeClass('collapsed')
.attr('aria-expanded', true)
this.transitioning = 1
var complete = function () {
this.$element
.removeClass('collapsing')
.addClass('collapse in')
[dimension]('auto')
.addClass('collapse in')[dimension]('')
this.transitioning = 0
this.$element.trigger('shown.bs.collapse')
this.$element
.trigger('shown.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
@ -70,9 +89,8 @@
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
this.$element
.one($.support.transition.end, $.proxy(complete, this))
.emulateTransitionEnd(350)
[dimension](this.$element[0][scrollSize])
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
}
Collapse.prototype.hide = function () {
@ -84,55 +102,85 @@
var dimension = this.dimension()
this.$element
[dimension](this.$element[dimension]())
[0].offsetHeight
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
this.$element
.addClass('collapsing')
.removeClass('collapse')
.removeClass('in')
.removeClass('collapse in')
.attr('aria-expanded', false)
this.$trigger
.addClass('collapsed')
.attr('aria-expanded', false)
this.transitioning = 1
var complete = function () {
this.transitioning = 0
this.$element
.trigger('hidden.bs.collapse')
.removeClass('collapsing')
.addClass('collapse')
.trigger('hidden.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
this.$element
[dimension](0)
.one($.support.transition.end, $.proxy(complete, this))
.emulateTransitionEnd(350)
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
}
Collapse.prototype.toggle = function () {
this[this.$element.hasClass('in') ? 'hide' : 'show']()
}
Collapse.prototype.getParent = function () {
return $(this.options.parent)
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
.each($.proxy(function (i, element) {
var $element = $(element)
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
}, this))
.end()
}
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
var isOpen = $element.hasClass('in')
$element.attr('aria-expanded', isOpen)
$trigger
.toggleClass('collapsed', !isOpen)
.attr('aria-expanded', isOpen)
}
function getTargetFromTrigger($trigger) {
var href
var target = $trigger.attr('data-target')
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
return $(target)
}
// COLLAPSE PLUGIN DEFINITION
// ==========================
var old = $.fn.collapse
$.fn.collapse = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.collapse')
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data && options.toggle && option == 'show') option = !option
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.collapse
$.fn.collapse = Plugin
$.fn.collapse.Constructor = Collapse
@ -148,23 +196,16 @@
// COLLAPSE DATA-API
// =================
$(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) {
var $this = $(this), href
var target = $this.attr('data-target')
|| e.preventDefault()
|| (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
var $target = $(target)
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
var $this = $(this)
if (!$this.attr('data-target')) e.preventDefault()
var $target = getTargetFromTrigger($this)
var data = $target.data('bs.collapse')
var option = data ? 'toggle' : $this.data()
var parent = $this.attr('data-parent')
var $parent = parent && $(parent)
if (!data || !data.transitioning) {
if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed')
$this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
}
$target.collapse(option)
Plugin.call($target, option)
})
}(jQuery);

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: dropdown.js v3.1.0
* Bootstrap: dropdown.js v3.3.4
* http://getbootstrap.com/javascript/#dropdowns
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -14,11 +14,13 @@
// =========================
var backdrop = '.dropdown-backdrop'
var toggle = '[data-toggle=dropdown]'
var toggle = '[data-toggle="dropdown"]'
var Dropdown = function (element) {
$(element).on('click.bs.dropdown', this.toggle)
}
Dropdown.VERSION = '3.3.4'
Dropdown.prototype.toggle = function (e) {
var $this = $(this)
@ -40,18 +42,20 @@
if (e.isDefaultPrevented()) return
$this
.trigger('focus')
.attr('aria-expanded', 'true')
$parent
.toggleClass('open')
.trigger('shown.bs.dropdown', relatedTarget)
$this.focus()
}
return false
}
Dropdown.prototype.keydown = function (e) {
if (!/(38|40|27)/.test(e.keyCode)) return
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
var $this = $(this)
@ -63,33 +67,40 @@
var $parent = getParent($this)
var isActive = $parent.hasClass('open')
if (!isActive || (isActive && e.keyCode == 27)) {
if (e.which == 27) $parent.find(toggle).focus()
return $this.click()
if ((!isActive && e.which != 27) || (isActive && e.which == 27)) {
if (e.which == 27) $parent.find(toggle).trigger('focus')
return $this.trigger('click')
}
var desc = ' li:not(.divider):visible a'
var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc)
var desc = ' li:not(.disabled):visible a'
var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc)
if (!$items.length) return
var index = $items.index($items.filter(':focus'))
var index = $items.index(e.target)
if (e.keyCode == 38 && index > 0) index-- // up
if (e.keyCode == 40 && index < $items.length - 1) index++ // down
if (e.which == 38 && index > 0) index-- // up
if (e.which == 40 && index < $items.length - 1) index++ // down
if (!~index) index = 0
$items.eq(index).focus()
$items.eq(index).trigger('focus')
}
function clearMenus(e) {
if (e && e.which === 3) return
$(backdrop).remove()
$(toggle).each(function () {
var $parent = getParent($(this))
var $this = $(this)
var $parent = getParent($this)
var relatedTarget = { relatedTarget: this }
if (!$parent.hasClass('open')) return
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
$this.attr('aria-expanded', 'false')
$parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
})
}
@ -99,7 +110,7 @@
if (!selector) {
selector = $this.attr('href')
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = selector && $(selector)
@ -111,9 +122,7 @@
// DROPDOWN PLUGIN DEFINITION
// ==========================
var old = $.fn.dropdown
$.fn.dropdown = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.dropdown')
@ -123,6 +132,9 @@
})
}
var old = $.fn.dropdown
$.fn.dropdown = Plugin
$.fn.dropdown.Constructor = Dropdown
@ -142,6 +154,8 @@
.on('click.bs.dropdown.data-api', clearMenus)
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
.on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]', Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown)
}(jQuery);

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: modal.js v3.1.0
* Bootstrap: modal.js v3.3.4
* http://getbootstrap.com/javascript/#modals
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -14,10 +14,15 @@
// ======================
var Modal = function (element, options) {
this.options = options
this.$element = $(element)
this.$backdrop =
this.isShown = null
this.options = options
this.$body = $(document.body)
this.$element = $(element)
this.$dialog = this.$element.find('.modal-dialog')
this.$backdrop = null
this.isShown = null
this.originalBodyPad = null
this.scrollbarWidth = 0
this.ignoreBackdropClick = false
if (this.options.remote) {
this.$element
@ -28,6 +33,11 @@
}
}
Modal.VERSION = '3.3.4'
Modal.TRANSITION_DURATION = 300
Modal.BACKDROP_TRANSITION_DURATION = 150
Modal.DEFAULTS = {
backdrop: true,
keyboard: true,
@ -35,7 +45,7 @@
}
Modal.prototype.toggle = function (_relatedTarget) {
return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
return this.isShown ? this.hide() : this.show(_relatedTarget)
}
Modal.prototype.show = function (_relatedTarget) {
@ -48,21 +58,34 @@
this.isShown = true
this.checkScrollbar()
this.setScrollbar()
this.$body.addClass('modal-open')
this.escape()
this.resize()
this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
this.$dialog.on('mousedown.dismiss.bs.modal', function () {
that.$element.one('mouseup.dismiss.bs.modal', function (e) {
if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true
})
})
this.backdrop(function () {
var transition = $.support.transition && that.$element.hasClass('fade')
if (!that.$element.parent().length) {
that.$element.appendTo(document.body) // don't move modals dom position
that.$element.appendTo(that.$body) // don't move modals dom position
}
that.$element
.show()
.scrollTop(0)
that.adjustDialog()
if (transition) {
that.$element[0].offsetWidth // force reflow
}
@ -76,12 +99,12 @@
var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
transition ?
that.$element.find('.modal-dialog') // wait for modal to slide in
.one($.support.transition.end, function () {
that.$element.focus().trigger(e)
that.$dialog // wait for modal to slide in
.one('bsTransitionEnd', function () {
that.$element.trigger('focus').trigger(e)
})
.emulateTransitionEnd(300) :
that.$element.focus().trigger(e)
.emulateTransitionEnd(Modal.TRANSITION_DURATION) :
that.$element.trigger('focus').trigger(e)
})
}
@ -97,6 +120,7 @@
this.isShown = false
this.escape()
this.resize()
$(document).off('focusin.bs.modal')
@ -104,11 +128,14 @@
.removeClass('in')
.attr('aria-hidden', true)
.off('click.dismiss.bs.modal')
.off('mouseup.dismiss.bs.modal')
this.$dialog.off('mousedown.dismiss.bs.modal')
$.support.transition && this.$element.hasClass('fade') ?
this.$element
.one($.support.transition.end, $.proxy(this.hideModal, this))
.emulateTransitionEnd(300) :
.one('bsTransitionEnd', $.proxy(this.hideModal, this))
.emulateTransitionEnd(Modal.TRANSITION_DURATION) :
this.hideModal()
}
@ -117,18 +144,26 @@
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function (e) {
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
this.$element.focus()
this.$element.trigger('focus')
}
}, this))
}
Modal.prototype.escape = function () {
if (this.isShown && this.options.keyboard) {
this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
e.which == 27 && this.hide()
}, this))
} else if (!this.isShown) {
this.$element.off('keyup.dismiss.bs.modal')
this.$element.off('keydown.dismiss.bs.modal')
}
}
Modal.prototype.resize = function () {
if (this.isShown) {
$(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))
} else {
$(window).off('resize.bs.modal')
}
}
@ -136,7 +171,9 @@
var that = this
this.$element.hide()
this.backdrop(function () {
that.removeBackdrop()
that.$body.removeClass('modal-open')
that.resetAdjustments()
that.resetScrollbar()
that.$element.trigger('hidden.bs.modal')
})
}
@ -147,19 +184,24 @@
}
Modal.prototype.backdrop = function (callback) {
var that = this
var animate = this.$element.hasClass('fade') ? 'fade' : ''
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
.appendTo(this.$body)
this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
if (this.ignoreBackdropClick) {
this.ignoreBackdropClick = false
return
}
if (e.target !== e.currentTarget) return
this.options.backdrop == 'static'
? this.$element[0].focus.call(this.$element[0])
: this.hide.call(this)
? this.$element[0].focus()
: this.hide()
}, this))
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
@ -170,31 +212,84 @@
doAnimate ?
this.$backdrop
.one($.support.transition.end, callback)
.emulateTransitionEnd(150) :
.one('bsTransitionEnd', callback)
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
callback()
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
var callbackRemove = function () {
that.removeBackdrop()
callback && callback()
}
$.support.transition && this.$element.hasClass('fade') ?
this.$backdrop
.one($.support.transition.end, callback)
.emulateTransitionEnd(150) :
callback()
.one('bsTransitionEnd', callbackRemove)
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
callbackRemove()
} else if (callback) {
callback()
}
}
// these following methods are used to handle overflowing modals
Modal.prototype.handleUpdate = function () {
this.adjustDialog()
}
Modal.prototype.adjustDialog = function () {
var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
this.$element.css({
paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
})
}
Modal.prototype.resetAdjustments = function () {
this.$element.css({
paddingLeft: '',
paddingRight: ''
})
}
Modal.prototype.checkScrollbar = function () {
var fullWindowWidth = window.innerWidth
if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
var documentElementRect = document.documentElement.getBoundingClientRect()
fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left)
}
this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth
this.scrollbarWidth = this.measureScrollbar()
}
Modal.prototype.setScrollbar = function () {
var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
this.originalBodyPad = document.body.style.paddingRight || ''
if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
}
Modal.prototype.resetScrollbar = function () {
this.$body.css('padding-right', this.originalBodyPad)
}
Modal.prototype.measureScrollbar = function () { // thx walsh
var scrollDiv = document.createElement('div')
scrollDiv.className = 'modal-scrollbar-measure'
this.$body.append(scrollDiv)
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
this.$body[0].removeChild(scrollDiv)
return scrollbarWidth
}
// MODAL PLUGIN DEFINITION
// =======================
var old = $.fn.modal
$.fn.modal = function (option, _relatedTarget) {
function Plugin(option, _relatedTarget) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.modal')
@ -206,6 +301,9 @@
})
}
var old = $.fn.modal
$.fn.modal = Plugin
$.fn.modal.Constructor = Modal
@ -224,20 +322,18 @@
$(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
var $this = $(this)
var href = $this.attr('href')
var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
if ($this.is('a')) e.preventDefault()
$target
.modal(option, this)
.one('hide', function () {
$this.is(':visible') && $this.focus()
$target.one('show.bs.modal', function (showEvent) {
if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
$target.one('hidden.bs.modal', function () {
$this.is(':visible') && $this.trigger('focus')
})
})
Plugin.call($target, option, this)
})
$(document)
.on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
.on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
}(jQuery);

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: popover.js v3.1.0
* Bootstrap: popover.js v3.3.4
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -19,11 +19,13 @@
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
Popover.VERSION = '3.3.4'
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
placement: 'right',
trigger: 'click',
content: '',
template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
})
@ -44,7 +46,7 @@
var content = this.getContent()
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
$tip.find('.popover-content')[ // we use append for html objects to maintain js events
$tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
](content)
@ -70,32 +72,28 @@
}
Popover.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.arrow')
}
Popover.prototype.tip = function () {
if (!this.$tip) this.$tip = $(this.options.template)
return this.$tip
return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
}
// POPOVER PLUGIN DEFINITION
// =========================
var old = $.fn.popover
$.fn.popover = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data && /destroy|hide/.test(option)) return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.popover
$.fn.popover = Plugin
$.fn.popover.Constructor = Popover

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: scrollspy.js v3.1.0
* Bootstrap: scrollspy.js v3.3.4
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -14,36 +14,45 @@
// ==========================
function ScrollSpy(element, options) {
var href
var process = $.proxy(this.process, this)
this.$element = $(element).is('body') ? $(window) : $(element)
this.$body = $('body')
this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process)
this.$body = $(document.body)
this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)
this.options = $.extend({}, ScrollSpy.DEFAULTS, options)
this.selector = (this.options.target
|| ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
|| '') + ' .nav li > a'
this.offsets = $([])
this.targets = $([])
this.selector = (this.options.target || '') + ' .nav li > a'
this.offsets = []
this.targets = []
this.activeTarget = null
this.scrollHeight = 0
this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))
this.refresh()
this.process()
}
ScrollSpy.VERSION = '3.3.4'
ScrollSpy.DEFAULTS = {
offset: 10
}
ScrollSpy.prototype.getScrollHeight = function () {
return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)
}
ScrollSpy.prototype.refresh = function () {
var offsetMethod = this.$element[0] == window ? 'offset' : 'position'
var that = this
var offsetMethod = 'offset'
var offsetBase = 0
this.offsets = $([])
this.targets = $([])
this.offsets = []
this.targets = []
this.scrollHeight = this.getScrollHeight()
var self = this
var $targets = this.$body
if (!$.isWindow(this.$scrollElement[0])) {
offsetMethod = 'position'
offsetBase = this.$scrollElement.scrollTop()
}
this.$body
.find(this.selector)
.map(function () {
var $el = $(this)
@ -53,50 +62,53 @@
return ($href
&& $href.length
&& $href.is(':visible')
&& [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null
&& [[$href[offsetMethod]().top + offsetBase, href]]) || null
})
.sort(function (a, b) { return a[0] - b[0] })
.each(function () {
self.offsets.push(this[0])
self.targets.push(this[1])
that.offsets.push(this[0])
that.targets.push(this[1])
})
}
ScrollSpy.prototype.process = function () {
var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
var maxScroll = scrollHeight - this.$scrollElement.height()
var scrollHeight = this.getScrollHeight()
var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height()
var offsets = this.offsets
var targets = this.targets
var activeTarget = this.activeTarget
var i
if (scrollTop >= maxScroll) {
return activeTarget != (i = targets.last()[0]) && this.activate(i)
if (this.scrollHeight != scrollHeight) {
this.refresh()
}
if (activeTarget && scrollTop <= offsets[0]) {
return activeTarget != (i = targets[0]) && this.activate(i)
if (scrollTop >= maxScroll) {
return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
}
if (activeTarget && scrollTop < offsets[0]) {
this.activeTarget = null
return this.clear()
}
for (i = offsets.length; i--;) {
activeTarget != targets[i]
&& scrollTop >= offsets[i]
&& (!offsets[i + 1] || scrollTop <= offsets[i + 1])
&& this.activate( targets[i] )
&& (offsets[i + 1] === undefined || scrollTop < offsets[i + 1])
&& this.activate(targets[i])
}
}
ScrollSpy.prototype.activate = function (target) {
this.activeTarget = target
$(this.selector)
.parentsUntil(this.options.target, '.active')
.removeClass('active')
this.clear()
var selector = this.selector +
'[data-target="' + target + '"],' +
this.selector + '[href="' + target + '"]'
'[data-target="' + target + '"],' +
this.selector + '[href="' + target + '"]'
var active = $(selector)
.parents('li')
@ -111,13 +123,17 @@
active.trigger('activate.bs.scrollspy')
}
ScrollSpy.prototype.clear = function () {
$(this.selector)
.parentsUntil(this.options.target, '.active')
.removeClass('active')
}
// SCROLLSPY PLUGIN DEFINITION
// ===========================
var old = $.fn.scrollspy
$.fn.scrollspy = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.scrollspy')
@ -128,6 +144,9 @@
})
}
var old = $.fn.scrollspy
$.fn.scrollspy = Plugin
$.fn.scrollspy.Constructor = ScrollSpy
@ -143,10 +162,10 @@
// SCROLLSPY DATA-API
// ==================
$(window).on('load', function () {
$(window).on('load.bs.scrollspy.data-api', function () {
$('[data-spy="scroll"]').each(function () {
var $spy = $(this)
$spy.scrollspy($spy.data())
Plugin.call($spy, $spy.data())
})
})

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: tab.js v3.1.0
* Bootstrap: tab.js v3.3.4
* http://getbootstrap.com/javascript/#tabs
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -17,6 +17,10 @@
this.element = $(element)
}
Tab.VERSION = '3.3.4'
Tab.TRANSITION_DURATION = 150
Tab.prototype.show = function () {
var $this = this.element
var $ul = $this.closest('ul:not(.dropdown-menu)')
@ -24,27 +28,35 @@
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
if ($this.parent('li').hasClass('active')) return
var previous = $ul.find('.active:last a')[0]
var e = $.Event('show.bs.tab', {
relatedTarget: previous
var $previous = $ul.find('.active:last a')
var hideEvent = $.Event('hide.bs.tab', {
relatedTarget: $this[0]
})
var showEvent = $.Event('show.bs.tab', {
relatedTarget: $previous[0]
})
$this.trigger(e)
$previous.trigger(hideEvent)
$this.trigger(showEvent)
if (e.isDefaultPrevented()) return
if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return
var $target = $(selector)
this.activate($this.parent('li'), $ul)
this.activate($this.closest('li'), $ul)
this.activate($target, $target.parent(), function () {
$previous.trigger({
type: 'hidden.bs.tab',
relatedTarget: $this[0]
})
$this.trigger({
type: 'shown.bs.tab',
relatedTarget: previous
relatedTarget: $previous[0]
})
})
}
@ -53,15 +65,21 @@
var $active = container.find('> .active')
var transition = callback
&& $.support.transition
&& $active.hasClass('fade')
&& (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length)
function next() {
$active
.removeClass('active')
.find('> .dropdown-menu > .active')
.removeClass('active')
.removeClass('active')
.end()
.find('[data-toggle="tab"]')
.attr('aria-expanded', false)
element.addClass('active')
element
.addClass('active')
.find('[data-toggle="tab"]')
.attr('aria-expanded', true)
if (transition) {
element[0].offsetWidth // reflow for transition
@ -70,17 +88,22 @@
element.removeClass('fade')
}
if (element.parent('.dropdown-menu')) {
element.closest('li.dropdown').addClass('active')
if (element.parent('.dropdown-menu').length) {
element
.closest('li.dropdown')
.addClass('active')
.end()
.find('[data-toggle="tab"]')
.attr('aria-expanded', true)
}
callback && callback()
}
transition ?
$active.length && transition ?
$active
.one($.support.transition.end, next)
.emulateTransitionEnd(150) :
.one('bsTransitionEnd', next)
.emulateTransitionEnd(Tab.TRANSITION_DURATION) :
next()
$active.removeClass('in')
@ -90,9 +113,7 @@
// TAB PLUGIN DEFINITION
// =====================
var old = $.fn.tab
$.fn.tab = function ( option ) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tab')
@ -102,6 +123,9 @@
})
}
var old = $.fn.tab
$.fn.tab = Plugin
$.fn.tab.Constructor = Tab
@ -117,9 +141,13 @@
// TAB DATA-API
// ============
$(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
var clickHandler = function (e) {
e.preventDefault()
$(this).tab('show')
})
Plugin.call($(this), 'show')
}
$(document)
.on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler)
.on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
}(jQuery);

View File

@ -1,9 +1,9 @@
/* ========================================================================
* Bootstrap: tooltip.js v3.1.0
* Bootstrap: tooltip.js v3.3.4
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -15,33 +15,46 @@
// ===============================
var Tooltip = function (element, options) {
this.type =
this.options =
this.enabled =
this.timeout =
this.hoverState =
this.type = null
this.options = null
this.enabled = null
this.timeout = null
this.hoverState = null
this.$element = null
this.init('tooltip', element, options)
}
Tooltip.VERSION = '3.3.4'
Tooltip.TRANSITION_DURATION = 150
Tooltip.DEFAULTS = {
animation: true,
placement: 'top',
selector: false,
template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false
container: false,
viewport: {
selector: 'body',
padding: 0
}
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
if (this.$element[0] instanceof document.constructor && !this.options.selector) {
throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
}
var triggers = this.options.trigger.split(' ')
@ -94,7 +107,17 @@
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
obj : $(obj.currentTarget).data('bs.' + this.type)
if (self && self.$tip && self.$tip.is(':visible')) {
self.hoverState = 'in'
return
}
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
clearTimeout(self.timeout)
@ -109,7 +132,12 @@
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
obj : $(obj.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
clearTimeout(self.timeout)
@ -128,12 +156,17 @@
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
var that = this;
var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
if (e.isDefaultPrevented() || !inDom) return
var that = this
var $tip = this.tip()
var tipId = this.getUID(this.type)
this.setContent()
$tip.attr('id', tipId)
this.$element.attr('aria-describedby', tipId)
if (this.options.animation) $tip.addClass('fade')
@ -149,6 +182,7 @@
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
.data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
@ -157,18 +191,14 @@
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
var $parent = this.$element.parent()
var orgPlacement = placement
var docScroll = document.documentElement.scrollTop || document.body.scrollTop
var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth()
var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
var $container = this.options.container ? $(this.options.container) : this.$element.parent()
var containerDim = this.getPosition($container)
placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' :
placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' :
placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' :
placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' :
placement
$tip
@ -179,22 +209,24 @@
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
this.hoverState = null
var complete = function() {
var complete = function () {
var prevHoverState = that.hoverState
that.$element.trigger('shown.bs.' + that.type)
that.hoverState = null
if (prevHoverState == 'out') that.leave(that)
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
}
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var replace
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
@ -228,33 +260,26 @@
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
replace = true
offset.top = offset.top + height - actualHeight
}
if (/bottom|top/.test(placement)) {
var delta = 0
var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
if (offset.left < 0) {
delta = offset.left * -2
offset.left = 0
if (delta.left) offset.left += delta.left
else offset.top += delta.top
$tip.offset(offset)
var isVertical = /top|bottom/.test(placement)
var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
}
this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
} else {
this.replaceArrow(actualHeight - height, actualHeight, 'top')
}
if (replace) $tip.offset(offset)
$tip.offset(offset)
this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
this.arrow()
.css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
.css(isVertical ? 'top' : 'left', '')
}
Tooltip.prototype.setContent = function () {
@ -265,14 +290,17 @@
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function () {
Tooltip.prototype.hide = function (callback) {
var that = this
var $tip = this.tip()
var $tip = $(this.$tip)
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
that.$element.trigger('hidden.bs.' + that.type)
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
callback && callback()
}
this.$element.trigger(e)
@ -281,10 +309,10 @@
$tip.removeClass('in')
$.support.transition && this.$tip.hasClass('fade') ?
$.support.transition && $tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
this.hoverState = null
@ -294,7 +322,7 @@
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
@ -303,19 +331,58 @@
return this.getTitle()
}
Tooltip.prototype.getPosition = function () {
var el = this.$element[0]
return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
width: el.offsetWidth,
height: el.offsetHeight
}, this.$element.offset())
Tooltip.prototype.getPosition = function ($element) {
$element = $element || this.$element
var el = $element[0]
var isBody = el.tagName == 'BODY'
var elRect = el.getBoundingClientRect()
if (elRect.width == null) {
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
}
var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
return $.extend({}, elRect, scroll, outerDims, elOffset)
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
var delta = { top: 0, left: 0 }
if (!this.$viewport) return delta
var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
var viewportDimensions = this.getPosition(this.$viewport)
if (/right|left/.test(placement)) {
var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
if (topEdgeOffset < viewportDimensions.top) { // top overflow
delta.top = viewportDimensions.top - topEdgeOffset
} else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
}
} else {
var leftEdgeOffset = pos.left - viewportPadding
var rightEdgeOffset = pos.left + viewportPadding + actualWidth
if (leftEdgeOffset < viewportDimensions.left) { // left overflow
delta.left = viewportDimensions.left - leftEdgeOffset
} else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
}
}
return delta
}
Tooltip.prototype.getTitle = function () {
@ -329,20 +396,18 @@
return title
}
Tooltip.prototype.getUID = function (prefix) {
do prefix += ~~(Math.random() * 1000000)
while (document.getElementById(prefix))
return prefix
}
Tooltip.prototype.tip = function () {
return this.$tip = this.$tip || $(this.options.template)
return (this.$tip = this.$tip || $(this.options.template))
}
Tooltip.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
}
Tooltip.prototype.validate = function () {
if (!this.$element[0].parentNode) {
this.hide()
this.$element = null
this.options = null
}
return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
}
Tooltip.prototype.enable = function () {
@ -358,33 +423,45 @@
}
Tooltip.prototype.toggle = function (e) {
var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
var self = this
if (e) {
self = $(e.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(e.currentTarget, this.getDelegateOptions())
$(e.currentTarget).data('bs.' + this.type, self)
}
}
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
Tooltip.prototype.destroy = function () {
var that = this
clearTimeout(this.timeout)
this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
this.hide(function () {
that.$element.off('.' + that.type).removeData('bs.' + that.type)
})
}
// TOOLTIP PLUGIN DEFINITION
// =========================
var old = $.fn.tooltip
$.fn.tooltip = function (option) {
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data && /destroy|hide/.test(option)) return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.tooltip
$.fn.tooltip = Plugin
$.fn.tooltip.Constructor = Tooltip

View File

@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: transition.js v3.1.0
* Bootstrap: transition.js v3.3.4
* http://getbootstrap.com/javascript/#transitions
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@ -17,10 +17,10 @@
var el = document.createElement('bootstrap')
var transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd',
'MozTransition' : 'transitionend',
'OTransition' : 'oTransitionEnd otransitionend',
'transition' : 'transitionend'
WebkitTransition : 'webkitTransitionEnd',
MozTransition : 'transitionend',
OTransition : 'oTransitionEnd otransitionend',
transition : 'transitionend'
}
for (var name in transEndEventNames) {
@ -34,8 +34,9 @@
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function (duration) {
var called = false, $el = this
$(this).one($.support.transition.end, function () { called = true })
var called = false
var $el = this
$(this).one('bsTransitionEnd', function () { called = true })
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
setTimeout(callback, duration)
return this
@ -43,6 +44,16 @@
$(function () {
$.support.transition = transitionEnd()
if (!$.support.transition) return
$.event.special.bsTransitionEnd = {
bindType: $.support.transition.end,
delegateType: $.support.transition.end,
handle: function (e) {
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
}
}
})
}(jQuery);

View File

@ -18,6 +18,7 @@
// Specified for the h4 to prevent conflicts of changing @headings-color
color: inherit;
}
// Provide class for links that match alerts
.alert-link {
font-weight: @alert-link-font-weight;
@ -28,17 +29,19 @@
> ul {
margin-bottom: 0;
}
> p + p {
margin-top: 5px;
}
}
// Dismissable alerts
// Dismissible alerts
//
// Expand the right padding and account for the close button's positioning.
.alert-dismissable {
padding-right: (@alert-padding + 20);
.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.
.alert-dismissible {
padding-right: (@alert-padding + 20);
// Adjust close link position
.close {
@ -56,12 +59,15 @@
.alert-success {
.alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);
}
.alert-info {
.alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);
}
.alert-warning {
.alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);
}
.alert-danger {
.alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);
}

Some files were not shown because too many files have changed in this diff Show More