mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge remote-tracking branch 'origin/3.1'
Conflicts: docs/en/topics/testing/create-silverstripe-test.md forms/Form.php i18n/i18n.php model/Image.php
This commit is contained in:
commit
455e550d9a
@ -35,4 +35,3 @@ After:
|
||||
Director:
|
||||
rules:
|
||||
'admin': 'AdminRootController'
|
||||
'dev/buildcache/$Action': 'RebuildStaticCacheTask'
|
@ -34,6 +34,7 @@ HtmlEditorConfig::get('cms')->enablePlugins('media', 'fullscreen', 'inlinepopups
|
||||
HtmlEditorConfig::get('cms')->enablePlugins(array(
|
||||
'ssbuttons' => sprintf('../../../%s/tinymce_ssbuttons/editor_plugin_src.js', THIRDPARTY_DIR)
|
||||
));
|
||||
HtmlEditorConfig::get('cms')->enablePlugins('advimagescale');
|
||||
|
||||
HtmlEditorConfig::get('cms')->insertButtonsBefore('formatselect', 'styleselect');
|
||||
HtmlEditorConfig::get('cms')->addButtonsToLine(2,
|
||||
|
@ -37,4 +37,9 @@ class CMSForm extends Form {
|
||||
return $this->responseNegotiator;
|
||||
}
|
||||
|
||||
public function FormName() {
|
||||
if($this->htmlID) return $this->htmlID;
|
||||
else return 'Form_' . str_replace(array('.', '/'), '', $this->name);
|
||||
}
|
||||
|
||||
}
|
@ -1379,7 +1379,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
);
|
||||
$form->addExtraClass('cms-batch-actions nostyle');
|
||||
$form->unsetValidator();
|
||||
|
||||
|
||||
$this->extend('updateBatchActionsForm', $form);
|
||||
return $form;
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
false,
|
||||
Member::get(),
|
||||
$memberListConfig = GridFieldConfig_RecordEditor::create()
|
||||
->addComponent(new GridFieldExportButton())
|
||||
->addComponent(new GridFieldButtonRow('after'))
|
||||
->addComponent(new GridFieldExportButton('buttons-after-left'))
|
||||
)->addExtraClass("members_grid");
|
||||
$memberListConfig->getComponentByType('GridFieldDetailForm')->setValidator(new Member_Validator());
|
||||
|
||||
@ -83,10 +84,10 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
));
|
||||
$columns->setFieldFormatting(array(
|
||||
'Breadcrumbs' => function($val, $item) {
|
||||
return $item->getBreadcrumbs(' > ');
|
||||
return Convert::raw2xml($item->getBreadcrumbs(' > '));
|
||||
}
|
||||
));
|
||||
|
||||
|
||||
$fields = new FieldList(
|
||||
$root = new TabSet(
|
||||
'Root',
|
||||
@ -100,34 +101,42 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
. ' database'
|
||||
)
|
||||
)
|
||||
),
|
||||
new HeaderField(_t('SecurityAdmin.IMPORTUSERS', 'Import users'), 3),
|
||||
new LiteralField(
|
||||
'MemberImportFormIframe',
|
||||
sprintf(
|
||||
'<iframe src="%s" id="MemberImportFormIframe" width="100%%" height="250px" frameBorder="0">'
|
||||
. '</iframe>',
|
||||
$this->Link('memberimport')
|
||||
)
|
||||
)
|
||||
),
|
||||
$groupsTab = new Tab('Groups', singleton('Group')->i18n_plural_name(),
|
||||
$groupList,
|
||||
new HeaderField(_t('SecurityAdmin.IMPORTGROUPS', 'Import groups'), 3),
|
||||
new LiteralField(
|
||||
'GroupImportFormIframe',
|
||||
sprintf(
|
||||
'<iframe src="%s" id="GroupImportFormIframe" width="100%%" height="250px" frameBorder="0">'
|
||||
. '</iframe>',
|
||||
$this->Link('groupimport')
|
||||
)
|
||||
)
|
||||
$groupList
|
||||
)
|
||||
),
|
||||
// necessary for tree node selection in LeftAndMain.EditForm.js
|
||||
new HiddenField('ID', false, 0)
|
||||
);
|
||||
|
||||
// Add import capabilities. Limit to admin since the import logic can affect assigned permissions
|
||||
if(Permission::check('ADMIN')) {
|
||||
$fields->addFieldsToTab('Root.Users', array(
|
||||
new HeaderField(_t('SecurityAdmin.IMPORTUSERS', 'Import users'), 3),
|
||||
new LiteralField(
|
||||
'MemberImportFormIframe',
|
||||
sprintf(
|
||||
'<iframe src="%s" id="MemberImportFormIframe" width="100%%" height="250px" frameBorder="0">'
|
||||
. '</iframe>',
|
||||
$this->Link('memberimport')
|
||||
)
|
||||
)
|
||||
));
|
||||
$fields->addFieldsToTab('Root.Groups', array(
|
||||
new HeaderField(_t('SecurityAdmin.IMPORTGROUPS', 'Import groups'), 3),
|
||||
new LiteralField(
|
||||
'GroupImportFormIframe',
|
||||
sprintf(
|
||||
'<iframe src="%s" id="GroupImportFormIframe" width="100%%" height="250px" frameBorder="0">'
|
||||
. '</iframe>',
|
||||
$this->Link('groupimport')
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
// Tab nav in CMS is rendered through separate template
|
||||
$root->setTemplate('CMSTabSet');
|
||||
|
||||
@ -195,6 +204,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
* @return Form
|
||||
*/
|
||||
public function MemberImportForm() {
|
||||
if(!Permission::check('ADMIN')) return false;
|
||||
|
||||
$group = $this->currentPage();
|
||||
$form = new MemberImportForm(
|
||||
$this,
|
||||
@ -225,6 +236,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
* @return Form
|
||||
*/
|
||||
public function GroupImportForm() {
|
||||
if(!Permission::check('ADMIN')) return false;
|
||||
|
||||
$form = new GroupImportForm(
|
||||
$this,
|
||||
'GroupImportForm'
|
||||
@ -306,7 +319,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
/**
|
||||
* The permissions represented in the $codes will not appearing in the form
|
||||
* containing {@link PermissionCheckboxSetField} so as not to be checked / unchecked.
|
||||
*
|
||||
*
|
||||
* @deprecated 3.1 Use "Permission.hidden_permissions" config setting instead
|
||||
* @param $codes String|Array
|
||||
*/
|
||||
|
@ -449,6 +449,10 @@ body.cms { overflow: hidden; }
|
||||
.cms-edit-form .message { margin: 16px; }
|
||||
.cms-edit-form .ui-tabs-panel .message { margin: 16px 0; }
|
||||
|
||||
.notice-item { border: 0; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; font-family: inherit; font-size: inherit; padding: 8px 10px 8px 10px; }
|
||||
|
||||
.notice-item-close { color: #333333; background: url(../images/filter-icons.png) no-repeat 0 -20px; width: 1px; height: 1px; overflow: hidden; padding: 0px 0 20px 15px; }
|
||||
|
||||
/** -------------------------------------------- Page icons -------------------------------------------- */
|
||||
.page-icon, a .jstree-pageicon { display: block; width: 16px; height: 16px; background: transparent url(../images/sitetree_ss_pageclass_icons_default.png) no-repeat; }
|
||||
|
||||
@ -874,11 +878,11 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
|
||||
.cms-menu.collapsed.cms-panel .cms-panel-content { display: block; }
|
||||
|
||||
.cms-menu-list li { /* Style applied to the menu flyout only when the collapsed setting */ }
|
||||
.cms-menu-list li a { display: block; height: 24px; line-height: 24px; font-size: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-shadow: #bfcad2 1px 1px 0; color: #1f1f1f; padding: 7px 5px 7px 8px; background-color: #b0bec7; cursor: pointer; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #92a5b2)); background-image: -webkit-linear-gradient(#b0bec7, #92a5b2); background-image: -moz-linear-gradient(#b0bec7, #92a5b2); background-image: -o-linear-gradient(#b0bec7, #92a5b2); background-image: linear-gradient(#b0bec7, #92a5b2); border-top: 1px solid #c2cdd4; border-bottom: 1px solid #748d9d; }
|
||||
.cms-menu-list li a { display: block; line-height: 16px; min-height: 16px; font-size: 12px; text-shadow: #bfcad2 1px 1px 0; color: #1f1f1f; padding: 11px 5px 11px 8px; background-color: #b0bec7; cursor: pointer; position: relative; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #92a5b2)); background-image: -webkit-linear-gradient(#b0bec7, #92a5b2); background-image: -moz-linear-gradient(#b0bec7, #92a5b2); background-image: -o-linear-gradient(#b0bec7, #92a5b2); background-image: linear-gradient(#b0bec7, #92a5b2); border-top: 1px solid #c2cdd4; border-bottom: 1px solid #748d9d; }
|
||||
.cms-menu-list li a:hover { text-decoration: none; background-color: #b6c3cb; border-bottom: 1px solid #8399a7; color: #2c2c2c; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JmY2FkMiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #bfcad2), color-stop(100%, #b0bec7)); background-image: -webkit-linear-gradient(#bfcad2, #b0bec7); background-image: -moz-linear-gradient(#bfcad2, #b0bec7); background-image: -o-linear-gradient(#bfcad2, #b0bec7); background-image: linear-gradient(#bfcad2, #b0bec7); }
|
||||
.cms-menu-list li a:focus, .cms-menu-list li a:active { border-top: 1px solid #a1b2bc; text-decoration: none; background-color: #a1b2bc; color: #393939; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ExYjJiYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #92a5b2), color-stop(100%, #a1b2bc)); background-image: -webkit-linear-gradient(#92a5b2, #a1b2bc); background-image: -moz-linear-gradient(#92a5b2, #a1b2bc); background-image: -o-linear-gradient(#92a5b2, #a1b2bc); background-image: linear-gradient(#92a5b2, #a1b2bc); }
|
||||
.cms-menu-list li a .icon { display: inline-block; float: left; margin: 4px 10px 0 4px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; }
|
||||
.cms-menu-list li a .text { display: inline-block; float: left; }
|
||||
.cms-menu-list li a .icon { display: block; position: absolute; top: 50%; margin-left: 4px; margin-top: -8px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; }
|
||||
.cms-menu-list li a .text { display: block; margin-left: 30px; }
|
||||
.cms-menu-list li a .toggle-children { display: inline-block; float: right; width: 20px; height: 100%; cursor: pointer; }
|
||||
.cms-menu-list li a .toggle-children .toggle-children-icon { display: inline-block; width: 8px; height: 8px; background: url('../images/sprites-32x32-s972e408b05.png') 0 -349px no-repeat; vertical-align: middle; }
|
||||
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -333px no-repeat; }
|
||||
|
@ -224,15 +224,31 @@
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a style element we can use in IE8 to fix fonts (see readystatechange binding in onadd below)
|
||||
*/
|
||||
getOrAppendFontFixStyleElement: function() {
|
||||
var style = $('#FontFixStyleElement');
|
||||
if (!style.length) {
|
||||
style = $(
|
||||
'<style type="text/css" id="FontFixStyleElement" disabled="disabled">'+
|
||||
':before,:after{content:none !important}'+
|
||||
'</style>'
|
||||
).appendTo('head');
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialise the preview element.
|
||||
*/
|
||||
onadd: function() {
|
||||
var self = this, layoutContainer = this.parent();
|
||||
var self = this, layoutContainer = this.parent(), iframe = this.find('iframe');
|
||||
|
||||
// Create layout and controls
|
||||
this.find('iframe').addClass('center');
|
||||
this.find('iframe').bind('load', function() {
|
||||
iframe.addClass('center');
|
||||
iframe.bind('load', function() {
|
||||
self._adjustIframeForPreview();
|
||||
|
||||
// Load edit view for new page, but only if the preview is activated at the moment.
|
||||
@ -241,7 +257,17 @@
|
||||
|
||||
$(this).removeClass('loading');
|
||||
});
|
||||
|
||||
|
||||
// If there's any webfonts in the preview, IE8 will start glitching. This fixes that.
|
||||
if ($.browser.msie && 8 === parseInt($.browser.version, 10)) {
|
||||
iframe.bind('readystatechange', function(e) {
|
||||
if(iframe[0].readyState == 'interactive') {
|
||||
self.getOrAppendFontFixStyleElement().removeAttr('disabled');
|
||||
setTimeout(function(){ self.getOrAppendFontFixStyleElement().attr('disabled', 'disabled'); }, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Preview might not be available in all admin interfaces - block/disable when necessary
|
||||
this.append('<div class="cms-preview-overlay ui-widget-overlay-light"></div>');
|
||||
this.find('.cms-preview-overlay').hide();
|
||||
|
@ -542,6 +542,8 @@ jQuery.noConflict();
|
||||
|
||||
// Store the fragment request so we can abort later, should we get a duplicate request.
|
||||
fragmentXHR[pjaxFragments] = xhr;
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -155,18 +155,16 @@
|
||||
li {
|
||||
a {
|
||||
display: block;
|
||||
height: $grid-y * 3;
|
||||
line-height: $grid-y * 3;
|
||||
line-height: $grid-y * 2;
|
||||
min-height: $grid-y * 2;
|
||||
font-size: $font-base-size;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: lighten($color-base, 5%) 1px 1px 0;
|
||||
color: $color-text-dark;
|
||||
padding: ($grid-y - 1) 5px ($grid-y - 1) 8px;
|
||||
padding: (1.5 * $grid-y - 1) 5px (1.5 * $grid-y - 1) 8px;
|
||||
background-color: $color-base;
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
@include background-image(linear-gradient(
|
||||
$color-base,
|
||||
darken($color-base, 10%)
|
||||
@ -200,16 +198,18 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin: 4px 10px 0 4px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-left: $grid-x / 2;
|
||||
margin-top: -8px;
|
||||
|
||||
@include opacity(0.7);
|
||||
}
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
display: block;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.toggle-children {
|
||||
|
@ -479,6 +479,26 @@ body.cms {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.notice-item {
|
||||
border: 0;
|
||||
@include border-radius(3px);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
padding: 8px 10px 8px 10px;
|
||||
}
|
||||
|
||||
.notice-item-close {
|
||||
color: #333333;
|
||||
background: url(../images/filter-icons.png) no-repeat 0 -20px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
padding: 0px 0 20px 15px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** --------------------------------------------
|
||||
* Page icons
|
||||
* -------------------------------------------- */
|
||||
|
@ -224,8 +224,11 @@ class RestfulService extends ViewableData {
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
|
||||
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
//include headers in the response
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
|
||||
|
||||
// Write headers to a temporary file
|
||||
$headerfd = tmpfile();
|
||||
curl_setopt($ch, CURLOPT_WRITEHEADER, $headerfd);
|
||||
|
||||
// Add headers
|
||||
if($this->customHeaders) {
|
||||
@ -260,8 +263,13 @@ class RestfulService extends ViewableData {
|
||||
curl_setopt_array($ch, $curlOptions);
|
||||
|
||||
// Run request
|
||||
$rawResponse = curl_exec($ch);
|
||||
$response = $this->extractResponse($ch, $rawResponse);
|
||||
$body = curl_exec($ch);
|
||||
|
||||
rewind($headerfd);
|
||||
$headers = stream_get_contents($headerfd);
|
||||
fclose($headerfd);
|
||||
|
||||
$response = $this->extractResponse($ch, $headers, $body);
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
@ -315,22 +323,19 @@ class RestfulService extends ViewableData {
|
||||
*
|
||||
* @return RestfulService_Response The response object
|
||||
*/
|
||||
protected function extractResponse($ch, $rawResponse) {
|
||||
protected function extractResponse($ch, $rawHeaders, $rawBody) {
|
||||
//get the status code
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
//get a curl error if there is one
|
||||
$curlError = curl_error($ch);
|
||||
//normalise the status code
|
||||
if(curl_error($ch) !== '' || $statusCode == 0) $statusCode = 500;
|
||||
//calculate the length of the header and extract it
|
||||
$headerLength = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$rawHeaders = substr($rawResponse, 0, $headerLength);
|
||||
//extract the body
|
||||
$body = substr($rawResponse, $headerLength);
|
||||
//parse the headers
|
||||
$headers = $this->parseRawHeaders($rawHeaders);
|
||||
$parts = array_filter(explode("\r\n\r\n", $rawHeaders));
|
||||
$lastHeaders = array_pop($parts);
|
||||
$headers = $this->parseRawHeaders($lastHeaders);
|
||||
//return the response object
|
||||
return new RestfulService_Response($body, $statusCode, $headers);
|
||||
return new RestfulService_Response($rawBody, $statusCode, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -552,8 +552,9 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
||||
public static function join_links() {
|
||||
$args = func_get_args();
|
||||
$result = "";
|
||||
$querystrings = array();
|
||||
$queryargs = array();
|
||||
$fragmentIdentifier = null;
|
||||
|
||||
foreach($args as $arg) {
|
||||
// Find fragment identifier - keep the last one
|
||||
if(strpos($arg,'#') !== false) {
|
||||
@ -562,7 +563,8 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
||||
// Find querystrings
|
||||
if(strpos($arg,'?') !== false) {
|
||||
list($arg, $suffix) = explode('?',$arg,2);
|
||||
$querystrings[] = $suffix;
|
||||
parse_str($suffix, $localargs);
|
||||
$queryargs = array_merge($queryargs, $localargs);
|
||||
}
|
||||
if((is_string($arg) && $arg) || is_numeric($arg)) {
|
||||
$arg = (string)$arg;
|
||||
@ -571,7 +573,8 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if($querystrings) $result .= '?' . implode('&', $querystrings);
|
||||
if($queryargs) $result .= '?' . http_build_query($queryargs);
|
||||
|
||||
if($fragmentIdentifier) $result .= "#$fragmentIdentifier";
|
||||
|
||||
return $result;
|
||||
|
@ -147,11 +147,22 @@ class Director implements TemplateGlobalProvider {
|
||||
|
||||
// Return code for a redirection request
|
||||
if(is_string($result) && substr($result,0,9) == 'redirect:') {
|
||||
$response = new SS_HTTPResponse();
|
||||
$response->redirect(substr($result, 9));
|
||||
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
|
||||
if ($res !== false) {
|
||||
$response->output();
|
||||
$url = substr($result, 9);
|
||||
|
||||
if(Director::is_cli()) {
|
||||
// on cli, follow SilverStripe redirects automatically
|
||||
return Director::direct(
|
||||
str_replace(Director::absoluteBaseURL(), '', $url),
|
||||
DataModel::inst()
|
||||
);
|
||||
} else {
|
||||
$response = new SS_HTTPResponse();
|
||||
$response->redirect($url);
|
||||
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
|
||||
|
||||
if ($res !== false) {
|
||||
$response->output();
|
||||
}
|
||||
}
|
||||
// Handle a controller
|
||||
} else if($result) {
|
||||
|
@ -343,7 +343,26 @@ class HTTP {
|
||||
$responseHeaders['Vary'] = 'Cookie, X-Forwarded-Protocol, User-Agent, Accept';
|
||||
}
|
||||
else {
|
||||
$responseHeaders["Cache-Control"] = "no-cache, max-age=0, must-revalidate, no-transform";
|
||||
if($body) {
|
||||
// Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
|
||||
$contentDisposition = $body->getHeader('Content-disposition');
|
||||
if (!$contentDisposition) $contentDisposition = $body->getHeader('Content-Disposition');
|
||||
}
|
||||
|
||||
if(
|
||||
$body &&
|
||||
Director::is_https() &&
|
||||
strstr($_SERVER["HTTP_USER_AGENT"], 'MSIE')==true &&
|
||||
strstr($contentDisposition, 'attachment;')==true
|
||||
) {
|
||||
// IE6-IE8 have problems saving files when https and no-cache are used
|
||||
// (http://support.microsoft.com/kb/323308)
|
||||
// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
|
||||
$responseHeaders["Cache-Control"] = "max-age=3, must-revalidate, no-transform";
|
||||
$responseHeaders["Pragma"] = "";
|
||||
} else {
|
||||
$responseHeaders["Cache-Control"] = "no-cache, max-age=0, must-revalidate, no-transform";
|
||||
}
|
||||
}
|
||||
|
||||
if(self::$modification_date && self::$cache_age > 0) {
|
||||
|
@ -393,13 +393,9 @@ class SS_HTTPRequest implements ArrayAccess {
|
||||
}
|
||||
$response = new SS_HTTPResponse($fileData);
|
||||
$response->addHeader("Content-Type", "$mimeType; name=\"" . addslashes($fileName) . "\"");
|
||||
// Note a IE-only fix that inspects this header in HTTP::add_cache_headers().
|
||||
$response->addHeader("Content-disposition", "attachment; filename=" . addslashes($fileName));
|
||||
$response->addHeader("Content-Length", strlen($fileData));
|
||||
$response->addHeader("Pragma", ""); // Necessary because IE has issues sending files over SSL
|
||||
|
||||
if(strstr($_SERVER["HTTP_USER_AGENT"],"MSIE") == true) {
|
||||
$response->addHeader('Cache-Control', 'max-age=3, must-revalidate'); // Workaround for IE6 and 7
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__) . '/InjectionCreator.php';
|
||||
require_once dirname(__FILE__) . '/SilverStripeInjectionCreator.php';
|
||||
require_once dirname(__FILE__) . '/ServiceConfigurationLocator.php';
|
||||
require_once dirname(__FILE__) . '/SilverStripeServiceConfigurationLocator.php';
|
||||
|
||||
/**
|
||||
* A simple injection manager that manages creating objects and injecting
|
||||
* dependencies between them. It borrows quite a lot from ideas taken from
|
||||
@ -830,94 +835,4 @@ class Injector {
|
||||
public function createWithArgs($name, $constructorArgs) {
|
||||
return $this->get($name, false, $constructorArgs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for creating new objects by the injector
|
||||
*/
|
||||
class InjectionCreator {
|
||||
/**
|
||||
*
|
||||
* @param string $object
|
||||
* A string representation of the class to create
|
||||
* @param array $params
|
||||
* An array of parameters to be passed to the constructor
|
||||
*/
|
||||
public function create($class, $params = array()) {
|
||||
$reflector = new ReflectionClass($class);
|
||||
if (count($params)) {
|
||||
return $reflector->newInstanceArgs($params);
|
||||
}
|
||||
return $reflector->newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
class SilverStripeInjectionCreator {
|
||||
/**
|
||||
*
|
||||
* @param string $object
|
||||
* A string representation of the class to create
|
||||
* @param array $params
|
||||
* An array of parameters to be passed to the constructor
|
||||
*/
|
||||
public function create($class, $params = array()) {
|
||||
$class = Object::getCustomClass($class);
|
||||
$reflector = new ReflectionClass($class);
|
||||
return $reflector->newInstanceArgs($params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to locate configuration for a particular named service.
|
||||
*
|
||||
* If it isn't found, return null
|
||||
*/
|
||||
class ServiceConfigurationLocator {
|
||||
public function locateConfigFor($name) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the SilverStripe configuration system to lookup config for a particular service
|
||||
*/
|
||||
class SilverStripeServiceConfigurationLocator {
|
||||
|
||||
private $configs = array();
|
||||
|
||||
public function locateConfigFor($name) {
|
||||
|
||||
if (isset($this->configs[$name])) {
|
||||
return $this->configs[$name];
|
||||
}
|
||||
|
||||
$config = Config::inst()->get('Injector', $name);
|
||||
if ($config) {
|
||||
$this->configs[$name] = $config;
|
||||
return $config;
|
||||
}
|
||||
|
||||
// do parent lookup if it's a class
|
||||
if (class_exists($name)) {
|
||||
$parents = array_reverse(array_keys(ClassInfo::ancestry($name)));
|
||||
array_shift($parents);
|
||||
foreach ($parents as $parent) {
|
||||
// have we already got for this?
|
||||
if (isset($this->configs[$parent])) {
|
||||
return $this->configs[$parent];
|
||||
}
|
||||
$config = Config::inst()->get('Injector', $parent);
|
||||
if ($config) {
|
||||
$this->configs[$name] = $config;
|
||||
return $config;
|
||||
} else {
|
||||
$this->configs[$parent] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// there is no parent config, so we'll record that as false so we don't do the expensive
|
||||
// lookup through parents again
|
||||
$this->configs[$name] = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -91,8 +91,12 @@ class PaginatedList extends SS_ListDecorator {
|
||||
*/
|
||||
public function getPageStart() {
|
||||
if ($this->pageStart === null) {
|
||||
if ($this->request && isset($this->request[$this->getPaginationGetVar()])) {
|
||||
$this->pageStart = (int) $this->request[$this->getPaginationGetVar()];
|
||||
if(
|
||||
$this->request
|
||||
&& isset($this->request[$this->getPaginationGetVar()])
|
||||
&& $this->request[$this->getPaginationGetVar()] > 0
|
||||
) {
|
||||
$this->pageStart = (int)$this->request[$this->getPaginationGetVar()];
|
||||
} else {
|
||||
$this->pageStart = 0;
|
||||
}
|
||||
@ -417,4 +421,4 @@ class PaginatedList extends SS_ListDecorator {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info { background-color: #c11f1d; padding-right: 130px; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #c11f1d), color-stop(4%, #bf1d1b), color-stop(8%, #b71b1c), color-stop(15%, #b61e1d), color-stop(27%, #b11d1d), color-stop(31%, #ab1d1c), color-stop(42%, #a51b1b), color-stop(46%, #9f1b19), color-stop(50%, #9f1b19), color-stop(54%, #991c1a), color-stop(58%, #971a18), color-stop(62%, #911b1b), color-stop(65%, #911b1b), color-stop(88%, #7e1816), color-stop(92%, #771919), color-stop(100%, #731817)); background-image: -webkit-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -moz-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -o-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name { width: 100%; cursor: default; background: #bcb9b9; background: rgba(201, 198, 198, 0.9); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.7); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-warning .ss-uploadfield-item-info { background-color: #e9d104; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e5d33b), color-stop(8%, #e2ce24), color-stop(50%, #d1be1c), color-stop(54%, #d1bc1b), color-stop(96%, #d09a1a), color-stop(100%, #ce8719)); background-image: -webkit-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: -moz-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: -o-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-warning .ss-uploadfield-item-info { background-color: #ff9300; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #eba547), color-stop(8%, #e89a30), color-stop(50%, #e68f19), color-stop(54%, #e68e19), color-stop(96%, #e17519), color-stop(100%, #dc6718)); background-image: -webkit-linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); background-image: -moz-linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); background-image: -o-linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); background-image: linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name { position: relative; z-index: 1; margin: 3px 0 3px 50px; width: 50%; color: #5e5e5e; background: #eeeded; background: rgba(255, 255, 255, 0.8); -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; line-height: 24px; height: 22px; padding: 0 5px; text-align: left; cursor: pointer; display: table; table-layout: fixed; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); display: inline; float: left; max-width: 50%; font-weight: normal; padding: 0 5px 0 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status { position: relative; float: right; padding: 0 0 0 5px; max-width: 30%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); }
|
||||
|
@ -14,6 +14,10 @@ Used in side panels and action tabs
|
||||
.cms .ss-gridfield > div { margin-bottom: 36px; }
|
||||
.cms .ss-gridfield > div.addNewGridFieldButton { margin-bottom: 0; }
|
||||
.cms .ss-gridfield > div.addNewGridFieldButton .action { margin-bottom: 12px; }
|
||||
.cms .ss-gridfield > div.ss-gridfield-buttonrow-before { margin-bottom: 0; }
|
||||
.cms .ss-gridfield > div.ss-gridfield-buttonrow-before .action { margin-bottom: 12px; }
|
||||
.cms .ss-gridfield > div.ss-gridfield-buttonrow-after { margin-bottom: 0; }
|
||||
.cms .ss-gridfield > div.ss-gridfield-buttonrow-after .action { margin-top: 12px; }
|
||||
.cms .ss-gridfield[data-selectable] tr.ui-selected, .cms .ss-gridfield[data-selectable] tr.ui-selecting { background: #FFFAD6 !important; }
|
||||
.cms .ss-gridfield[data-selectable] td { cursor: pointer; }
|
||||
.cms .ss-gridfield span button#action_gridfield_relationfind { display: none; }
|
||||
@ -29,7 +33,7 @@ Used in side panels and action tabs
|
||||
.cms .ss-gridfield .add-existing-autocompleter { width: 500px; }
|
||||
.cms .ss-gridfield .add-existing-autocompleter span { display: -moz-inline-stack; display: inline-block; vertical-align: top; *vertical-align: auto; zoom: 1; *display: inline; }
|
||||
.cms .ss-gridfield .add-existing-autocompleter input.relation-search { width: 270px; margin-bottom: 12px; }
|
||||
.cms .ss-gridfield .grid-csv-button, .cms .ss-gridfield .grid-print-button { margin-bottom: 12px; display: -moz-inline-stack; display: inline-block; vertical-align: middle; *vertical-align: auto; zoom: 1; *display: inline; }
|
||||
.cms .ss-gridfield .grid-csv-button, .cms .ss-gridfield .grid-print-button { font-size: 12px; margin-bottom: 0; display: -moz-inline-stack; display: inline-block; vertical-align: middle; *vertical-align: auto; zoom: 1; *display: inline; }
|
||||
.cms table.ss-gridfield-table { display: table; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; padding: 0; border-collapse: separate; border-bottom: 0 none; width: 100%; }
|
||||
.cms table.ss-gridfield-table thead { color: #323e46; background: transparent; }
|
||||
.cms table.ss-gridfield-table thead tr.filter-header .fieldgroup { max-width: 512px; }
|
||||
@ -127,4 +131,3 @@ Used in side panels and action tabs
|
||||
.cms table.ss-gridfield-table tr.last td { border-bottom: 0 none; }
|
||||
.cms table.ss-gridfield-table td:first-child { border-left: 1px solid rgba(0, 0, 0, 0.1); }
|
||||
.cms table.ss-gridfield-table td:last-child { border-right: 1px solid rgba(0, 0, 0, 0.1); }
|
||||
.cms .grid-bottom-button { margin-top: 12px; }
|
||||
|
@ -1,8 +1,18 @@
|
||||
/*Mixin used to generate slightly smaller text and forms
|
||||
Used in side panels and action tabs
|
||||
*/
|
||||
div.TreeDropdownField { width: 400px; background: #fff; border: 1px solid #aaa; cursor: pointer; overflow: visible; position: relative; }
|
||||
div.TreeDropdownField input { border: none; background: none; padding: 0; margin: 0; }
|
||||
div.TreeDropdownField .treedropdownfield-title { float: left; padding: 7px; width: 90%; line-height: 16px; overflow: hidden; outline: none; }
|
||||
div.TreeDropdownField .treedropdownfield-panel { clear: left; position: absolute; overflow: auto; display: none; cursor: default; border: 1px solid #aaa; border-top: none; margin: 1px 0 0 -1px; /* account for border on container div */ max-height: 200px; background-color: #fff; z-index: 50; -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -o-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); }
|
||||
div.TreeDropdownField .treedropdownfield-panel.loading { min-height: 30px; background: white url("../images/network-save.gif") 7px 7px no-repeat; }
|
||||
div.TreeDropdownField .treedropdownfield-title, div.TreeDropdownField .treedropdownfield-search { float: left; padding: 7px; width: 90%; line-height: 16px; overflow: hidden; outline: none; z-index: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
|
||||
div.TreeDropdownField .treedropdownfield-search { background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px; background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, linear-gradient(top, #eeeeee 1%, #ffffff 15%); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; position: relative; z-index: 1100; border: 1px solid #aaa; display: inline-block; font-family: sans-serif; font-size: 1em; margin: 1.5%; outline: 0; padding: 4px 20px 4px 5px; width: 97%; }
|
||||
div.TreeDropdownField.searchable .treedropdownfield-panel.loading { min-height: 64px; background-position: 98% 39px; }
|
||||
div.TreeDropdownField .treedropdownfield-panel { clear: left; position: absolute; display: none; cursor: default; border: 1px solid #aaa; border-top: none; margin: 1px 0 0 -1px; /* account for border on container div */ background-color: #fff; z-index: 70; -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -o-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); }
|
||||
div.TreeDropdownField .treedropdownfield-panel.loading { min-height: 30px; background: white url("../images/network-save.gif") 98% 7px no-repeat; }
|
||||
div.TreeDropdownField .treedropdownfield-panel .tree-holder { position: relative; z-index: 1; }
|
||||
div.TreeDropdownField .treedropdownfield-panel .tree-holder > ul { position: relative; max-height: 200px; overflow-y: auto; }
|
||||
div.TreeDropdownField .treedropdownfield-panel ul { overflow-x: hidden; float: left; width: 100%; }
|
||||
div.TreeDropdownField .treedropdownfield-panel ul .jstree-icon { margin-left: 5px; }
|
||||
div.TreeDropdownField .treedropdownfield-panel ul .jstree-open > ins { background-position: -18px 0; }
|
||||
div.TreeDropdownField .treedropdownfield-panel ul.tree { margin: 0; }
|
||||
div.TreeDropdownField .treedropdownfield-panel ul.tree a { font-size: 12px; }
|
||||
div.TreeDropdownField .treedropdownfield-toggle-panel-link { border: none; margin: 0; z-index: 0; padding: 7px 3px; overflow: hidden; -webkit-border-radius: 0 4px 4px 0; -moz-border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0; }
|
||||
|
@ -1,6 +1,6 @@
|
||||
body { background-color: #eee; margin: 0; overflow-x: hidden; padding: 0; font-family: Helvetica,Arial,sans-serif; }
|
||||
body { background: #eee !important; margin: 0; overflow-x: hidden; padding: 0; font-family: Helvetica,Arial,sans-serif; }
|
||||
|
||||
.info { margin: 0 0 6px 0; padding: 18px; background-color: #003050; position: relative; line-height: 24px; color: #fff; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #002137), color-stop(10%, #003050), color-stop(90%, #003050), color-stop(100%, #002137)); background-image: -webkit-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -moz-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -o-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: linear-gradient(#002137, #003050 10%, #003050 90%, #002137); }
|
||||
.info { margin: 0 0 6px 0; padding: 18px; background-color: #003050; position: relative; line-height: 24px; color: #fff; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #002137), color-stop(10%, #003050), color-stop(90%, #003050), color-stop(100%, #002137)); background-image: -webkit-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -moz-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -o-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: linear-gradient(#002137, #003050 10%, #003050 90%, #002137); z-index: 9999; }
|
||||
.info h1 { margin: 0 0 6px 0; padding: 0 32px 0 0; color: #fff; font-size: 24px; text-shadow: 0 1px #002137; line-height: 30px; background: url(../admin/images/logo_small.png) no-repeat right 3px; }
|
||||
.info h3 { color: #7da4be; font-size: 16px; line-height: 18px; font-weight: normal; }
|
||||
.info p { margin: 0; font-size: 14px; color: #fff; }
|
||||
@ -9,7 +9,7 @@ body { background-color: #eee; margin: 0; overflow-x: hidden; padding: 0; font-f
|
||||
|
||||
.header { margin: 0; border-bottom: 6px solid #ccdef3; height: 23px; background-color: #666673; padding: 4px 0 2px 6px; }
|
||||
|
||||
.trace, .build, .options { padding: 6px 12px; }
|
||||
.trace, .build, .options { padding: 6px 12px; background: #eee !important; position: relative; z-index: 9999; }
|
||||
.trace li, .build li, .options li { font-size: 14px; margin: 6px 0; }
|
||||
|
||||
a { color: #666; }
|
||||
|
@ -1,4 +1,4 @@
|
||||
# 3.0.6 (Not yet released)
|
||||
# 3.0.6
|
||||
|
||||
## Overview
|
||||
|
||||
@ -7,21 +7,37 @@
|
||||
|
||||
## Details
|
||||
|
||||
### Security: Require ADMIN for ?flush=1
|
||||
### Security: Require ADMIN for ?flush=1 (SS-2013-001)
|
||||
|
||||
Flushing the various manifests (class, template, config) is performed through a GET
|
||||
parameter (`flush=1`). Since this action requires more server resources than normal requests,
|
||||
it can facilitate [denial-of-service attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack).
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-001-require-admin-for-flush1/)
|
||||
|
||||
To prevent this, main.php now checks and only allows the flush parameter in the following cases:
|
||||
### Security: Privilege escalation through Group hierarchy setting (SS-2013-003)
|
||||
|
||||
* The [environment](/topics/environment-management) is in "dev mode"
|
||||
* A user is logged in with ADMIN permissions
|
||||
* An error occurs during startup
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-003-privilege-escalation-through-group-hierarchy-setting/)
|
||||
|
||||
This applies to both `flush=1` and `flush=all` (technically we only check for the existence of any parameter value)
|
||||
but only through web requests made through main.php - CLI requests, or any other request that goes through
|
||||
a custom start up script will still process all flush requests as normal.
|
||||
### Security: Privilege escalation through Group and Member CSV upload (SS-2013-004)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-004-privilege-escalation-through-group-and-member-csv-upload/)
|
||||
|
||||
### Security: Privilege escalation through APPLY_ROLES assignment (SS-2013-005)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-005-privilege-escalation-through-apply-roles-assignment/)
|
||||
|
||||
### Security: Information disclosure in Versioned.php (SS-2013-006)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-006-information-disclosure-in-versioned/)
|
||||
|
||||
### Security: Privilege escalation through Group hierarchy setting (SS-2013-003)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-003-privilege-escalation-through-group-hierarchy-setting/)
|
||||
|
||||
### Security: Privilege escalation through Group and Member CSV upload (SS-2013-004)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-004-privilege-escalation-through-group-and-member-csv-upload/)
|
||||
|
||||
### Security: Privilege escalation through APPLY_ROLES assignment (SS-2013-005)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-005-privilege-escalation-through-apply-roles-assignment/)
|
||||
|
||||
## Upgrading
|
||||
|
||||
@ -34,4 +50,4 @@ a custom start up script will still process all flush requests as normal.
|
||||
Before: `BackLink_Button.ss.Back`, after `BackLink_Button_ss.Back`. Please fix any custom language
|
||||
files or uses of those entities in custom code.
|
||||
* If using "Māori/Te Reo" (mi_NZ) as your CMS locale, please re-select it in `admin/myprofile`
|
||||
to ensure correct operation (it has changed its locale identifier)
|
||||
to ensure correct operation (it has changed its locale identifier)
|
||||
|
17
docs/en/changelogs/3.0.7.md
Normal file
17
docs/en/changelogs/3.0.7.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 3.0.7
|
||||
|
||||
## Overview
|
||||
|
||||
### Security: XSS in form validation errors (SS-2013-008)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)
|
||||
|
||||
### Security: XSS in CMS "Pages" section (SS-2013-009)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-section/)
|
||||
|
||||
### API: Form validation message no longer allow HTML
|
||||
|
||||
Due to cross-site scripting concerns when user data is used for form messages,
|
||||
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
|
||||
in the `FormField->validate()` API.
|
@ -9,7 +9,6 @@
|
||||
* Decluttered "Edit Page" buttons by moving minor actions into a "more options" panel
|
||||
* Auto-detect CMS changes and highlight the save button for better informancy
|
||||
* New context action "Show children as list" on tree for better management on large sites
|
||||
* Display "last edited" and "last published" data for pages in CMS
|
||||
* CMS form fields now support help text through `setDescription()`, both inline and as tooltips
|
||||
* Removed SiteTree "MetaTitle" and "MetaKeywords" fields
|
||||
* More legible and simplified tab and menu styling in the CMS
|
||||
@ -23,22 +22,21 @@
|
||||
* Static properties are immutable and private, you must use Config API
|
||||
* Statics in custom Page classes need to be "private"
|
||||
* `$default_cast` is now `Text` instead of `HTMLText`, to secure templates from XSS by default
|
||||
* Shortcodes are no longer supported in template files. They continue to work in DB fields and other
|
||||
HTMLText-cast fields.
|
||||
* Shortcodes are no longer supported in template files (still works in DB fields and through HTMLText casting)
|
||||
* `DataList` and `ArrayList` are now immutable, they'll return cloned instances on modification
|
||||
* Behaviour testing support through [Behat](http://behat.org), with CMS test coverage
|
||||
(see the [SilverStripe Behat Extension]() for details)
|
||||
* Removed legacy table APIs (e.g. `TableListField`), use GridField instead
|
||||
* Deny URL access if `Controller::$allowed_actions` is undefined
|
||||
* Removed support for "*" rules in `Controller::$allowed_actions`
|
||||
* Removed support for overriding rules on parent classes through `Controller::$allowed_actions`
|
||||
* `RestfulService` verifies SSL peers by default
|
||||
* UploadField functions on new records
|
||||
* Editing of relation table data (`$many_many_extraFields`) in `GridField`
|
||||
* Optional integration with ImageMagick as a new image manipulation backend
|
||||
* Support for PHP 5.4's built-in webserver
|
||||
* Support for [Composer](http://getcomposer.org) dependency manager (also works with 3.0)
|
||||
* Added support for filtering incoming HTML from TinyMCE (disabled by default, see [security](/topics/security))
|
||||
* More secure forms by limiting HTTP submissions to GET or POST only (optional)
|
||||
* Behaviour testing support through [Behat](http://behat.org), with CMS test coverage
|
||||
(see the [SilverStripe Behat Extension]() for details)
|
||||
|
||||
## Details
|
||||
|
||||
|
26
docs/en/changelogs/3.1.1.md
Normal file
26
docs/en/changelogs/3.1.1.md
Normal file
@ -0,0 +1,26 @@
|
||||
# 3.1.1 (unreleased)
|
||||
|
||||
## Overview ##
|
||||
|
||||
### CMS
|
||||
|
||||
### Framework
|
||||
|
||||
* Treedropdownfield showsearch defaults to true
|
||||
|
||||
## Details
|
||||
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Treedropdownfield showsearch defaults to true
|
||||
The showSearch option of TreedropdownField is now set to true by default. This is to provide a fallback ui for when children of a tree node fail to render (due to too many children). You may set search as false when initializing a TreedropdownField, or afterwards:
|
||||
|
||||
:::php
|
||||
$treedropdownfield->setShowSearch(false);
|
||||
|
||||
If your data requires a specialized search function, you may specify it within:
|
||||
|
||||
:::php
|
||||
$treedropdownfield->setSearchFunction();
|
||||
|
17
docs/en/changelogs/rc/3.0.6-rc2.md
Normal file
17
docs/en/changelogs/rc/3.0.6-rc2.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 3.0.6-rc2
|
||||
|
||||
# Overview
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* 2013-08-30 [f803704](https://github.com/silverstripe/sapphire/commit/f803704) Disallow permissions assign for APPLY_ROLES (SS-2013-005) (Ingo Schommer)
|
||||
* 2013-08-30 [05757ef](https://github.com/silverstripe/sapphire/commit/05757ef) Privilege escalation through APPLY_ROLES assignment (SS-2013-005) (Ingo Schommer)
|
||||
* 2013-08-30 [6cff967](https://github.com/silverstripe/sapphire/commit/6cff967) Privilege escalation through Group and Member CSV upload (SS-2013-004) (Ingo Schommer)
|
||||
* 2013-08-30 [720c149](https://github.com/silverstripe/sapphire/commit/720c149) Privilege escalation through Group hierarchy setting (SS-2013-003) (Ingo Schommer)
|
||||
* 2013-08-26 [65ad510](https://github.com/silverstripe/sapphire/commit/65ad510) fixed grammatical errors and formatting issues (jbridson)
|
||||
* 2013-08-19 [4a7aef0](https://github.com/silverstripe/sapphire/commit/4a7aef0) Double slashes in ParameterConfirmationToken (Hamish Friedlander)
|
||||
* 2013-08-09 [b1664f8](https://github.com/silverstripe/silverstripe-cms/commit/b1664f8) Check for stage and drafts in SiteTree::canView() (Simon Welsh)
|
||||
* 2013-08-08 [2fae928](https://github.com/silverstripe/silverstripe-cms/commit/2fae928) ArchiveDate enforcement (Hamish Friedlander)
|
17
docs/en/changelogs/rc/3.0.7-rc1.md
Normal file
17
docs/en/changelogs/rc/3.0.7-rc1.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 3.0.7-rc1
|
||||
|
||||
## Overview
|
||||
|
||||
### Security: XSS in form validation errors (SS-2013-008)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)
|
||||
|
||||
### Security: XSS in CMS "Pages" section (SS-2013-009)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-section/)
|
||||
|
||||
### API: Form validation message no longer allow HTML
|
||||
|
||||
Due to cross-site scripting concerns when user data is used for form messages,
|
||||
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
|
||||
in the `FormField->validate()` API.
|
38
docs/en/changelogs/rc/3.1.0-rc2.md
Normal file
38
docs/en/changelogs/rc/3.1.0-rc2.md
Normal file
@ -0,0 +1,38 @@
|
||||
# 3.1.0-rc2
|
||||
|
||||
## Overview
|
||||
|
||||
### Security: Privilege escalation through Group hierarchy setting (SS-2013-003)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-003-privilege-escalation-through-group-hierarchy-setting/)
|
||||
|
||||
### Security: Privilege escalation through Group and Member CSV upload (SS-2013-004)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-004-privilege-escalation-through-group-and-member-csv-upload/)
|
||||
|
||||
### Security: Privilege escalation through APPLY_ROLES assignment (SS-2013-005)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-005-privilege-escalation-through-apply-roles-assignment/)
|
||||
|
||||
## Changelog
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* 2013-08-30 [091c096](https://github.com/silverstripe/sapphire/commit/091c096) Disallow permissions assign for APPLY_ROLES (SS-2013-005) (Ingo Schommer)
|
||||
* 2013-08-30 [cfa88ad](https://github.com/silverstripe/sapphire/commit/cfa88ad) Privilege escalation through APPLY_ROLES assignment (SS-2013-005) (Ingo Schommer)
|
||||
* 2013-08-30 [46556b6](https://github.com/silverstripe/sapphire/commit/46556b6) Privilege escalation through Group and Member CSV upload (SS-2013-004) (Ingo Schommer)
|
||||
* 2013-08-30 [68ca47b](https://github.com/silverstripe/sapphire/commit/68ca47b) Privilege escalation through Group hierarchy setting (SS-2013-003) (Ingo Schommer)
|
||||
* 2013-08-23 [1461ae9](https://github.com/silverstripe/sapphire/commit/1461ae9) Fix regression in IE no-cache https file downloads. (Mateusz Uzdowski)
|
||||
* 2013-08-22 [45c1d2b](https://github.com/silverstripe/sapphire/commit/45c1d2b) webfonts in preview iframe breaking admin fonts (Hamish Friedlander)
|
||||
* 2013-08-21 [a2026ad](https://github.com/silverstripe/sapphire/commit/a2026ad) flushing on non-dev when Session::cookie_secure is true (Hamish Friedlander)
|
||||
* 2013-08-20 [1c31c09](https://github.com/silverstripe/sapphire/commit/1c31c09) Correct Zend_Locale fallbacks in i18n/DateField/DateTimeField (Ingo Schommer)
|
||||
* 2013-08-20 [68d8ec3](https://github.com/silverstripe/sapphire/commit/68d8ec3) Memory leaks in jstree drag & drop (Hamish Friedlander)
|
||||
* 2013-08-20 [fda4b91](https://github.com/silverstripe/sapphire/commit/fda4b91) Make sure CurrentXHR is set back to null on completion (Hamish Friedlander)
|
||||
* 2013-08-20 [e282f0b](https://github.com/silverstripe/sapphire/commit/e282f0b) Two memory leaks with HtmlEditorField (Hamish Friedlander)
|
||||
* 2013-08-19 [4a7aef0](https://github.com/silverstripe/sapphire/commit/4a7aef0) Double slashes in ParameterConfirmationToken (Hamish Friedlander)
|
||||
* 2013-08-15 [0ca4969](https://github.com/silverstripe/sapphire/commit/0ca4969) Dont update preview iframe if hidden (Hamish Friedlander)
|
||||
* 2013-08-12 [c59305d](https://github.com/silverstripe/sapphire/commit/c59305d) Multiple redraw calls on navigation (Hamish Friedlander)
|
||||
* 2013-08-09 [b1664f8](https://github.com/silverstripe/silverstripe-cms/commit/b1664f8) Check for stage and drafts in SiteTree::canView() (Simon Welsh)
|
||||
* 2013-08-08 [2fae928](https://github.com/silverstripe/silverstripe-cms/commit/2fae928) ArchiveDate enforcement (Hamish Friedlander)
|
||||
* 2013-08-07 [fb67181](https://github.com/silverstripe/sapphire/commit/fb67181) Context menu too long - CSS only (Fixes CMS #811) (Naomi Guyer)
|
||||
* 2013-08-07 [71608f0](https://github.com/silverstripe/silverstripe-cms/commit/71608f0) Add SiteTree link tracking as an extension, and apply to SiteTree itself (Hamish Friedlander)
|
21
docs/en/changelogs/rc/3.1.0-rc3.md
Normal file
21
docs/en/changelogs/rc/3.1.0-rc3.md
Normal file
@ -0,0 +1,21 @@
|
||||
# 3.1.0-rc3
|
||||
|
||||
# Overview
|
||||
|
||||
### Security: XSS in CMS "Security" section (SS-2013-007)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-007-xss-in-cms-security-section/)
|
||||
|
||||
### Security: XSS in form validation errors (SS-2013-008)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)
|
||||
|
||||
### Security: XSS in CMS "Pages" section (SS-2013-009)
|
||||
|
||||
See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-section/)
|
||||
|
||||
### API: Form validation message no longer allow HTML
|
||||
|
||||
Due to cross-site scripting concerns when user data is used for form messages,
|
||||
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
|
||||
in the `FormField->validate()` API.
|
@ -19,7 +19,7 @@ add a `.cms-description-tooltip` class.
|
||||
:::php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-help-tooltip');
|
||||
->addExtraClass('cms-description-tooltip');
|
||||
|
||||
Tooltips are only supported
|
||||
for native, focusable input elements, which excludes
|
||||
@ -27,4 +27,4 @@ more complex fields like `GridField`, `UploadField`
|
||||
or `DropdownField` with the chosen.js behaviour applied.
|
||||
|
||||
Note: For more advanced help text we recommend using
|
||||
[Custom form field templates](/topics/forms#custom-form-field-templates);
|
||||
[Custom form field templates](/topics/forms#custom-form-field-templates);
|
||||
|
140
docs/en/howto/gridfield-rowaction.md
Normal file
140
docs/en/howto/gridfield-rowaction.md
Normal file
@ -0,0 +1,140 @@
|
||||
# How to add a custom action to a GridField row
|
||||
|
||||
In a [GridField](/reference/grid-field) instance each table row can have a
|
||||
number of actions located the end of the row such as edit or delete actions.
|
||||
Each action is represented as a instance of a specific class
|
||||
(e.g [api:GridFieldEditButton]) which has been added to the `GridFieldConfig`
|
||||
for that `GridField`
|
||||
|
||||
As a developer, you can create your own custom actions to be located alongside
|
||||
the built in buttons.
|
||||
|
||||
For example let's create a custom action on the GridField to allow the user to
|
||||
perform custom operations on a row.
|
||||
|
||||
## Basic GridFieldCustomAction boilerplate
|
||||
|
||||
A basic outline of our new `GridFieldCustomAction.php` will look like something
|
||||
below:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider {
|
||||
|
||||
public function augmentColumns($gridField, &$columns) {
|
||||
if(!in_array('Actions', $columns)) {
|
||||
$columns[] = 'Actions';
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnAttributes($gridField, $record, $columnName) {
|
||||
return array('class' => 'col-buttons');
|
||||
}
|
||||
|
||||
|
||||
public function getColumnMetadata($gridField, $columnName) {
|
||||
if($columnName == 'Actions') {
|
||||
return array('title' => '');
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnsHandled($gridField) {
|
||||
return array('Actions');
|
||||
}
|
||||
|
||||
public function getColumnContent($gridField, $record, $columnName) {
|
||||
if(!$record->canEdit()) return;
|
||||
|
||||
$field = GridField_FormAction::create(
|
||||
$gridField,
|
||||
'CustomAction'.$record->ID,
|
||||
'Do Action',
|
||||
"docustomaction",
|
||||
array('RecordID' => $record->ID)
|
||||
);
|
||||
|
||||
|
||||
return $field->Field();
|
||||
}
|
||||
|
||||
public function getActions($gridField) {
|
||||
return array('docustomaction');
|
||||
}
|
||||
|
||||
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
|
||||
if($actionName == 'docustomaction') {
|
||||
// perform your action here
|
||||
|
||||
// output a success message to the user
|
||||
Controller::curr()->getResponse()->setStatusCode(
|
||||
200,
|
||||
'Do Custom Action Done.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## Add the GridFieldCustomAction to the current `GridFieldConfig`
|
||||
|
||||
While we're working on the code, to add this new action to the `GridField`, add
|
||||
a new instance of the class to the [api:GridFieldConfig] object. The `GridField`
|
||||
[Reference](/reference/grid-field) documentation has more information about
|
||||
manipulating the `GridFieldConfig` instance if required.
|
||||
|
||||
:::php
|
||||
$config = GridFieldConfig::create();
|
||||
$config->addComponent(new GridFieldCustomAction());
|
||||
|
||||
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
|
||||
|
||||
Now let's go back and dive through the `GridFieldCustomAction` class in more
|
||||
detail.
|
||||
|
||||
First thing to note is that our new class implements two interfaces,
|
||||
[api:GridField_ColumnProvider] and [api:GridField_ActionProvider].
|
||||
|
||||
Each interface allows our class to define particular behaviors and is a core
|
||||
concept of the modular `GridFieldConfig` system.
|
||||
|
||||
The `GridField_ColumnProvider` implementation tells SilverStripe that this class
|
||||
will provide the `GridField` with an additional column of information. By
|
||||
implementing this interface we're required to define several methods to explain
|
||||
where we want the column to exist and how we need it to be formatted. This is
|
||||
done via the following methods:
|
||||
|
||||
* `augmentColumns`
|
||||
* `getColumnAttributes`
|
||||
* `getColumnMetadata`
|
||||
* `getColumnsHandled`
|
||||
* `getColumnContent`
|
||||
|
||||
In this example, we simply add the new column to the existing `Actions` column
|
||||
located at the end of the table. Our `getColumnContent` implementation produces
|
||||
a custom button for the user to click on the page.
|
||||
|
||||
The second interface we add is `GridField_ActionProvider`. This interface is
|
||||
used as we're providing a custom action for the user to take (`docustomaction`).
|
||||
This action is triggered when a user clicks on the button defined in
|
||||
`getColumnContent`. As with the `GridField_ColumnProvider` interface, by adding
|
||||
this interface we have to define two methods to describe the behavior of the
|
||||
action:
|
||||
|
||||
* `getActions` returns an array of all the custom actions we want this class to
|
||||
handle (i.e `docustomaction`) .
|
||||
* `handleAction` method which will contain the logic for performing the
|
||||
specific action (e.g publishing the row to a thirdparty service).
|
||||
|
||||
Inside `handleAction` we have access to the current GridField and GridField row
|
||||
through the `$arguments`. If your column provides more than one action (e.g two
|
||||
links) both actions will be handled through the one `handleAction` method. The
|
||||
called method is available as a parameter.
|
||||
|
||||
To finish off our basic example, the `handleAction` method simply returns a
|
||||
message to the user interface indicating a successful message.
|
||||
|
||||
## Related
|
||||
|
||||
* [GridField Reference](/reference/grid-field)
|
||||
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)
|
||||
* [Tutorial 5: Dataobject Relationship Management](/tutorials/5-dataobject-relationship-management)
|
@ -19,3 +19,4 @@ the language and functions which are used in the guides.
|
||||
* [How to create a navigation menu](navigation-menu). Create primary navigation for your website.
|
||||
* [Paginating A List](pagination). Add pagination for an SS_List object.
|
||||
* [How to make a simple contact form](simple-contact-form).
|
||||
* [How to add a custom action to a GridField row](gridfield-rowaction)
|
@ -12,7 +12,11 @@ Please register a free translator account to get started, even if you just feel
|
||||
|
||||
We provide a GUI for translations through [transifex.com](http://transifex.com). If you don't have an account yet, please follow the links there to sign up. Select a project from the [list of translatable modules](https://www.transifex.com/accounts/profile/silverstripe/) and start translating online!
|
||||
|
||||
For all modules listed there, we automatically import new master strings as they get committed to the various codebases, so you're always translating on the latest and greatest version.
|
||||
For all modules listed there, we automatically import new master strings
|
||||
as they get committed to the various codebases (via a nightly task),
|
||||
so you're always translating on the latest and greatest version.
|
||||
You can check the last successful push of the translation master strings in our
|
||||
[public continuous integration server](http://teamcity.silverstripe.com/viewType.html?buildTypeId=bt112) (select "log in as guest").
|
||||
|
||||
## FAQ
|
||||
|
||||
|
@ -319,7 +319,7 @@ Upon the receipt of the response, the fragment will be injected into DOM where a
|
||||
has been found on an element (this element will get completely replaced). Afterwards a `afterloadfragment` event
|
||||
will be triggered. In case of a request error a `loadfragmenterror` will be raised and DOM will not be touched.
|
||||
|
||||
You can hook up a response handler that obtains all the details of the XHR request like this:
|
||||
You can hook up a response handler that obtains all the details of the XHR request via Entwine handler:
|
||||
|
||||
'from .cms-container': {
|
||||
onafterloadfragment: function(e, data) {
|
||||
@ -328,6 +328,15 @@ You can hook up a response handler that obtains all the details of the XHR reque
|
||||
}
|
||||
}
|
||||
|
||||
Alternatively you can use the jQuery deferred API:
|
||||
|
||||
$('.cms-container')
|
||||
.loadFragment('admin/foobar/', 'Fragment1')
|
||||
.success(function(data, status, xhr) {
|
||||
// Say 'success'!
|
||||
alert(status);
|
||||
});
|
||||
|
||||
## Ajax Redirects
|
||||
|
||||
Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,
|
||||
|
@ -333,3 +333,4 @@ records, use the `DataObject->can...()` methods
|
||||
|
||||
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)
|
||||
* [Tutorial 5: Dataobject Relationship Management](/tutorials/5-dataobject-relationship-management)
|
||||
* [How to add a custom action to a GridField row](/howto/gridfield-rowaction)
|
||||
|
@ -15,23 +15,19 @@ The simple usage, Permission::check("PERM_CODE") will detect if the currently lo
|
||||
**Group ACLs**
|
||||
|
||||
* Call **Permission::check("MY_PERMISSION_CODE")** to see if the current user has MY_PERMISSION_CODE.
|
||||
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
|
||||
|
||||
You can use whatever codes you like, but for the sanity of developers and users, it would be worth listing the codes in
|
||||
[permissions:codes](/reference/permission)
|
||||
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
|
||||
|
||||
## PermissionProvider
|
||||
|
||||
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*. This method should return a
|
||||
map of permission code names with a human readable explanation of its purpose (see
|
||||
[permissions:codes](/reference/permission)).
|
||||
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*.
|
||||
This method should return a map of permission code names with a human readable explanation of its purpose.
|
||||
|
||||
:::php
|
||||
class Page_Controller implements PermissionProvider {
|
||||
public function init() {
|
||||
if(!Permission::check("VIEW_SITE")) Security::permissionFailure();
|
||||
}
|
||||
|
||||
|
||||
public function providePermissions() {
|
||||
return array(
|
||||
"VIEW_SITE" => "Access the site",
|
||||
@ -53,7 +49,7 @@ By default, permissions are used in the following way:
|
||||
* If not logged in, the 'View' permissions must be 'anyone logged in' for a page to be displayed in a menu
|
||||
* If logged in, you must be allowed to view a page for it to be displayed in a menu
|
||||
|
||||
**NOTE:** Should the canView() method on SiteTree be updated to call Permission::check("SITETREE_VIEW", $this->ID)?
|
||||
**NOTE:** Should the canView() method on SiteTree be updated to call Permission::check("SITETREE_VIEW", $this->ID)?
|
||||
Making this work well is a subtle business and should be discussed with a few developers.
|
||||
|
||||
## Setting up permissions
|
||||
|
@ -91,7 +91,7 @@ JavaScript in a separate file and instead load, via search and replace, several
|
||||
:::php
|
||||
$vars = array(
|
||||
"EditorCSS" => "mot/css/editor.css",
|
||||
)
|
||||
);
|
||||
Requirements::javascriptTemplate("cms/javascript/editor.template.js", $vars);
|
||||
|
||||
|
||||
@ -185,4 +185,4 @@ slightly different JS/CSS requirements, the whole lot will be refetched.
|
||||
nature of an ajax-request. Needs some more research
|
||||
|
||||
## API Documentation
|
||||
`[api:Requirements]`
|
||||
`[api:Requirements]`
|
||||
|
@ -417,7 +417,7 @@ have to repeat it on each reference of a property.
|
||||
properties of the collection itself, instead of iterating over it. For example:
|
||||
|
||||
:::ss
|
||||
$Children.Length
|
||||
$Children.Count
|
||||
|
||||
returns the number of items in the $Children collection.
|
||||
|
||||
|
@ -38,7 +38,7 @@ a description of the configuration setting.
|
||||
Each named class configuration property can contain either an array or a non-array value.
|
||||
If the value is an array, each value in the array may also be one of those three types
|
||||
|
||||
As mentioned, this value of any specific class configuration property comes from several sources. These sources do not
|
||||
As mentioned, the value of any specific class configuration property comes from several sources. These sources do not
|
||||
override each other (except in one specific circumstance) - instead the values from each source are merged together
|
||||
to give the final configuration value, using these rules:
|
||||
|
||||
@ -49,10 +49,10 @@ to give the final configuration value, using these rules:
|
||||
- If the value is not an array, the highest priority value is used without any attempt to merge
|
||||
|
||||
It is an error to have mixed types of the same named property in different locations (but an error will not necessarily
|
||||
be raised due to optimisations in the lookup code)
|
||||
be raised due to optimisations in the lookup code).
|
||||
|
||||
The exception to this is "false-ish" values - empty arrays, empty strings, etc. When merging a non-false-ish value with a
|
||||
false-ish value, the result will be the non-false-ish value regardless of priority. When merging two false-sh values
|
||||
false-ish value, the result will be the non-false-ish value regardless of priority. When merging two false-ish values
|
||||
the result will be the higher priority false-ish value.
|
||||
|
||||
The locations that configuration values are taken from in highest -> lowest priority order are:
|
||||
@ -85,7 +85,7 @@ done by calling the static method `[api:Config::inst()]`, like so:
|
||||
|
||||
$config = Config::inst();
|
||||
|
||||
There are then three public methods available on the instance so obtained
|
||||
There are then three public methods available on the instance so obtained:
|
||||
|
||||
- Config#get() returns the value of a specified classes' property
|
||||
- Config#remove() removes information from the value of a specified classes' property.
|
||||
@ -136,7 +136,7 @@ is only one set of values the header can be omitted.
|
||||
|
||||
### The header
|
||||
|
||||
Each value section of a YAML file has
|
||||
Each value section of a YAML file has:
|
||||
|
||||
- A reference path, made up of the module name, the config file name, and a fragment identifier
|
||||
- A set of rules for the value section's priority relative to other value sections
|
||||
@ -150,13 +150,13 @@ value section in the header section that immediately preceeds the value section.
|
||||
Each value section has a reference path. Each path looks a little like a URL,
|
||||
and is of this form: `module/file#fragment`.
|
||||
|
||||
- "module" is the name of the module this YAML file is in
|
||||
- "file" is the name of this YAML file, stripped of the extension (so for routes.yml, it would be routes)
|
||||
- "fragment" is a specified identifier. It is specified by putting a `Name: {fragment}` key / value pair into the header
|
||||
- "module" is the name of the module this YAML file is in.
|
||||
- "file" is the name of this YAML file, stripped of the extension (so for routes.yml, it would be routes).
|
||||
- "fragment" is a specified identifier. It is specified by putting a `Name: {fragment}` key / value pair into the header.
|
||||
section. If you don't specify a name, a random one will be assigned.
|
||||
|
||||
This reference path has no affect on the value section itself, but is how other header sections refer to this value
|
||||
section in their priority chain rules
|
||||
section in their priority chain rules.
|
||||
|
||||
#### Priorities
|
||||
|
||||
@ -190,7 +190,7 @@ one value section can not be both before _and_ after another. However when you h
|
||||
was a difference in how many wildcards were used, the one with the least wildcards will be kept and the other one
|
||||
ignored.
|
||||
|
||||
A more complex example, taken from framework/_config/routes.yml
|
||||
A more complex example, taken from framework/_config/routes.yml:
|
||||
|
||||
:::yml
|
||||
---
|
||||
@ -212,8 +212,8 @@ The value section above has two rules:
|
||||
|
||||
In this case there would appear to be a problem - adminroutes can not be both before all other value sections _and_
|
||||
after value sections with a name of `rootroutes`. However because `\*` has three wildcards
|
||||
(it is the equivalent of `\*/\*#\*`) but `#rootroutes` only has two (it is the equivalent of `\*/\*#rootroutes`),
|
||||
`\*` in this case means "every value section _except_ ones that have a fragment name of rootroutes"
|
||||
(it is the equivalent of `\*/\*#\*`) but `#rootroutes` only has two (it is the equivalent of `\*/\*#rootroutes`).
|
||||
In this case `\*` means "every value section _except_ ones that have a fragment name of rootroutes".
|
||||
|
||||
One important thing to note: it is possible to create chains that are unsolvable. For instance, A must be before B,
|
||||
B must be before C, C must be before A. In this case you will get an error when accessing your site.
|
||||
@ -221,7 +221,7 @@ B must be before C, C must be before A. In this case you will get an error when
|
||||
#### Exclusionary rules
|
||||
|
||||
Some value sections might only make sense under certain environmental conditions - a class exists, a module is installed,
|
||||
an environment variable or constant is set, or SilverStripe is running in a certain environment mode (live, dev, etc)
|
||||
an environment variable or constant is set, or SilverStripe is running in a certain environment mode (live, dev, etc).
|
||||
|
||||
To accommodate this, value sections can be filtered to only be used when either a rule matches or doesn't match the
|
||||
current environment.
|
||||
@ -267,12 +267,12 @@ will result in only the latter coming through.
|
||||
|
||||
### The values
|
||||
|
||||
The values section of YAML configuration files is quite simple - it is simply a nested key / value pair structure
|
||||
The values section of a YAML configuration file is quite simple - it is simply a nested key / value pair structure
|
||||
where the top level key is the class name to set the property on, and the sub key / value pairs are the properties
|
||||
and values themselves (where values of course can themselves be nested hashes).
|
||||
and values themselves (where values, of course, can themselves be nested hashes).
|
||||
|
||||
A simple example setting a property called "foo" to the scalar "bar" on class "MyClass", and a property called "baz"
|
||||
to a nested array on class "MyOtherClass".
|
||||
to a nested array on class "MyOtherClass":
|
||||
|
||||
:::yml
|
||||
MyClass:
|
||||
@ -319,8 +319,9 @@ classes (see [common-problems](/installation/common-problems)).
|
||||
|
||||
## Configuration through the CMS
|
||||
|
||||
SilverStripe framework does not provide a method to set most system-level configuration via a web panel.
|
||||
This lack of a configuration GUI is on purpose, as we'd like to keep developer-level options where they belong (into
|
||||
SilverStripe framework does not provide a method to set configuration via a web panel.
|
||||
|
||||
This lack of a configuration-GUI is on purpose, as we'd like to keep developer-level options where they belong (into
|
||||
code), without cluttering up the interface. See this core forum discussion ["The role of the
|
||||
CMS"](http://www.silverstripe.org/archive/show/532) for further reasoning.
|
||||
|
||||
|
@ -584,6 +584,35 @@ object type.
|
||||
}
|
||||
|
||||
|
||||
### belongs_to
|
||||
|
||||
Defines a 1-to-1 relationship with another object, which declares the other end
|
||||
of the relationship with a corresponding $has_one. A single database column named
|
||||
`<relationship-name>ID` will be created in the object with the $has_one, but
|
||||
the $belongs_to by itself will not create a database field. This field will hold
|
||||
the ID of the object declaring the $belongs_to.
|
||||
|
||||
Similarly with $has_many, dot notation can be used to explicitly specify the $has_one
|
||||
which refers to this relation. This is not mandatory unless the relationship would
|
||||
be otherwise ambiguous.
|
||||
|
||||
:::php
|
||||
|
||||
class Torso extends DataObject {
|
||||
// HeadID will be generated on the Torso table
|
||||
private static $has_one = array(
|
||||
'Head' => 'Head'
|
||||
);
|
||||
}
|
||||
|
||||
class Head extends DataObject {
|
||||
// No database field created. The '.Head' suffix could be omitted
|
||||
private static $belongs_to = array(
|
||||
'Torso' => 'Torso.Head'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
### many_many
|
||||
|
||||
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will
|
||||
|
@ -131,6 +131,21 @@ An alternative (or additional) approach to validation is to place it directly
|
||||
on the model. SilverStripe provides a `[api:DataObject->validate()]` method for this purpose.
|
||||
Refer to the ["datamodel" topic](/topics/datamodel#validation-and-constraints) for more information.
|
||||
|
||||
## Validation in the CMS
|
||||
|
||||
Since you're not creating the forms for editing CMS records,
|
||||
SilverStripe provides you with a `getCMSValidator()` method on your models
|
||||
to return a `[api:Validator]` instance.
|
||||
|
||||
:::php
|
||||
class Page extends SiteTree {
|
||||
private static $db = array('MyRequiredField' => 'Text');
|
||||
|
||||
public function getCMSValidator() {
|
||||
return new RequiredFields(array('MyRequiredField'));
|
||||
}
|
||||
}
|
||||
|
||||
## Subclassing Validator
|
||||
|
||||
To create your own validator, you need to subclass validator and define two methods:
|
||||
|
@ -156,7 +156,7 @@ data.
|
||||
|
||||
// Template method
|
||||
public function HelloForm() {
|
||||
return new MyForm($this, 'MyCustomForm');
|
||||
return new MyForm($this, 'HelloForm');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -323,7 +323,11 @@ match the hash stored in the users session, the request is discarded.
|
||||
You can disable this behaviour through `[api:Form->disableSecurityToken()]`.
|
||||
|
||||
It is also recommended to limit form submissions to the intended HTTP verb (mostly `GET` or `POST`)
|
||||
through `[api:Form->setStrictFormMethodCheck()]`.
|
||||
through `[api:Form->setStrictFormMethodCheck()]`.
|
||||
|
||||
Sometimes you need to handle state-changing HTTP submissions which aren't handled through
|
||||
SilverStripe's form system. In this case, you can also check the current HTTP request
|
||||
for a valid token through `[api:SecurityToken::checkRequest()]`.
|
||||
|
||||
## Casting user input
|
||||
|
||||
|
@ -1,69 +0,0 @@
|
||||
# How To Create a SilverStripe Test
|
||||
|
||||
A unit test class will test the behaviour of one of your `[api:DataObjects]`. This simple fragment of `[api:SiteTreeTest]`
|
||||
provides us the basics of creating unit tests.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class SiteTreeTest extends SapphireTest {
|
||||
|
||||
// Define the fixture file to use for this test class
|
||||
private static $fixture_file = 'SiteTreeTest.yml';
|
||||
|
||||
/**
|
||||
* Test generation of the URLSegment values.
|
||||
* - Turns things into lowercase-hyphen-format
|
||||
* - Generates from Title by default, unless URLSegment is explicitly set
|
||||
* - Resolves duplicates by appending a number
|
||||
*/
|
||||
public function testURLGeneration() {
|
||||
$expectedURLs = array(
|
||||
'home' => 'home',
|
||||
'staff' => 'my-staff',
|
||||
'about' => 'about-us',
|
||||
'staffduplicate' => 'my-staff-2',
|
||||
'product1' => '1-1-test-product',
|
||||
'product2' => 'another-product',
|
||||
'product3' => 'another-product-2',
|
||||
'product4' => 'another-product-3',
|
||||
);
|
||||
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
There are a number of points to note in this code fragment:
|
||||
|
||||
* Your test is a **subclass of SapphireTest**. Both unit tests and functional tests are a subclass of `[api:SapphireTest]`.
|
||||
* **static $fixture_file** is defined. The testing framework will automatically set up a new database for **each** of
|
||||
your tests. The initial database content will be sourced from the YML file that you list in $fixture_file. The property can take an array of fixture paths.
|
||||
* Each **method that starts with the word "test"** will be executed by the TestRunner. Define as many as you like; the
|
||||
database will be rebuilt for each of these.
|
||||
* **$this->objFromFixture($className, $identifier)** can be used to select one of the objects named in your fixture
|
||||
file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YML
|
||||
file but not saved in the database anywhere. objFromFixture() looks the `[api:DataObject]` up in memory rather than using the
|
||||
database. This means that you can use it to test the functions responsible for looking up content in the database.
|
||||
|
||||
## Assertion commands
|
||||
|
||||
**$this->assertEquals()** is an example of an assertion function.
|
||||
These functions form the basis of our tests - a test
|
||||
fails if and only if one or more of the assertions fail.
|
||||
See [the PHPUnit manual](http://www.phpunit.de/manual/current/en/api.html#api.assert)
|
||||
for a listing of all PHPUnit's built-in assertions.
|
||||
|
||||
The `[api:SapphireTest]` class comes with additional assertions which are more
|
||||
specific to the framework, e.g. `[api:SapphireTest->assertEmailSent()]`
|
||||
which can simulate sending emails through the `[api:Email->send()]` API without actually
|
||||
using a mail server (see the [testing emails](email-sending)) guide.
|
||||
|
||||
## Fixtures
|
||||
|
||||
Often you need to test your functionality with some existing data, so called "fixtures".
|
||||
These records are inserted on a fresh test database automatically.
|
||||
[Read more about fixtures](fixtures).
|
@ -1,4 +1,4 @@
|
||||
# Writing functional tests
|
||||
# Creating a functional tests
|
||||
|
||||
Functional tests test your controllers. The core of these are the same as unit tests:
|
||||
|
||||
@ -13,7 +13,7 @@ URLs. Here is an example from the subsites module:
|
||||
:::php
|
||||
class SubsiteAdminTest extends SapphireTest {
|
||||
private static $fixture_file = 'subsites/tests/SubsiteTest.yml';
|
||||
|
||||
|
||||
/**
|
||||
* Return a session that has a user logged in as an administrator
|
||||
*/
|
||||
@ -22,27 +22,27 @@ URLs. Here is an example from the subsites module:
|
||||
'loggedInAs' => $this->idFromFixture('Member', 'admin')
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test generation of the view
|
||||
*/
|
||||
public function testBasicView() {
|
||||
// Open the admin area logged in as admin
|
||||
$response1 = Director::test('admin/subsites/', null, $this->adminLoggedInSession());
|
||||
|
||||
|
||||
// Confirm that this URL gets you the entire page, with the edit form loaded
|
||||
$response2 = Director::test('admin/subsites/show/1', null, $this->adminLoggedInSession());
|
||||
$this->assertTrue(strpos($response2->getBody(), 'id="Root_Configuration"') !== false);
|
||||
$this->assertTrue(strpos($response2->getBody(), '<head') !== false);
|
||||
|
||||
|
||||
// Confirm that this URL gets you just the form content, with the edit form loaded
|
||||
$response3 = Director::test('admin/subsites/show/1', array('ajax' => 1), $this->adminLoggedInSession());
|
||||
|
||||
|
||||
$this->assertTrue(strpos($response3->getBody(), 'id="Root_Configuration"') !== false);
|
||||
$this->assertTrue(strpos($response3->getBody(), '<form') === false);
|
||||
$this->assertTrue(strpos($response3->getBody(), '<head') === false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
We are using a new static method here: **Director::test($url, $postVars, $sessionObj)**
|
||||
@ -64,5 +64,6 @@ We can use string processing on the body of the response to then see if it fits
|
||||
If you're testing for natural language responses like error messages, make sure to use [i18n](/topics/i18n) translations through
|
||||
the *_t()* method to avoid tests failing when i18n is enabled.
|
||||
|
||||
Note that for a more highlevel testing approach, SilverStripe also supports
|
||||
[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions.
|
||||
Note that for a more highlevel testing approach, SilverStripe also supports
|
||||
[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts
|
||||
directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions.
|
64
docs/en/topics/testing/creating-a-silverstripe-test.md
Normal file
64
docs/en/topics/testing/creating-a-silverstripe-test.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Creating a SilverStripe Test
|
||||
|
||||
A test is created by extending one of two classes, SapphireTest and FunctionalTest.
|
||||
You would subclass `[api:SapphireTest]` to test your application logic,
|
||||
for example testing the behaviour of one of your `[DataObjects](api:DataObject)`,
|
||||
whereas `[api:FunctionalTest]` is extended when you want to test your application's functionality,
|
||||
such as testing the results of GET and POST requests,
|
||||
and validating the content of a page. FunctionalTest is a subclass of SapphireTest.
|
||||
|
||||
## Creating a test from SapphireTest
|
||||
|
||||
Here is an example of a test which extends SapphireTest:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class SiteTreeTest extends SapphireTest {
|
||||
|
||||
// Define the fixture file to use for this test class
|
||||
private static $fixture_file = 'SiteTreeTest.yml';
|
||||
|
||||
/**
|
||||
* Test generation of the URLSegment values.
|
||||
* - Turns things into lowercase-hyphen-format
|
||||
* - Generates from Title by default, unless URLSegment is explicitly set
|
||||
* - Resolves duplicates by appending a number
|
||||
*/
|
||||
public function testURLGeneration() {
|
||||
$expectedURLs = array(
|
||||
'home' => 'home',
|
||||
'staff' => 'my-staff',
|
||||
'about' => 'about-us',
|
||||
'staffduplicate' => 'my-staff-2'
|
||||
);
|
||||
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Firstly we define a static member `$fixture_file`, this should point to a file that represents the data we want to test,
|
||||
represented in YAML. When our test is run, the data from this file will be loaded into a test database for our test to use.
|
||||
This property can be an array of strings pointing to many .yml files, but for our test we are just using a string on its
|
||||
own. For more detail on fixtures, see [this page](fixtures).
|
||||
|
||||
The second part of our class is the `testURLGeneration` method. This method is our test. You can asign many tests, but
|
||||
again for our purposes there is just the one. When the test is executed, methods prefixed with the word `test` will be
|
||||
run. The test database is rebuilt every time one of these methods is run.
|
||||
|
||||
Inside our test method is the `objFromFixture` method that will generate an object for us based on data from our fixture
|
||||
file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YAML file
|
||||
but not saved in the database anywhere, `objFromFixture` looks the `[api:DataObject]` up in memory rather than using the
|
||||
database. This means that you can use it to test the functions responsible for looking up content in the database.
|
||||
|
||||
The final part of our test is an assertion command, `assertEquals`. An assertion command allows us to test for something
|
||||
in our test methods (in this case we are testing if two values are equal). A test method can have more than one assertion
|
||||
command, and if any one of these assertions fail, so will the test method.
|
||||
|
||||
For more information on PHPUnit's assertions see the [PHPUnit manual](http://www.phpunit.de/manual/current/en/api.html#api.assert).
|
||||
|
||||
The `[api:SapphireTest]` class comes with additional assertions which are more specific to Sapphire, for example the
|
||||
`assertEmailSent` method, which simulates sending emails through the `Email->send()`
|
||||
API without actually using a mail server. For more details on this see the [testing emails](testing-email) guide.
|
@ -2,80 +2,126 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Often you need to test your functionality with some existing data, so called "fixtures".
|
||||
The `[api:SapphireTest]` class already prepares an empty database for you,
|
||||
and you have various ways to define those fixtures.
|
||||
You will often find the need to test your functionality with some consistent data.
|
||||
If we are testing our code with the same data each time,
|
||||
we can trust our tests to yeild reliable results.
|
||||
In Silverstripe we define this data via 'fixtures' (so called because of their fixed nature).
|
||||
The `[api:SapphireTest]` class takes care of populating a test database with data from these fixtures -
|
||||
all we have to do is define them, and we have a few ways in which we can do this.
|
||||
|
||||
## YAML Fixtures
|
||||
|
||||
YAML is a markup language which is deliberately simple and easy to read,
|
||||
so ideal for our fixture generation.
|
||||
so it is ideal for fixture generation.
|
||||
|
||||
We will begin with a sample file and talk our way through it.
|
||||
Say we have the following two DataObjects:
|
||||
|
||||
Page:
|
||||
home:
|
||||
Title: Home
|
||||
about:
|
||||
Title: About Us
|
||||
staff:
|
||||
Title: Staff
|
||||
URLSegment: my-staff
|
||||
Parent: =>Page.about
|
||||
|
||||
RedirectorPage:
|
||||
redirect_home:
|
||||
RedirectionType: Internal
|
||||
LinkTo: =>Page.home
|
||||
:::php
|
||||
class Player extends DataObject {
|
||||
static $db = array (
|
||||
'Name' => 'Varchar(255)'
|
||||
);
|
||||
|
||||
static $has_one = array(
|
||||
'Team' => 'Team'
|
||||
);
|
||||
}
|
||||
|
||||
The contents of the YAML file are broken into three levels.
|
||||
class Team extends DataObject {
|
||||
static $db = array (
|
||||
'Name' => 'Varchar(255)',
|
||||
'Origin' => 'Varchar(255)'
|
||||
);
|
||||
|
||||
* **Top level: class names** - `Page` and `RedirectorPage`. This is the name of the dataobject class that should be created.
|
||||
The fact that `RedirectorPage` is actually a subclass is irrelevant to the system populating the database. It just
|
||||
instantiates the object you specify.
|
||||
* **Second level: identifiers** - `home`, `about`, etc. These are the identifiers that you pass as
|
||||
the second argument of SapphireTest::objFromFixture(). Each identifier you specify delimits a new database record.
|
||||
This means that every record needs to have an identifier, whether you use it or not.
|
||||
* **Third level: fields** - each field for the record is listed as a 3rd level entry. In most cases, the field's raw
|
||||
content is provided. However, if you want to define a relationship, you can do so using "=>".
|
||||
static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
|
||||
There are a couple of lines like this:
|
||||
We can represent multiple instances of them in `YAML` as follows:
|
||||
|
||||
Parent: =>Page.about
|
||||
:::yml
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
Team: =>Team.hurricanes
|
||||
joe:
|
||||
Name: Joe
|
||||
Team: =>Team.crusaders
|
||||
jack:
|
||||
Name: Jack
|
||||
Team: =>Team.crusaders
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Origin: Wellington
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Origin: Bay of Plenty
|
||||
|
||||
This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about".
|
||||
This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and
|
||||
not the name of the database field (ParentID)
|
||||
Our `YAML` is broken up into three levels, signified by the indentation of each line.
|
||||
In the first level of indentation, `Player` and `Team`,
|
||||
represent the class names of the objects we want to be created for the test.
|
||||
|
||||
On many-many relationships, you should specify a comma separated list of values.
|
||||
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers.
|
||||
These are what you pass as the second argument of `SapphireTest::objFromFixture()`.
|
||||
Each identifier you specify represents a new object.
|
||||
|
||||
MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3
|
||||
The third and final level represents each individual object's fields.
|
||||
A field can either be provided with raw data (such as the Names for our Players),
|
||||
or we can define a relationship, as seen by the fields prefixed with `=>`.
|
||||
|
||||
An crucial thing to note is that **the YAML file specifies DataObjects, not database records**. The database is
|
||||
populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any
|
||||
onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our
|
||||
testURLGeneration() test above.
|
||||
Each one of our Players has a relationship to a Team,
|
||||
this is shown with the `Team` field for each `Player` being set to `=>Team.` followed by a team name.
|
||||
Take the player John for example, his team is the Hurricanes which is represented by `=>Team.hurricanes`.
|
||||
This is tells the system that we want to set up a relationship for the `Player` object `john` with the `Team` object `hurricanes`.
|
||||
It will populate the `Player` object's `TeamID` with the ID of `hurricanes`,
|
||||
just like how a relationship is always set up.
|
||||
|
||||
For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the
|
||||
fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2.
|
||||
<div class="hint" markdown='1'>
|
||||
Note that we use the name of the relationship (Team), and not the name of the database field (TeamID).
|
||||
</div>
|
||||
|
||||
Finally, be aware that requireDefaultRecords() is **not** called by the database populator - so you will need to specify
|
||||
standard pages such as 404 and home in your YAML file.
|
||||
This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship.
|
||||
For `many-many` relationships, we specify a comma separated list of values.
|
||||
For example we could just as easily write the above as:
|
||||
|
||||
:::yml
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
joe:
|
||||
Name: Joe
|
||||
jack:
|
||||
Name: Jack
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Origin: Wellington
|
||||
Players: =>Player.john
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Origin: Bay of Plenty
|
||||
Players: =>Player.joe,=>Player.jack
|
||||
|
||||
A crucial thing to note is that **the YAML file specifies DataObjects, not database records**.
|
||||
The database is populated by instantiating DataObject objects and setting the fields declared in the YML,
|
||||
then calling write() on those objects.
|
||||
This means that any `onBeforeWrite()` or default value logic will be executed as part of the test.
|
||||
The reasoning behind this is to allow us to test the `onBeforeWrite` functionality of our objects.
|
||||
You can see this kind of testing in action in the `testURLGeneration()` test from the example in
|
||||
[Creating a SilverStripe Test](creating-a-silverstripe-test).
|
||||
|
||||
## Test Class Definition
|
||||
|
||||
### Manual Object Creation
|
||||
|
||||
|
||||
## Manual Object Creation
|
||||
|
||||
Sometimes statically defined fixtures don't suffice, because of the complexity of the tested model,
|
||||
or because the YAML format doesn't allow you to modify all model state.
|
||||
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model,
|
||||
or because the YAML format doesn't allow you to modify all of a model's state.
|
||||
One common example here is publishing pages (page fixtures aren't published by default).
|
||||
|
||||
You can always resort to creating objects manually in the test setup phase.
|
||||
Since the test database is cleared on every test method, you'll get a fresh
|
||||
set of test instances every time.
|
||||
Since the test database is cleared on every test method, you'll get a fresh set of test instances every time.
|
||||
|
||||
:::php
|
||||
class SiteTreeTest extends SapphireTest {
|
||||
@ -94,20 +140,21 @@ set of test instances every time.
|
||||
|
||||
### Why Factories?
|
||||
|
||||
Manually defined fixture provide full flexibility, but very little in terms of structure and convention.
|
||||
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you
|
||||
to set default values, callbacks on object creation, and dynamic/lazy value setting.
|
||||
By the way, the `SapphireTest` YAML fixtures rely on internally on this class as well.
|
||||
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
|
||||
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values,
|
||||
callbacks on object creation, and dynamic/lazy value setting.
|
||||
|
||||
The idea is that rather than instanciating objects directly, we'll have a factory class for them.
|
||||
This factory can have so called "blueprints" defined on it, which tells the factory
|
||||
how to instanciate an object of a specific type. Blueprints need a name,
|
||||
which is usually set to the class it creates.
|
||||
<div class="hint" markdown='1'>
|
||||
SapphireTest uses FixtureFactory under the hood when it is provided with YAML based fixtures.
|
||||
</div>
|
||||
|
||||
The idea is that rather than instantiating objects directly, we'll have a factory class for them.
|
||||
This factory can have so called "blueprints" defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a name, which is usually set to the class it creates.
|
||||
|
||||
### Usage
|
||||
|
||||
Since blueprints are auto-created for all available DataObject subclasses,
|
||||
you only need to instanciate a factory to start using it.
|
||||
you only need to instantiate a factory to start using it.
|
||||
|
||||
:::php
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
@ -116,7 +163,7 @@ you only need to instanciate a factory to start using it.
|
||||
It is important to remember that fixtures are referenced by arbitrary
|
||||
identifiers ('myobj1'). These are internally mapped to their database identifiers.
|
||||
|
||||
:::
|
||||
:::php
|
||||
$databaseId = $factory->getId('MyClass', 'myobj1');
|
||||
|
||||
In order to create an object with certain properties, just add a second argument:
|
||||
@ -124,7 +171,7 @@ In order to create an object with certain properties, just add a second argument
|
||||
:::php
|
||||
$obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value'));
|
||||
|
||||
### Default Properties
|
||||
#### Default Properties
|
||||
|
||||
Blueprints can be overwritten in order to customize their behaviour,
|
||||
for example with default properties in case none are passed into `createObject()`.
|
||||
@ -134,11 +181,10 @@ for example with default properties in case none are passed into `createObject()
|
||||
'MyProperty' => 'My Default Value'
|
||||
));
|
||||
|
||||
### Dependent Properties
|
||||
#### Dependent Properties
|
||||
|
||||
Values can be set on demand through anonymous functions,
|
||||
which can either generate random defaults, or create
|
||||
composite values based on other fixture data.
|
||||
Values can be set on demand through anonymous functions, which can either generate random defaults,
|
||||
or create composite values based on other fixture data.
|
||||
|
||||
:::php
|
||||
$factory->define('Member', array(
|
||||
@ -152,7 +198,7 @@ composite values based on other fixture data.
|
||||
}
|
||||
));
|
||||
|
||||
### Relations
|
||||
#### Relations
|
||||
|
||||
Model relations can be expressed through the same notation as in the YAML fixture format
|
||||
described earlier, through the `=>` prefix on data values.
|
||||
@ -162,7 +208,7 @@ described earlier, through the `=>` prefix on data values.
|
||||
'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2'
|
||||
));
|
||||
|
||||
### Callbacks
|
||||
#### Callbacks
|
||||
|
||||
Sometimes new model instances need to be modified in ways which can't be expressed
|
||||
in their properties, for example to publish a page, which requires a method call.
|
||||
@ -195,7 +241,7 @@ By default, blueprint names equal the class names they manage.
|
||||
$obj->Groups()->add($adminGroup);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$member = $factory->createObject('Member'); // not in admin group
|
||||
$admin = $factory->createObject('AdminMember'); // in admin group
|
||||
|
||||
|
46
docs/en/topics/testing/glossary.md
Normal file
46
docs/en/topics/testing/glossary.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Glossary
|
||||
|
||||
**Assertion:** A predicate statement that must be true when a test runs.
|
||||
|
||||
**Behat:** A behaviour-driven testing library used with SilverStripe as a higher-level
|
||||
alternative to the `FunctionalTest` API, see [http://behat.org](http://behat.org).
|
||||
|
||||
**Test Case:** The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the
|
||||
base test case.
|
||||
|
||||
**Test Suite:** Also known as a 'test group', a composite of test cases, used to collect individual unit tests into
|
||||
packages, allowing all tests to be run at once.
|
||||
|
||||
**Fixture:** Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be
|
||||
in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be
|
||||
used to create fixtures for the duration of a test - `setUp` - and clean them up after the test is done - `tearDown'.
|
||||
|
||||
**Refactoring:** A behavior preserving transformation of code. If you change the code, while keeping the actual
|
||||
functionality the same, it is refactoring. If you change the behavior or add new functionality it's not.
|
||||
|
||||
**Smell:** A code smell is a symptom of a problem. Usually refers to code that is structured in a way that will lead to
|
||||
problems with maintenance or understanding.
|
||||
|
||||
**Spike:** A limited and throwaway sketch of code or experiment to get a feel for how long it will take to implement a
|
||||
certain feature, or a possible direction for how that feature might work.
|
||||
|
||||
**Test Double:** Also known as a 'Substitute'. A general term for a dummy object that replaces a real object with the
|
||||
same interface. Substituting objects is useful when a real object is difficult or impossible to incorporate into a unit
|
||||
test.
|
||||
|
||||
**Fake Object**: A substitute object that simply replaces a real object with the same interface, and returns a
|
||||
pre-determined (usually fixed) value from each method.
|
||||
|
||||
**Mock Object:** A substitute object that mimics the same behavior as a real object (some people think of mocks as
|
||||
"crash test dummy" objects). Mocks differ from other kinds of substitute objects in that they must understand the
|
||||
context of each call to them, setting expectations of which, and what order, methods will be invoked and what parameters
|
||||
will be passed.
|
||||
|
||||
**Test-Driven Development (TDD):** A style of programming where tests for a new feature are constructed before any code
|
||||
is written. Code to implement the feature is then written with the aim of making the tests pass. Testing is used to
|
||||
understand the problem space and discover suitable APIs for performing specific actions.
|
||||
|
||||
**Behavior Driven Development (BDD):** An extension of the test-driven programming style, where tests are used primarily
|
||||
for describing the specification of how code should perform. In practice, there's little or no technical difference - it
|
||||
all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so *Specification*
|
||||
is used in place of *Test Case*, and *should* is used in place of *expect* and *assert*.
|
@ -2,105 +2,65 @@
|
||||
|
||||
The SilverStripe core contains various features designed to simplify the process of creating and managing automated tests.
|
||||
|
||||
* [Create a unit test](create-silverstripe-test): Writing tests to check core data objects
|
||||
* [Creating a functional test](create-functional-test): An overview of functional tests and how to write a functional test
|
||||
* [Email Sending](email-sending): An overview of the built-in email testing code
|
||||
* [Troubleshooting](testing-guide-troubleshooting): Frequently asked questions list for testing issues
|
||||
* [Why Unit Test?](why-test): Why should you test and how to start testing
|
||||
|
||||
If you are familiar with PHP coding but new to unit testing, you should read the [Introduction](/topics/testing) and
|
||||
check out Mark's presentation [Getting to Grips with SilverStripe Testing](http://www.slideshare.net/maetl/getting-to-grips-with-silverstripe-testing).
|
||||
This section's page [Why Unit Test?](why-should-i-test) will give you the reasons behind why you should be testing your
|
||||
code.
|
||||
|
||||
You should also read over [the PHPUnit manual](http://www.phpunit.de/manual/current/en/). It provides a lot of
|
||||
fundamental concepts that we build on in this documentation.
|
||||
|
||||
If you're more familiar with unit testing, but want a refresher of some of the concepts and terminology, you can browse
|
||||
the [Testing Glossary](#glossary).
|
||||
To get started now, follow the installation instructions below, and check
|
||||
[Troubleshooting](/topics/testing/testing-guide-troubleshooting) in case you run into any problems.
|
||||
the [Testing Glossary](glossary). To get started now, follow the installation instructions below, and check
|
||||
[Troubleshooting](troubleshooting) in case you run into any problems.
|
||||
|
||||
## Installation
|
||||
|
||||
### Via Composer
|
||||
|
||||
Unit tests are not included in the normal SilverStripe downloads,
|
||||
you are expected to work with local git repositories
|
||||
Unit tests are not included in the normal SilverStripe downloads, you are expected to work with local git repositories
|
||||
([installation instructions](/topics/installation/composer)).
|
||||
|
||||
Once you've got the project up and running,
|
||||
check out the additional requirements to run unit tests:
|
||||
Once you've got the project up and running, check out the additional requirements to run unit tests:
|
||||
|
||||
composer update --dev
|
||||
|
||||
The will install (among other things) the [PHPUnit](http://www.phpunit.de/) dependency,
|
||||
which is the framework we're building our unit tests on.
|
||||
Composer installs it alongside the required PHP classes into the `vendor/bin/` directory.
|
||||
You can either use it through its full path (`vendor/bin/phpunit`), or symlink it
|
||||
into the root directory of your website:
|
||||
This will install (among other things) the [PHPUnit](http://www.phpunit.de/) dependency, which is the framework we're
|
||||
building our unit tests on. Composer installs it alongside the required PHP classes into the `vendor/bin/` directory.
|
||||
You can either use it through its full path (`vendor/bin/phpunit`), or symlink it into the root directory of your website:
|
||||
|
||||
ln -s vendor/bin/phpunit phpunit
|
||||
|
||||
### Via PEAR
|
||||
|
||||
Alternatively, you can check out phpunit globally via the PEAR packanage manager
|
||||
Alternatively, you can check out PHPUnit globally via the PEAR packanage manager
|
||||
([instructions](https://github.com/sebastianbergmann/phpunit/)).
|
||||
|
||||
pear config-set auto_discover 1
|
||||
pear install pear.phpunit.de/PHPUnit
|
||||
|
||||
## Running Tests
|
||||
## Configuration
|
||||
|
||||
### Via the "phpunit" Binary on Command Line
|
||||
### phpunit.xml
|
||||
|
||||
The `phpunit` binary should be used from the root directory of your website.
|
||||
The `phpunit` executable can be configured by commandline arguments or through an XML file. File-based configuration has
|
||||
the advantage of enforcing certain rules across test executions (e.g. excluding files from code coverage reports), and
|
||||
of course this information can be version controlled and shared with other team members.
|
||||
|
||||
# Runs all tests defined in phpunit.xml
|
||||
phpunit
|
||||
|
||||
# Run all tests of a specific module
|
||||
phpunit framework/tests/
|
||||
|
||||
# Run specific tests within a specific module
|
||||
phpunit framework/tests/filesystem
|
||||
|
||||
# Run a specific test
|
||||
phpunit framework/tests/filesystem/FolderTest.php
|
||||
|
||||
# Run tests with optional `$_GET` parameters (you need an empty second argument)
|
||||
phpunit framework/tests '' flush=all
|
||||
**Note: This doesn't apply for running tests through the "sake" wrapper**
|
||||
|
||||
All command-line arguments are documented on
|
||||
[phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
|
||||
SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point. Copy the file into a new
|
||||
`phpunit.xml` and customize to your needs - PHPUnit will auto-detect its existence, and prioritize it over the default
|
||||
file.
|
||||
|
||||
### Via the "sake" Wrapper on Command Line
|
||||
There's nothing stopping you from creating multiple XML files (see the `--configuration` flag in
|
||||
[PHPUnit documentation](http://www.phpunit.de/manual/current/en/textui.html)). For example, you could have a
|
||||
`phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below).
|
||||
|
||||
The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized
|
||||
"[api:TestRunner]" class that handles the PHPUnit configuration and output formatting.
|
||||
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
|
||||
particularly around formatting test output.
|
||||
### Database Permissions
|
||||
|
||||
# Run all tests
|
||||
sake dev/tests/all
|
||||
|
||||
# Run all tests of a specific module (comma-separated)
|
||||
sake dev/tests/module/framework,cms
|
||||
|
||||
# Run specific tests (comma-separated)
|
||||
sake dev/tests/FolderTest,OtherTest
|
||||
|
||||
# Run tests with optional `$_GET` parameters
|
||||
sake dev/tests/all flush=all
|
||||
|
||||
# Skip some tests
|
||||
sake dev/tests/all SkipTests=MySkippedTest
|
||||
|
||||
### Via Web Browser
|
||||
|
||||
Executing tests from the command line is recommended, since it most closely reflects
|
||||
test runs in any automated testing environments. If for some reason you don't have
|
||||
access to the command line, you can also run tests through the browser.
|
||||
|
||||
http://localhost/dev/tests
|
||||
SilverStripe tests create thier own database when they are run. Because of this the database user in your config file
|
||||
should have the appropriate permissions to create new databases on your server, otherwise tests will not run.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
@ -120,71 +80,60 @@ Some people may note that we have used the same naming convention as Ruby on Rai
|
||||
|
||||
Tutorials and recipes for creating tests using the SilverStripe framework:
|
||||
|
||||
* **[Create a SilverStripe Test](/topics/testing/create-silverstripe-test)**
|
||||
* **[Create a Functional Test](/topics/testing/create-functional-test)**
|
||||
* **[Test Outgoing Email Sending](/topics/testing/email-sending)**
|
||||
* [Creating a SilverStripe test](creating-a-silverstripe-test): Writing tests to check core data objects
|
||||
* [Creating a functional test](creating-a-functional-test): An overview of functional tests and how to write a functional test
|
||||
* [Testing Outgoing Email](testing-email): An overview of the built-in email testing code
|
||||
|
||||
## Configuration
|
||||
## Running Tests
|
||||
|
||||
### phpunit.xml
|
||||
### Via the "phpunit" Binary on Command Line
|
||||
|
||||
The `phpunit` executable can be configured by commandline arguments or through an XML file.
|
||||
File-based configuration has the advantage of enforcing certain rules across
|
||||
test executions (e.g. excluding files from code coverage reports), and of course this
|
||||
information can be version controlled and shared with other team members.
|
||||
The `phpunit` binary should be used from the root directory of your website.
|
||||
|
||||
**Note: This doesn't apply for running tests through the "sake" wrapper**
|
||||
# Runs all tests defined in phpunit.xml
|
||||
phpunit
|
||||
|
||||
SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point.
|
||||
Copy the file into a new `phpunit.xml` and customize to your needs - PHPUnit will auto-detect
|
||||
its existence, and prioritize it over the default file.
|
||||
# Run all tests of a specific module
|
||||
phpunit framework/tests/
|
||||
|
||||
There's nothing stopping you from creating multiple XML files (see the `--configuration` flag in [PHPUnit documentation](http://www.phpunit.de/manual/current/en/textui.html)).
|
||||
For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below).
|
||||
# Run specific tests within a specific module
|
||||
phpunit framework/tests/filesystem
|
||||
|
||||
## Glossary {#glossary}
|
||||
# Run a specific test
|
||||
phpunit framework/tests/filesystem/FolderTest.php
|
||||
|
||||
**Assertion:** A predicate statement that must be true when a test runs.
|
||||
# Run tests with optional `$_GET` parameters (you need an empty second argument)
|
||||
phpunit framework/tests '' flush=all
|
||||
|
||||
**Behat:** A behaviour-driven testing library used with SilverStripe as a higher-level
|
||||
alternative to the `FunctionalTest` API, see [http://behat.org](http://behat.org).
|
||||
All command-line arguments are documented on
|
||||
[phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
|
||||
|
||||
**Test Case:** The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the
|
||||
base test case.
|
||||
### Via the "sake" Wrapper on Command Line
|
||||
|
||||
**Test Suite:** Also known as a 'test group', a composite of test cases, used to collect individual unit tests into
|
||||
packages, allowing all tests to be run at once.
|
||||
The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized
|
||||
`[api:TestRunner]` class that handles the PHPUnit configuration and output formatting.
|
||||
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
|
||||
particularly around formatting test output.
|
||||
|
||||
**Fixture:** Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be
|
||||
in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be
|
||||
used to create fixtures for the duration of a test - `setUp` - and clean them up after the test is done - `tearDown'.
|
||||
# Run all tests
|
||||
sake dev/tests/all
|
||||
|
||||
**Refactoring:** A behavior preserving transformation of code. If you change the code, while keeping the actual
|
||||
functionality the same, it is refactoring. If you change the behavior or add new functionality it's not.
|
||||
# Run all tests of a specific module (comma-separated)
|
||||
sake dev/tests/module/framework,cms
|
||||
|
||||
**Smell:** A code smell is a symptom of a problem. Usually refers to code that is structured in a way that will lead to
|
||||
problems with maintenance or understanding.
|
||||
# Run specific tests (comma-separated)
|
||||
sake dev/tests/FolderTest,OtherTest
|
||||
|
||||
**Spike:** A limited and throwaway sketch of code or experiment to get a feel for how long it will take to implement a
|
||||
certain feature, or a possible direction for how that feature might work.
|
||||
# Run tests with optional `$_GET` parameters
|
||||
sake dev/tests/all flush=all
|
||||
|
||||
**Test Double:** Also known as a 'Substitute'. A general term for a dummy object that replaces a real object with the
|
||||
same interface. Substituting objects is useful when a real object is difficult or impossible to incorporate into a unit
|
||||
test.
|
||||
# Skip some tests
|
||||
sake dev/tests/all SkipTests=MySkippedTest
|
||||
|
||||
**Fake Object**: A substitute object that simply replaces a real object with the same interface, and returns a
|
||||
pre-determined (usually fixed) value from each method.
|
||||
### Via Web Browser
|
||||
|
||||
**Mock Object:** A substitute object that mimicks the same behavior as a real object (some people think of mocks as
|
||||
"crash test dummy" objects). Mocks differ from other kinds of substitute objects in that they must understand the
|
||||
context of each call to them, setting expectations of which, and what order, methods will be invoked and what parameters
|
||||
will be passed.
|
||||
Executing tests from the command line is recommended, since it most closely reflects
|
||||
test runs in any automated testing environments. If for some reason you don't have
|
||||
access to the command line, you can also run tests through the browser.
|
||||
|
||||
**Test-Driven Development (TDD):** A style of programming where tests for a new feature are constructed before any code
|
||||
is written. Code to implement the feature is then written with the aim of making the tests pass. Testing is used to
|
||||
understand the problem space and discover suitable APIs for performing specific actions.
|
||||
|
||||
**Behavior Driven Development (BDD):** An extension of the test-driven programming style, where tests are used primarily
|
||||
for describing the specification of how code should perform. In practice, there's little or no technical difference - it
|
||||
all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so *Specification*
|
||||
is used in place of *Test Case*, and *should* is used in place of *expect* and *assert*.
|
||||
http://localhost/dev/tests
|
||||
|
@ -1,11 +1,12 @@
|
||||
# Email Sending
|
||||
# Testing Email
|
||||
|
||||
SilverStripe's test system has built-in support for testing emails sent using the Email class.
|
||||
SilverStripe's test system has built-in support for testing emails sent using the `[api:Email]` class.
|
||||
|
||||
## How it works
|
||||
|
||||
For this to work, you need to send emails using the `Email` class, which is generally the way that we recommend you
|
||||
send emails in your SilverStripe application. Here is a simple example of how you might do this:
|
||||
For this to work, you need to send emails using the `Email` class,
|
||||
which is generally the way that we recommend you send emails in your SilverStripe application.
|
||||
Here is a simple example of how you might do this:
|
||||
|
||||
:::php
|
||||
$e = new Email();
|
||||
@ -14,35 +15,32 @@ send emails in your SilverStripe application. Here is a simple example of how y
|
||||
$e->Body = "I just really wanted to email you and say hi.";
|
||||
$e->send();
|
||||
|
||||
|
||||
Normally, the send() method would send an email using PHP's mail() function. However, if you are running a `[api:SapphireTest]`
|
||||
test, then it holds off actually sending the email, and instead lets you assert that an email was sent using this method.
|
||||
Normally, the `send()` method would send an email using PHP's `mail()` function.
|
||||
However, if you are running a `[api:SapphireTest]` test, then it holds off actually sending the email,
|
||||
and instead lets you assert that an email was sent using this method.
|
||||
|
||||
:::php
|
||||
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
|
||||
|
||||
The arguments are `$to`, `$from`, `$subject`, `$body`, and can take one of the three following types:
|
||||
|
||||
The arguments are `$to`, `$from`, `$subject`, `$body`, and can be take one of the following three forms:
|
||||
|
||||
* A string: match exactly that string
|
||||
* `null/false`: match anything
|
||||
* A PERL regular expression (starting with '/'): match that regular expression
|
||||
* A string: match exactly that string
|
||||
* `null/false`: match anything
|
||||
* A PERL regular expression (starting with '/'): match that regular expression
|
||||
|
||||
## How to use it
|
||||
|
||||
Given all of that, there is not a lot that you have to do in order to test emailing functionality in your application.
|
||||
|
||||
* Write your SilverStripe application, using the Email class to send emails.
|
||||
* Write tests that trigger the email sending functionality.
|
||||
* Include appropriate `$this->assertEmailSent()` calls in those tests.
|
||||
Whenever we include e-mailing functionality in our application,
|
||||
we simply use `$this->assertEmailSent()` to check our mail has been passed to PHP `mail` in our tests.
|
||||
|
||||
That's it!
|
||||
|
||||
## What isn't tested
|
||||
|
||||
It's important to realise that this email testing doesn't actually test everything that there is to do with email. The
|
||||
focus of this email testing system is testing that your application is triggering emails correctly. It doesn't test
|
||||
your email infrastructure outside of the webserver. For example:
|
||||
It's important to realise that this email testing doesn't actually test everything that there is to do with email.
|
||||
The focus of this email testing system is testing that your application is triggering emails correctly.
|
||||
It doesn't test your email infrastructure outside of the webserver. For example:
|
||||
|
||||
* It won't test that email is correctly configured on your webserver
|
||||
* It won't test whether your emails are going to be lost in someone's spam filter
|
||||
@ -50,9 +48,9 @@ your email infrastructure outside of the webserver. For example:
|
||||
|
||||
## How it's built
|
||||
|
||||
For those of you who want to dig a little deeper, here's a quick run-through of how the system has been built. As well
|
||||
as explaining how we built the email test, this is a good design pattern for making other "tricky external systems"
|
||||
testable:
|
||||
For those of you who want to dig a little deeper, here's a quick run-through of how the system has been built.
|
||||
As well as explaining how we built the email test,
|
||||
this is a good design pattern for making other "tricky external systems" testable:
|
||||
|
||||
1. The `Email::send()` method makes uses of a static object, `Email::$mailer`, to do the dirty work of calling
|
||||
mail(). The default mailer is an object of type `Mailer`, which performs a normal send.
|
@ -81,7 +81,7 @@ but also include information about when and how a record was published.
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
$versions = $record->allVersions();
|
||||
echo $versions->First()->Version; // instance of Versioned_Versoin
|
||||
echo $versions->First()->Version; // instance of Versioned_Version
|
||||
|
||||
### Writing Versions and Changing Stages
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
## Overview
|
||||
|
||||
|
||||
In the [first tutorial](1-building-a-basic-site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works.
|
||||
In the [first tutorial (Building a basic site)](1-building-a-basic-site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works.
|
||||
|
||||
## What are we working towards?
|
||||
|
||||
@ -54,7 +54,7 @@ A more in-depth introduction of Model-View-Controller can be found
|
||||
|
||||
## Creating the news section page types
|
||||
|
||||
To create a news section we'll need two new page types. The first one is obvious: we need an *ArticlePage* page type. The second is a little less obvious: we need an *ArticleHolder* page type to contain our article pages.
|
||||
To create a News section we'll need two new page types. The first one is obvious: we need an *ArticlePage* page type. The second is a little less obvious: we need an *ArticleHolder* page type to contain our article pages.
|
||||
|
||||
We'll start with the *ArticlePage* page type. First we create the model, a class called "ArticlePage". We put the *ArticlePage* class into a file called "ArticlePage.php" inside *mysite/code*. All other classes relating to *ArticlePage* should be placed within "ArticlePage.php", this includes our controller (*ArticlePage_Controller*).
|
||||
|
||||
@ -69,7 +69,7 @@ We'll start with the *ArticlePage* page type. First we create the model, a class
|
||||
|
||||
|
||||
|
||||
Here we've created our data object/controller pair, but we haven't extended them at all. Don't worry about the *$db* and *$has_one* arrays just yet, we'll explain them shortly. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
|
||||
Here we've created our data object/controller pair, but we haven't extended them at all. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
|
||||
to specifically create the view for this page type.
|
||||
|
||||
Let's create the *ArticleHolder* page type.
|
||||
@ -100,8 +100,8 @@ page type "News", it would conflict with the page name also called "News".
|
||||
|
||||
## Adding date and author fields
|
||||
|
||||
Now that we have an *ArticlePage* page type, let's make it a little more useful. Remember the *$db* array? We can use
|
||||
this array to add extra fields to the database. It would be nice to know when each article was posted, and who posted
|
||||
Now that we have an *ArticlePage* page type, let's make it a little more useful. We can use
|
||||
the $db array to add extra fields to the database. It would be nice to know when each article was posted, and who posted
|
||||
it. Add a *$db* property definition in the *ArticlePage* class:
|
||||
|
||||
:::php
|
||||
@ -188,7 +188,7 @@ Now that we have created our page types, let's add some content. Go into the CMS
|
||||
At the moment, your date field will look just like a text field.
|
||||
This makes it confusing and doesn't give the user much help when adding a date.
|
||||
|
||||
To make the date field a bit more user friendly, you can add a dropdown calendar, set the date format and add better title. By default,
|
||||
To make the date field a bit more user friendly, you can add a dropdown calendar, set the date format and add a better title. By default,
|
||||
the date field will have the date format defined by your locale.
|
||||
|
||||
:::php
|
||||
@ -213,7 +213,7 @@ Let's walk through these changes.
|
||||
:::php
|
||||
$fields->addFieldToTab('Root.Main', $dateField = new DateField('Date','Article Date (for example: 20/12/2010)'), 'Content');
|
||||
|
||||
*$dateField* is declared only to in order to change the configuration of the DateField.
|
||||
*$dateField* is declared in order to change the configuration of the DateField.
|
||||
|
||||
:::php
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
@ -303,7 +303,7 @@ Here we use the page control *Children*. As the name suggests, this control allo
|
||||
|
||||
### Using include files in templates
|
||||
|
||||
We can make our templates more modular and easier to maintain by separating commonly-used components in to *include files*. We are already familiar with the `<% include Sidebar %>` line from looking at the menu in the [first tutorial](1-building-a-basic-site).
|
||||
We can make our templates more modular and easier to maintain by separating commonly-used components in to *include files*. We are already familiar with the `<% include Sidebar %>` line from looking at the menu in the [first tutorial (Building a basic site)](1-building-a-basic-site).
|
||||
|
||||
We'll separate the display of linked articles as we want to reuse this code later on.
|
||||
|
||||
@ -331,7 +331,7 @@ Paste the code that was in ArticleHolder into a new include file called ArticleT
|
||||
|
||||
### Changing the icons of pages in the CMS
|
||||
|
||||
Let's now make a purely cosmetic change that nevertheless helps to make the information presented in the CMS clearer.
|
||||
Now let's make a purely cosmetic change that nevertheless helps to make the information presented in the CMS clearer.
|
||||
Add the following field to the *ArticleHolder* and *ArticlePage* classes:
|
||||
|
||||
:::php
|
||||
|
@ -24,6 +24,10 @@ class Folder extends File {
|
||||
private static $plural_name = "Folders";
|
||||
|
||||
private static $default_sort = "\"Name\"";
|
||||
|
||||
private static $casting = array (
|
||||
'TreeTitle' => 'HTMLText'
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -184,7 +184,7 @@ class ImagickBackend extends Imagick implements Image_Backend {
|
||||
* @param int $height
|
||||
* @return Image_Backend
|
||||
*/
|
||||
public function paddedResize($width, $height, $backgroundColor = "FFFFFF") {
|
||||
public function paddedResize($width, $height, $backgroundColor = "#FFFFFF00") {
|
||||
if(!$this->valid()) return;
|
||||
|
||||
$width = round($width);
|
||||
|
@ -35,6 +35,9 @@ class CreditCardField extends TextField {
|
||||
* @return string
|
||||
*/
|
||||
protected function getTabIndexHTML($increment = 0) {
|
||||
// we can't add a tabindex if there hasn't been one set yet.
|
||||
if($this->getAttribute('tabindex') === null) return false;
|
||||
|
||||
$tabIndex = (int)$this->getAttribute('tabindex') + (int)$increment;
|
||||
return (is_numeric($tabIndex)) ? ' tabindex = "' . $tabIndex . '"' : '';
|
||||
}
|
||||
|
@ -217,6 +217,8 @@ class DateField extends TextField {
|
||||
* @param String|Array $val
|
||||
*/
|
||||
public function setValue($val) {
|
||||
$locale = new Zend_Locale($this->locale);
|
||||
|
||||
if(empty($val)) {
|
||||
$this->value = null;
|
||||
$this->valueObj = null;
|
||||
@ -226,7 +228,7 @@ class DateField extends TextField {
|
||||
if(is_array($val) && $this->validateArrayValue($val)) {
|
||||
// set() gets confused with custom date formats when using array notation
|
||||
if(!(empty($val['day']) || empty($val['month']) || empty($val['year']))) {
|
||||
$this->valueObj = new Zend_Date($val, null, $this->locale);
|
||||
$this->valueObj = new Zend_Date($val, null, $locale);
|
||||
$this->value = $this->valueObj->toArray();
|
||||
} else {
|
||||
$this->value = $val;
|
||||
@ -234,8 +236,8 @@ class DateField extends TextField {
|
||||
}
|
||||
}
|
||||
// load ISO date from database (usually through Form->loadDataForm())
|
||||
else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) {
|
||||
$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'), $this->locale);
|
||||
else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $locale)) {
|
||||
$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'), $locale);
|
||||
$this->value = $this->valueObj->toArray();
|
||||
}
|
||||
else {
|
||||
@ -247,15 +249,15 @@ class DateField extends TextField {
|
||||
// Caution: Its important to have this check *before* the ISO date fallback,
|
||||
// as some dates are falsely detected as ISO by isDate(), e.g. '03/04/03'
|
||||
// (en_NZ for 3rd of April, definetly not yyyy-MM-dd)
|
||||
if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('dateformat'), $this->locale)) {
|
||||
$this->valueObj = new Zend_Date($val, $this->getConfig('dateformat'), $this->locale);
|
||||
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $this->locale);
|
||||
if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('dateformat'), $locale)) {
|
||||
$this->valueObj = new Zend_Date($val, $this->getConfig('dateformat'), $locale);
|
||||
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $locale);
|
||||
|
||||
}
|
||||
// load ISO date from database (usually through Form->loadDataForm())
|
||||
else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'))) {
|
||||
$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'));
|
||||
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $this->locale);
|
||||
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $locale);
|
||||
}
|
||||
else {
|
||||
$this->value = $val;
|
||||
@ -367,7 +369,7 @@ class DateField extends TextField {
|
||||
} else {
|
||||
$minDate = new Zend_Date(strftime('%Y-%m-%d', strtotime($min)), $this->getConfig('datavalueformat'));
|
||||
}
|
||||
if(!$this->valueObj->isLater($minDate) && !$this->valueObj->equals($minDate)) {
|
||||
if(!$this->valueObj || (!$this->valueObj->isLater($minDate) && !$this->valueObj->equals($minDate))) {
|
||||
$validator->validationError(
|
||||
$this->name,
|
||||
_t(
|
||||
@ -388,7 +390,7 @@ class DateField extends TextField {
|
||||
} else {
|
||||
$maxDate = new Zend_Date(strftime('%Y-%m-%d', strtotime($max)), $this->getConfig('datavalueformat'));
|
||||
}
|
||||
if(!$this->valueObj->isEarlier($maxDate) && !$this->valueObj->equals($maxDate)) {
|
||||
if(!$this->valueObj || (!$this->valueObj->isEarlier($maxDate) && !$this->valueObj->equals($maxDate))) {
|
||||
$validator->validationError(
|
||||
$this->name,
|
||||
_t('DateField.VALIDDATEMAXDATE',
|
||||
|
@ -115,6 +115,8 @@ class DatetimeField extends FormField {
|
||||
* the 'date' value may contain array notation was well (see {@link DateField->setValue()}).
|
||||
*/
|
||||
public function setValue($val) {
|
||||
$locale = new Zend_Locale($this->locale);
|
||||
|
||||
// If timezones are enabled, assume user data needs to be reverted to server timezone
|
||||
if($this->getConfig('usertimezone')) {
|
||||
// Accept user input on timezone, but only when timezone support is enabled
|
||||
@ -130,7 +132,7 @@ class DatetimeField extends FormField {
|
||||
$this->timeField->setValue(null);
|
||||
} else {
|
||||
// Case 1: String setting from database, in ISO date format
|
||||
if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) {
|
||||
if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $locale)) {
|
||||
$this->value = $val;
|
||||
}
|
||||
// Case 2: Array form submission with user date format
|
||||
@ -145,13 +147,13 @@ class DatetimeField extends FormField {
|
||||
$this->dateField->setValue($val['date']);
|
||||
$this->timeField->setValue($val['time']);
|
||||
if($this->dateField->dataValue() && $this->timeField->dataValue()) {
|
||||
$userValueObj = new Zend_Date(null, null, $this->locale);
|
||||
$userValueObj = new Zend_Date(null, null, $locale);
|
||||
$userValueObj->setDate($this->dateField->dataValue(),
|
||||
$this->dateField->getConfig('datavalueformat'));
|
||||
$userValueObj->setTime($this->timeField->dataValue(),
|
||||
$this->timeField->getConfig('datavalueformat'));
|
||||
if($userTz) $userValueObj->setTimezone($dataTz);
|
||||
$this->value = $userValueObj->get($this->getConfig('datavalueformat'), $this->locale);
|
||||
$this->value = $userValueObj->get($this->getConfig('datavalueformat'), $locale);
|
||||
unset($userValueObj);
|
||||
} else {
|
||||
// Validation happens later, so set the raw string in case Zend_Date doesn't accept it
|
||||
@ -168,8 +170,8 @@ class DatetimeField extends FormField {
|
||||
}
|
||||
|
||||
// view settings (dates might differ from $this->value based on user timezone settings)
|
||||
if (Zend_Date::isDate($this->value, $this->getConfig('datavalueformat'), $this->locale)) {
|
||||
$valueObj = new Zend_Date($this->value, $this->getConfig('datavalueformat'), $this->locale);
|
||||
if (Zend_Date::isDate($this->value, $this->getConfig('datavalueformat'), $locale)) {
|
||||
$valueObj = new Zend_Date($this->value, $this->getConfig('datavalueformat'), $locale);
|
||||
if($userTz) $valueObj->setTimezone($userTz);
|
||||
|
||||
// Set view values in sub-fields
|
||||
@ -177,9 +179,9 @@ class DatetimeField extends FormField {
|
||||
$this->dateField->setValue($valueObj->toArray());
|
||||
} else {
|
||||
$this->dateField->setValue(
|
||||
$valueObj->get($this->dateField->getConfig('dateformat'), $this->locale));
|
||||
$valueObj->get($this->dateField->getConfig('dateformat'), $locale));
|
||||
}
|
||||
$this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $this->locale));
|
||||
$this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $locale));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,6 +155,10 @@ class Form extends RequestHandler {
|
||||
'forTemplate',
|
||||
);
|
||||
|
||||
private static $casting = array(
|
||||
'Message' => 'Text'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var FormTemplateHelper
|
||||
*/
|
||||
@ -256,6 +260,8 @@ class Form extends RequestHandler {
|
||||
if(isset($errorInfo['message']) && isset($errorInfo['type'])) {
|
||||
$this->setMessage($errorInfo['message'], $errorInfo['type']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -504,7 +510,7 @@ class Form extends RequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error message to a field on this form. It will be saved into the session
|
||||
* Add a plain text error message to a field on this form. It will be saved into the session
|
||||
* and used the next time this form is displayed.
|
||||
*/
|
||||
public function addErrorMessage($fieldName, $message, $messageType) {
|
||||
@ -1425,7 +1431,7 @@ class Form extends RequestHandler {
|
||||
$this->getTemplate(),
|
||||
'Form'
|
||||
));
|
||||
|
||||
|
||||
$return = $view->dontRewriteHashlinks()->process($this);
|
||||
|
||||
// Now that we're rendered, clear message
|
||||
@ -1626,14 +1632,12 @@ class Form extends RequestHandler {
|
||||
* @return Form
|
||||
*/
|
||||
public function addExtraClass($class) {
|
||||
$classes = explode(' ', $class);
|
||||
|
||||
//split at white space
|
||||
$classes = preg_split('/\s+/', $class);
|
||||
foreach($classes as $class) {
|
||||
$value = trim($class);
|
||||
|
||||
$this->extraClasses[] = $value;
|
||||
//add classes one by one
|
||||
$this->extraClasses[$class] = $class;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -1644,9 +1648,12 @@ class Form extends RequestHandler {
|
||||
* @param string $class
|
||||
*/
|
||||
public function removeExtraClass($class) {
|
||||
$classes = explode(' ', $class);
|
||||
$this->extraClasses = array_diff($this->extraClasses, $classes);
|
||||
|
||||
//split at white space
|
||||
$classes = preg_split('/\s+/', $class);
|
||||
foreach ($classes as $class) {
|
||||
//unset one by one
|
||||
unset($this->extraClasses[$class]);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,10 @@ class FormField extends RequestHandler {
|
||||
*/
|
||||
protected $attributes = array();
|
||||
|
||||
private static $casting = array(
|
||||
'Message' => 'Text'
|
||||
);
|
||||
|
||||
/**
|
||||
* Takes a fieldname and converts camelcase to spaced
|
||||
* words. Also resolves combined fieldnames with dot syntax
|
||||
@ -223,7 +227,7 @@ class FormField extends RequestHandler {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the field message, used by form validation.
|
||||
*
|
||||
* Use {@link setError()} to set this property.
|
||||
@ -352,24 +356,32 @@ class FormField extends RequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a CSS-class to the formfield-container.
|
||||
* Add one or more CSS-classes to the formfield-container.
|
||||
*
|
||||
* @param $class String
|
||||
*/
|
||||
public function addExtraClass($class) {
|
||||
//split at white space to extract all the classes
|
||||
$classes = preg_split('/\s+/', $class);
|
||||
foreach ($classes as $class) {
|
||||
//add each class one by one
|
||||
$this->extraClasses[$class] = $class;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a CSS-class from the formfield-container.
|
||||
* Remove one or more CSS-classes from the formfield-container.
|
||||
*
|
||||
* @param $class String
|
||||
*/
|
||||
public function removeExtraClass($class) {
|
||||
$pos = array_search($class, $this->extraClasses);
|
||||
if($pos !== false) unset($this->extraClasses[$pos]);
|
||||
|
||||
//split at white space to extract all the classes
|
||||
$classes = preg_split('/\s+/', $class);
|
||||
foreach ($classes as $class) {
|
||||
//unset each class one by one
|
||||
unset($this->extraClasses[$class]);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -419,7 +431,7 @@ class FormField extends RequestHandler {
|
||||
'id' => $this->ID(),
|
||||
'disabled' => $this->isDisabled(),
|
||||
);
|
||||
|
||||
|
||||
if ($this->Required()) {
|
||||
$attrs['required'] = 'required';
|
||||
$attrs['aria-required'] = 'true';
|
||||
@ -847,8 +859,8 @@ class FormField extends RequestHandler {
|
||||
$clone->setDisabled(true);
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function transform(FormTransformation $trans) {
|
||||
return $trans->transform($this);
|
||||
@ -878,7 +890,7 @@ class FormField extends RequestHandler {
|
||||
public function createTag($tag, $attributes, $content = null) {
|
||||
Deprecation::notice('3.2', 'Use FormField::create_tag()');
|
||||
return self::create_tag($tag, $attributes, $content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method each {@link FormField} subclass must implement,
|
||||
@ -949,7 +961,7 @@ class FormField extends RequestHandler {
|
||||
if(is_object($this->containerFieldList)) return $this->containerFieldList->rootFieldList();
|
||||
else user_error("rootFieldList() called on $this->class object without a containerFieldList", E_USER_ERROR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns another instance of this field, but "cast" to a different class.
|
||||
* The logic tries to retain all of the instance properties,
|
||||
@ -982,7 +994,7 @@ class FormField extends RequestHandler {
|
||||
// of the field, e.g. its "type" attribute.
|
||||
foreach($this->attributes as $k => $v) {
|
||||
$field->setAttribute($k, $v);
|
||||
}
|
||||
}
|
||||
$field->dontEscape = $this->dontEscape;
|
||||
|
||||
return $field;
|
||||
|
@ -86,7 +86,7 @@ class TreeDropdownField extends FormField {
|
||||
* entering the text in the input field.
|
||||
*/
|
||||
public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID',
|
||||
$labelField = 'TreeTitle', $showSearch = false
|
||||
$labelField = 'TreeTitle', $showSearch = true
|
||||
) {
|
||||
|
||||
$this->sourceObject = $sourceObject;
|
||||
@ -181,8 +181,12 @@ class TreeDropdownField extends FormField {
|
||||
if($record) {
|
||||
$title = $record->{$this->labelField};
|
||||
} else {
|
||||
if($this->showSearch){
|
||||
$title = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown');
|
||||
}else{
|
||||
$title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Implement for TreeMultiSelectField
|
||||
$metadata = array(
|
||||
@ -265,9 +269,23 @@ class TreeDropdownField extends FormField {
|
||||
$obj->markToExpose($this->objectForKey($value));
|
||||
}
|
||||
}
|
||||
$eval = '"<li id=\"selector-' . $this->getName() . '-{$child->' . $this->keyField . '}\" data-id=\"$child->'
|
||||
. $this->keyField . '\" class=\"class-$child->class"'
|
||||
. ' . $child->markingClasses() . "\"><a rel=\"$child->ID\">" . $child->' . $this->labelField . ' . "</a>"';
|
||||
|
||||
$self = $this;
|
||||
$escapeLabelField = ($obj->escapeTypeForField($this->labelField) != 'xml');
|
||||
$titleFn = function(&$child) use(&$self, $escapeLabelField) {
|
||||
$keyField = $self->keyField;
|
||||
$labelField = $self->labelField;
|
||||
return sprintf(
|
||||
'<li id="selector-%s-%s" data-id="%s" class="class-%s %s"><a rel="%d">%s</a>',
|
||||
Convert::raw2xml($self->getName()),
|
||||
Convert::raw2xml($child->$keyField),
|
||||
Convert::raw2xml($child->$keyField),
|
||||
Convert::raw2xml($child->class),
|
||||
Convert::raw2xml($child->markingClasses()),
|
||||
(int)$child->ID,
|
||||
$escapeLabelField ? Convert::raw2xml($child->$labelField) : $child->$labelField
|
||||
);
|
||||
};
|
||||
|
||||
// Limit the amount of nodes shown for performance reasons.
|
||||
// Skip the check if we're filtering the tree, since its not clear how many children will
|
||||
@ -290,7 +308,7 @@ class TreeDropdownField extends FormField {
|
||||
if($isSubTree) {
|
||||
$html = $obj->getChildrenAsUL(
|
||||
"",
|
||||
$eval,
|
||||
$titleFn,
|
||||
null,
|
||||
true,
|
||||
$this->childrenMethod,
|
||||
@ -303,7 +321,7 @@ class TreeDropdownField extends FormField {
|
||||
} else {
|
||||
$html = $obj->getChildrenAsUL(
|
||||
'class="tree"',
|
||||
$eval,
|
||||
$titleFn,
|
||||
null,
|
||||
true,
|
||||
$this->childrenMethod,
|
||||
@ -385,10 +403,32 @@ class TreeDropdownField extends FormField {
|
||||
*/
|
||||
protected function populateIDs() {
|
||||
// get all the leaves to be displayed
|
||||
if ( $this->searchCallback )
|
||||
if ($this->searchCallback) {
|
||||
$res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
|
||||
else
|
||||
$res = DataObject::get($this->sourceObject, "\"$this->labelField\" LIKE '%$this->search%'");
|
||||
} else {
|
||||
$sourceObject = $this->sourceObject;
|
||||
$wheres = array();
|
||||
if(singleton($sourceObject)->hasDatabaseField($this->labelField)) {
|
||||
$wheres[] = "\"$searchField\" LIKE '%$this->search%'";
|
||||
} else {
|
||||
if(singleton($sourceObject)->hasDatabaseField('Title')) {
|
||||
$wheres[] = "\"Title\" LIKE '%$this->search%'";
|
||||
}
|
||||
if(singleton($sourceObject)->hasDatabaseField('Name')) {
|
||||
$wheres[] = "\"Name\" LIKE '%$this->search%'";
|
||||
}
|
||||
}
|
||||
|
||||
if(!$wheres) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot query by %s.%s, not a valid database column',
|
||||
$sourceObject,
|
||||
$this->labelField
|
||||
));
|
||||
}
|
||||
|
||||
$res = DataObject::get($this->sourceObject, implode(' OR ', $wheres));
|
||||
}
|
||||
|
||||
if( $res ) {
|
||||
// iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
|
||||
|
@ -231,6 +231,11 @@ class GridFieldAddExistingAutocompleter
|
||||
|
||||
$json = array();
|
||||
foreach($results as $result) {
|
||||
// Prevent a circular reference and associated error in CMS/admin
|
||||
$hideFromSearch = ($gridField->getForm()->getRecord() && ($result->ID == $gridField->getForm()->getRecord()->ID));
|
||||
if($hideFromSearch) {
|
||||
continue;
|
||||
}
|
||||
$json[$result->ID] = SSViewer::fromString($this->resultsFormat)->process($result);
|
||||
}
|
||||
return Convert::array2json($json);
|
||||
|
@ -21,6 +21,7 @@ class GridFieldButtonRow implements GridField_HTMLProvider {
|
||||
|
||||
public function getHTMLFragments( $gridField) {
|
||||
$data = new ArrayData(array(
|
||||
"TargetFragmentName" => $this->targetFragment,
|
||||
"LeftFragment" => "\$DefineFragment(buttons-{$this->targetFragment}-left)",
|
||||
"RightFragment" => "\$DefineFragment(buttons-{$this->targetFragment}-right)",
|
||||
));
|
||||
|
@ -95,10 +95,15 @@ class GridFieldDataColumns implements GridField_ColumnProvider {
|
||||
|
||||
/**
|
||||
* Specify custom formatting for fields, e.g. to render a link instead of pure text.
|
||||
*
|
||||
* Caution: Make sure to escape special php-characters like in a normal php-statement.
|
||||
* Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'.
|
||||
*
|
||||
* Alternatively, pass a anonymous function, which takes two parameters:
|
||||
* The value and the original list item.
|
||||
* The value and the original list item.
|
||||
*
|
||||
* Formatting is applied after field casting, so if you're modifying the string
|
||||
* to include further data through custom formatting, ensure it's correctly escaped.
|
||||
*
|
||||
* @param array $formatting
|
||||
*/
|
||||
|
@ -53,7 +53,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
|
||||
$button->setAttribute('data-icon', 'download-csv');
|
||||
$button->addExtraClass('no-ajax');
|
||||
return array(
|
||||
$this->targetFragment => '<p class="grid-bottom-button grid-csv-button">' . $button->Field() . '</p>',
|
||||
$this->targetFragment => '<p class="grid-csv-button">' . $button->Field() . '</p>',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2233,12 +2233,10 @@ class i18n extends Object implements TemplateGlobalProvider {
|
||||
// TODO Replace with CLDR list of actually available languages/regions
|
||||
// Only allow explicitly registered locales, otherwise we'll get into trouble
|
||||
// if the locale doesn't exist in Zend's CLDR data
|
||||
$labelLocale = str_replace('-', '_', self::get_locale_from_lang($locale));
|
||||
if(isset(self::$all_locales[$locale])) {
|
||||
$locales[$locale] = self::$all_locales[$locale];
|
||||
} else if(isset(self::$all_locales[$labelLocale])) {
|
||||
$locales[$locale] = self::$all_locales[$labelLocale];
|
||||
}
|
||||
$fullLocale = self::get_locale_from_lang($locale);
|
||||
if(isset($allLocales[$fullLocale])) {
|
||||
$locales[$fullLocale] = $allLocales[$fullLocale];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2356,7 +2354,7 @@ class i18n extends Object implements TemplateGlobalProvider {
|
||||
public static function get_locale_from_lang($lang) {
|
||||
$subtags = Config::inst()->get('i18n', 'likely_subtags');
|
||||
if(preg_match('/\-|_/', $lang)) {
|
||||
return $lang;
|
||||
return str_replace('-', '_', $lang);
|
||||
} else if(isset($subtags[$lang])) {
|
||||
return $subtags[$lang];
|
||||
} else {
|
||||
|
@ -77,8 +77,8 @@
|
||||
});
|
||||
} else {
|
||||
checkboxes.each(function() {
|
||||
$(this).attr('checked', '');
|
||||
$(this).attr('disabled', '');
|
||||
$(this).prop('checked', false);
|
||||
$(this).prop('disabled', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,8 @@
|
||||
|
||||
var strings = {
|
||||
'openlink': 'Open',
|
||||
'fieldTitle': '(choose)',
|
||||
'searchFieldTitle': '(choose or search)'
|
||||
'fieldTitle': '(Choose)',
|
||||
'searchFieldTitle': '(Choose or Search)'
|
||||
};
|
||||
|
||||
var _clickTestFn = function(e) {
|
||||
@ -280,24 +280,14 @@
|
||||
$('.TreeDropdownField.searchable').entwine({
|
||||
onadd: function() {
|
||||
this._super();
|
||||
|
||||
var title = decodeURIComponent(this.data('title'));
|
||||
this.find('.treedropdownfield-title').replaceWith(
|
||||
$('<input type="text" class="treedropdownfield-title search" data-skip-autofocus="true" />')
|
||||
var title = ss.i18n._t(
|
||||
'DropdownField.ENTERTOSEARCH',
|
||||
'Press enter to search'
|
||||
);
|
||||
|
||||
this.find('.treedropdownfield-panel').prepend(
|
||||
$('<input type="text" class="search treedropdownfield-search" data-skip-autofocus="true" placeholder="' + title + '" value="" />')
|
||||
);
|
||||
|
||||
this.setTitle(title ? title : strings.searchFieldTitle);
|
||||
},
|
||||
setTitle: function(title) {
|
||||
if(!title && title !== '') title = strings.fieldTitle;
|
||||
|
||||
this.find('.treedropdownfield-title').val(title);
|
||||
},
|
||||
getTitle: function() {
|
||||
return this.find('.treedropdownfield-title').val();
|
||||
},
|
||||
resetTitle: function() {
|
||||
this.setTitle(decodeURIComponent(this.data('title')));
|
||||
},
|
||||
search: function(str, callback) {
|
||||
this.openPanel();
|
||||
@ -306,19 +296,10 @@
|
||||
cancelSearch: function() {
|
||||
this.closePanel();
|
||||
this.loadTree();
|
||||
this.resetTitle();
|
||||
}
|
||||
});
|
||||
|
||||
$('.TreeDropdownField.searchable input.search').entwine({
|
||||
onfocusin: function(e) {
|
||||
var field = this.getField();
|
||||
field.setTitle('');
|
||||
},
|
||||
onfocusout: function(e) {
|
||||
var field = this.getField();
|
||||
field.resetTitle();
|
||||
},
|
||||
onkeydown: function(e) {
|
||||
var field = this.getField();
|
||||
if(e.keyCode == 13) {
|
||||
|
22
lang/cs.yml
22
lang/cs.yml
@ -13,12 +13,13 @@ cs:
|
||||
SIZE: 'Velikost'
|
||||
TITLE: Titulek
|
||||
TYPE: 'Typ'
|
||||
URL: URL
|
||||
AssetUploadField:
|
||||
ChooseFiles: 'Vyberte soubory'
|
||||
DRAGFILESHERE: 'Táhni soubory sem'
|
||||
DROPAREA: 'Oblast upustění'
|
||||
EDITALL: 'Editovat vše'
|
||||
EDITANDORGANIZE: 'Editovat & organizovat'
|
||||
EDITANDORGANIZE: 'Editovat a organizovat'
|
||||
EDITINFO: 'Editovat soubory'
|
||||
FILES: Soubory
|
||||
FROMCOMPUTER: 'Vyberte soubory z vašeho počítače'
|
||||
@ -73,6 +74,7 @@ cs:
|
||||
ChangePasswordEmail_ss:
|
||||
CHANGEPASSWORDTEXT1: 'Vaše heslo bylo změněno pro'
|
||||
CHANGEPASSWORDTEXT2: 'Nyní můžete použít následující přihlašovací údaje pro přihlášení:'
|
||||
EMAIL: E-mail
|
||||
HELLO: Dobrý den
|
||||
PASSWORD: Heslo
|
||||
ComplexTableField:
|
||||
@ -304,6 +306,7 @@ cs:
|
||||
LINKOPENNEWWIN: 'Otevřít odkaz v novém okně?'
|
||||
LINKTO: 'Odkázat na'
|
||||
PAGE: Stránku
|
||||
URL: URL
|
||||
URLNOTANOEMBEDRESOURCE: 'URL ''{url}'' nemůže být vloženo do zdroje médií.'
|
||||
UpdateMEDIA: 'Aktualizovat média'
|
||||
BUTTONADDURL: 'Přidat url'
|
||||
@ -342,6 +345,7 @@ cs:
|
||||
IP: 'IP adresy'
|
||||
PLURALNAME: 'Pokusy přihlášení'
|
||||
SINGULARNAME: 'Pokus přihlášení'
|
||||
Status: Stav
|
||||
Member:
|
||||
ADDGROUP: 'Přidat skupinu'
|
||||
BUTTONCHANGEPASSWORD: 'Změnit heslo'
|
||||
@ -354,9 +358,11 @@ cs:
|
||||
DATEFORMAT: 'Formát datumu'
|
||||
DefaultAdminFirstname: 'Implicitní Admin'
|
||||
DefaultDateTime: výchozí
|
||||
EMAIL: E-mail
|
||||
EMPTYNEWPASSWORD: 'Nové heslo nesmí být prázdné, zkuste to znovu'
|
||||
ENTEREMAIL: 'Zadejte e-mailovou adresu pro získání odkazu na restart hesla.'
|
||||
ERRORLOCKEDOUT: 'Váš účet byl dočasně zablokován, kvůli příliš velkému množství nezdařených pokusů o přihlášení. Zkuste se prosím přihlásit za 20 minut.'
|
||||
ERRORLOCKEDOUT2: 'Váš účet byl dočasně zablokován, kvůli příliš velkému množství nezdařených pokusů o přihlášení. Zkuste se prosím přihlásit za {count} minut.'
|
||||
ERRORNEWPASSWORD: 'Zadali jste nové heslo rozdílně, zkuste to znovu'
|
||||
ERRORPASSWORDNOTMATCH: 'Váše současné heslo není správně, prosím zkuste to znovu'
|
||||
ERRORWRONGCRED: 'Toto nevypadá jako správná emailová adresa nebo heslo. Prosím zkuste to znovu.'
|
||||
@ -384,6 +390,7 @@ cs:
|
||||
db_NumVisit: 'Počet návštěvníků'
|
||||
db_Password: Heslo
|
||||
db_PasswordExpiry: 'Datum vypršení hesla'
|
||||
NoPassword: 'Neni zde heslo pro tohoto člena'
|
||||
MemberAuthenticator:
|
||||
TITLE: 'E-mail a Heslo'
|
||||
MemberDatetimeOptionsetField:
|
||||
@ -466,6 +473,7 @@ cs:
|
||||
PermissionRole:
|
||||
OnlyAdminCanApply: 'Pouze administrátor může použít'
|
||||
PLURALNAME: Role
|
||||
SINGULARNAME: Role
|
||||
Title: Název
|
||||
PermissionRoleCode:
|
||||
PLURALNAME: 'Kódy role oprávnění'
|
||||
@ -481,6 +489,7 @@ cs:
|
||||
NOTFOUND: 'Žádné položky'
|
||||
Security:
|
||||
ALREADYLOGGEDIN: 'K této stránce nemáte přístup. Pokud máte jiný účet, který k ní může přistupovat, můžete se přihlásit níže'
|
||||
LOSTPASSWORDHEADER: 'Ztracené heslo'
|
||||
BUTTONSEND: 'Pošlete mi nulovací odkaz pro heslo'
|
||||
CHANGEPASSWORDBELOW: 'Svoje heslo si můžete změnit níže.'
|
||||
CHANGEPASSWORDHEADER: 'Změnit heslo'
|
||||
@ -516,7 +525,9 @@ cs:
|
||||
FileFieldLabel: 'Soubor CSV <small>(Povoleny přípony: *.csv)</small>'
|
||||
SilverStripeNavigator:
|
||||
Edit: Editovat
|
||||
Auto: Auto
|
||||
ChangeViewMode: 'Změnit mód zobrazení'
|
||||
Desktop: Desktop
|
||||
DualWindowView: 'Dualní okno'
|
||||
EditView: 'Mód editace'
|
||||
Mobile: Mobilní telefon
|
||||
@ -524,6 +535,7 @@ cs:
|
||||
PreviewView: 'Mód náhledu'
|
||||
Responsive: Responzivní
|
||||
SplitView: 'Mód rozdělení'
|
||||
Tablet: Tablet
|
||||
ViewDeviceWidth: 'Vyberte šířku náhledu'
|
||||
Width: šířka
|
||||
SimpleImageField:
|
||||
@ -593,3 +605,11 @@ cs:
|
||||
PREVIEW: 'Náhled webu'
|
||||
GridFieldEditButton_ss:
|
||||
EDIT: Editovat
|
||||
ContentController:
|
||||
NOTLOGGEDIN: 'Nepřihlášen'
|
||||
GridFieldItemEditView:
|
||||
Go_back: 'Jdi zpět'
|
||||
PasswordValidator:
|
||||
LOWCHARSTRENGTH: 'Prosím, posilněte heslo přidáním některých z následujících znaků: %s'
|
||||
PREVPASSWORD: 'Již jste použil toto heslo v minulosti, vyberte nové heslo, prosím'
|
||||
TOOSHORT: 'Heslo je příliš krátké, musí být %s nebo více znaků dlouhé'
|
||||
|
17
lang/de.yml
17
lang/de.yml
@ -392,6 +392,7 @@ de:
|
||||
db_NumVisit: 'Anzahl der Besuche'
|
||||
db_Password: Passwort
|
||||
db_PasswordExpiry: 'Ablaufdatum des Passworts'
|
||||
NoPassword: 'Dieser Benutzer hat kein Passwort.'
|
||||
MemberAuthenticator:
|
||||
TITLE: 'E-Mail & Passwort'
|
||||
MemberDatetimeOptionsetField:
|
||||
@ -490,6 +491,7 @@ de:
|
||||
NOTFOUND: 'Keine Elemente gefunden'
|
||||
Security:
|
||||
ALREADYLOGGEDIN: 'Sie haben keinen Zugriff auf diese Seite. Wenn Sie ein anderes Konto besitzen, mit dem Sie auf diese Seite zugreifen können, melden Sie sich bitte unten an.'
|
||||
LOSTPASSWORDHEADER: 'Passwort vergessen'
|
||||
BUTTONSEND: 'Senden Sie mir den Link zur Passwortrücksetzung'
|
||||
CHANGEPASSWORDBELOW: 'Sie können Ihr Passwort unten ändern.'
|
||||
CHANGEPASSWORDHEADER: 'Passwort ändern'
|
||||
@ -531,6 +533,7 @@ de:
|
||||
DualWindowView: 'Zwei Fenster'
|
||||
EditView: 'Bearbeitungsmodus'
|
||||
Mobile: Mobil
|
||||
PreviewState: 'Vorschau Status'
|
||||
PreviewView: 'Vorschaumodus'
|
||||
Responsive: Responsive
|
||||
SplitView: 'Geteilter Modus'
|
||||
@ -573,23 +576,29 @@ de:
|
||||
ATTACHFILES: 'Dateien anhängen'
|
||||
AttachFile: 'Datei(en) anhängen'
|
||||
DELETE: 'Aus Dateien löschen'
|
||||
DELETEINFO: 'Diese Datei am Server löschen'
|
||||
DOEDIT: Speichern
|
||||
DROPFILE: 'Datei hier ablegen'
|
||||
DROPFILES: 'Dateien hier ablegen'
|
||||
Dimensions: Dimensionen
|
||||
EDIT: Bearbeiten
|
||||
EDITINFO: 'Diese Datei bearbeiten'
|
||||
FIELDNOTSET: 'Dateiinformationen nicht gefunden'
|
||||
FROMCOMPUTER: 'Von Ihrem Computer'
|
||||
FROMCOMPUTERINFO: 'Aus Dateien auswählen'
|
||||
FROMFILES: 'Aus Dateien'
|
||||
HOTLINKINFO: 'Info: Dieses Bild wird verknüpft. Bitte vergewissere dich die Erlaubnis des Inhabers der Ursprungsseite zu haben.'
|
||||
MAXNUMBEROFFILES: 'Maximale Anzahl an {count} Datei(en) überschritten'
|
||||
MAXNUMBEROFFILESSHORT: 'SIe können maximal {count} Datei(en) hochladen'
|
||||
MAXNUMBEROFFILESONE: 'SIe können maximal eine Datei hochladen'
|
||||
REMOVE: Entfernen
|
||||
REMOVEERROR: 'Fehler beim Entfernen der Datei'
|
||||
REMOVEINFO: 'Diese Datei entfernen aber nicht am Server löschen'
|
||||
STARTALL: 'Alle starten'
|
||||
STARTALLINFO: 'Alle Uploads starten'
|
||||
Saved: Gespeichert
|
||||
CHOOSEANOTHERFILE: 'Andere Datei auswählen'
|
||||
CHOOSEANOTHERINFO: 'Diese Datei mit einer Datei vom Server ersetzen'
|
||||
OVERWRITEWARNING: 'Eine Datei mit dem selben Namen existiert bereits'
|
||||
UPLOADSINTO: 'speichert nach /{path}'
|
||||
Versioned:
|
||||
@ -598,3 +607,11 @@ de:
|
||||
PREVIEW: 'Vorschau der Webseite'
|
||||
GridFieldEditButton_ss:
|
||||
EDIT: Bearbeiten
|
||||
ContentController:
|
||||
NOTLOGGEDIN: 'Nicht eingeloggt'
|
||||
GridFieldItemEditView:
|
||||
Go_back: 'Zurück'
|
||||
PasswordValidator:
|
||||
LOWCHARSTRENGTH: 'Bitte erhöhen Sie die Sicherheit des Passworts, indem Sie auch einige der folgenden Zeichen verwenden: %s'
|
||||
PREVPASSWORD: 'Sie haben dieses Passwort schon einmal verwendet. Bitte wählen Sie ein neues Passwort'
|
||||
TOOSHORT: 'Das Passwort ist zu kurz, es muss mindestens %s Zeichen lang sein'
|
||||
|
95
lang/fr.yml
95
lang/fr.yml
@ -1,8 +1,11 @@
|
||||
fr:
|
||||
AssetAdmin:
|
||||
ALLOWEDEXTS: 'Extensions autorisées'
|
||||
NEWFOLDER: Nouveau dossier
|
||||
SHOWALLOWEDEXTS: 'Montrer les extensions autorisées'
|
||||
AssetTableField:
|
||||
CREATED: 'Premier chargement'
|
||||
DIM: Dimensions
|
||||
FILENAME: Nom du fichier
|
||||
FOLDER: Dossier
|
||||
LASTEDIT: 'Dernière modification'
|
||||
@ -10,6 +13,7 @@ fr:
|
||||
SIZE: 'Taille'
|
||||
TITLE: Titre
|
||||
TYPE: 'Type'
|
||||
URL: URL
|
||||
AssetUploadField:
|
||||
ChooseFiles: 'Choisissez les fichiers'
|
||||
DRAGFILESHERE: 'Glissez des fichiers ici'
|
||||
@ -20,6 +24,7 @@ fr:
|
||||
FILES: Fichiers
|
||||
FROMCOMPUTER: 'Choisissez des fichiers de votre ordinateur'
|
||||
FROMCOMPUTERINFO: 'Télécharger depuis votre ordinateur'
|
||||
TOTAL: Total
|
||||
TOUPLOAD: 'Choisissez les fichiers à télécharger…'
|
||||
UPLOADINPROGRESS: 'Patientez s''il vous plaît… téléchargement en cours '
|
||||
UPLOADOR: OU
|
||||
@ -35,6 +40,7 @@ fr:
|
||||
COLOREDEXAMPLE: 'texte bleu'
|
||||
EMAILLINK: 'Lien email'
|
||||
EMAILLINKDESCRIPTION: 'Créer un lien vers une adresse email'
|
||||
IMAGE: Image
|
||||
IMAGEDESCRIPTION: 'Afficher une image dans votre message'
|
||||
ITALIC: 'Texte en italique'
|
||||
ITALICEXAMPLE: Italique
|
||||
@ -68,6 +74,7 @@ fr:
|
||||
ChangePasswordEmail_ss:
|
||||
CHANGEPASSWORDTEXT1: 'Vous avez modifié votre mot de passe pour'
|
||||
CHANGEPASSWORDTEXT2: 'Vous pouvez maintenant utiliser les identifiants suivants pour vous connecter :'
|
||||
EMAIL: Email
|
||||
HELLO: Salut
|
||||
PASSWORD: Mot de passe
|
||||
ComplexTableField:
|
||||
@ -90,12 +97,31 @@ fr:
|
||||
CreditCardField:
|
||||
FIRST: premier
|
||||
FOURTH: quatrième
|
||||
SECOND: second
|
||||
THIRD: troisième
|
||||
CurrencyField:
|
||||
CURRENCYSYMBOL: $
|
||||
DataObject:
|
||||
PLURALNAME: 'Modèles de données'
|
||||
SINGULARNAME: 'Modèle de donées'
|
||||
Date:
|
||||
DAY: jour
|
||||
DAYS: jours
|
||||
HOUR: heure
|
||||
HOURS: heures
|
||||
MIN: min
|
||||
MINS: mins
|
||||
MONTH: mois
|
||||
MONTHS: mois
|
||||
SEC: sec
|
||||
SECS: secs
|
||||
TIMEDIFFAGO: 'Il y a {difference}'
|
||||
TIMEDIFFIN: 'Dans {difference}'
|
||||
YEAR: année
|
||||
YEARS: années
|
||||
LessThanMinuteAgo: 'moins d''une minute'
|
||||
DateField:
|
||||
NOTSET: 'pas d''ensemble'
|
||||
NOTSET: 'non renseigné'
|
||||
TODAY: aujourd'hui
|
||||
VALIDDATEFORMAT2: 'Saisissez la date au format valide ({format})'
|
||||
VALIDDATEMAXDATE: 'La date doit être antérieure ou égale à celle qui a été autorisée ({date})'
|
||||
@ -169,6 +195,7 @@ fr:
|
||||
TEXT2: 'lien de réinitialisation de mot de passe'
|
||||
TEXT3: pour
|
||||
Form:
|
||||
FIELDISREQUIRED: '{name} requis'
|
||||
SubmitBtnLabel: Envoyer
|
||||
VALIDATIONCREDITNUMBER: 'Vérifiez que vous avez bien saisi votre numéro de carte bleue {number}.'
|
||||
VALIDATIONNOTUNIQUE: 'La valeur entrée n''est pas unique'
|
||||
@ -177,8 +204,10 @@ fr:
|
||||
VALIDATIONSTRONGPASSWORD: 'Le mot de passe doit comporter au moins un chiffre et un caractère alphanumérique'
|
||||
VALIDATOR: Validateur
|
||||
VALIDCURRENCY: 'Saisissez une monnaie valide'
|
||||
CSRF_FAILED_MESSAGE: 'Il semble qu''il y ait eu un problème technique. Veuillez cliquez sur le bouton Retour, raffraîchir votre navigateur, et essayer à nouveau'
|
||||
FormField:
|
||||
NONE: aucun
|
||||
Example: 'par exemple %s'
|
||||
GridAction:
|
||||
DELETE_DESCRIPTION: Supprime
|
||||
Delete: Supprimer
|
||||
@ -200,6 +229,7 @@ fr:
|
||||
ResetFilter: Réinitialiser
|
||||
GridFieldAction_Delete:
|
||||
DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer'
|
||||
EditPermissionsFailure: 'Pas de permissions pour délier l''enregistrement'
|
||||
GridFieldDetailForm:
|
||||
CancelBtn: Annuler
|
||||
Create: Créer
|
||||
@ -207,12 +237,16 @@ fr:
|
||||
DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer'
|
||||
Deleted: '%s %s supprimés'
|
||||
Save: Enregistrer
|
||||
Saved: '{name} {link} sauvegardé'
|
||||
GridFieldItemEditView_ss:
|
||||
Go_back: 'Revenir en arrière'
|
||||
Group:
|
||||
AddRole: 'Ajouter un rôle pour ce groupe'
|
||||
Code: 'Code de groupe'
|
||||
DefaultGroupTitleAdministrators: Administrateur
|
||||
DefaultGroupTitleContentAuthors: 'Auteurs du contenu'
|
||||
Description: Description
|
||||
GroupReminder: 'Si vous choisissez un groupe parent, ce groupe prendra tous ses rôles'
|
||||
Locked: 'Verrouillé?'
|
||||
NoRoles: 'Vous n’avez pas la permission pour faire ça'
|
||||
PLURALNAME: Groupes
|
||||
@ -255,6 +289,7 @@ fr:
|
||||
IMAGEALT: 'Texte alternatif (alt)'
|
||||
IMAGEALTTEXT: 'Texte alternatif (alt) - s''affiche si l''image ne peut être affichée.'
|
||||
IMAGEALTTEXTDESC: 'Proposé aux lecteurs d’écran ou si l’image ne peut pas être affichée'
|
||||
IMAGEDIMENSIONS: Dimensions
|
||||
IMAGEHEIGHTPX: Hauteur
|
||||
IMAGETITLE: 'Texte du titre (tooltip) - informations à propos de l''image'
|
||||
IMAGETITLETEXT: 'Texte du titre (info-bulle)'
|
||||
@ -270,11 +305,16 @@ fr:
|
||||
LINKINTERNAL: 'Une page du site'
|
||||
LINKOPENNEWWIN: 'Ouvrir le lien dans une nouvelle fenêtre ?'
|
||||
LINKTO: 'Lier à'
|
||||
PAGE: Page
|
||||
URL: URL
|
||||
URLNOTANOEMBEDRESOURCE: 'L’URL {url} n’a pas pu être utilisée comme ressource média.'
|
||||
UpdateMEDIA: 'Mettre à jour le support audiovisuel'
|
||||
BUTTONADDURL: 'Ajouter une URL'
|
||||
Image:
|
||||
PLURALNAME: Fichiers
|
||||
SINGULARNAME: Fichier
|
||||
ImageField:
|
||||
IMAGE: Image
|
||||
Image_Cached:
|
||||
PLURALNAME: Fichiers
|
||||
SINGULARNAME: Fichier
|
||||
@ -283,6 +323,7 @@ fr:
|
||||
LeftAndMain:
|
||||
CANT_REORGANISE: 'Vous n’avez pas l’autorisation de modifier les pages de premier niveau. Vos modifications n’ont pas été enregistrées.'
|
||||
DELETED: Supprimé.
|
||||
DropdownBatchActionsDefault: Actions
|
||||
HELP: Aide
|
||||
PAGETYPE: 'Type de page :'
|
||||
PERMAGAIN: 'Vous avez été déconnecté du CMS. Si vous voulez vous reconnecter, entrez un nom d''utilisateur et un mot de passe ci-dessous.'
|
||||
@ -293,6 +334,9 @@ fr:
|
||||
REORGANISATIONSUCCESSFUL: 'L’arbre du site a été bien réorganisé.'
|
||||
SAVEDUP: Enregistré.
|
||||
VersionUnknown: inconnu
|
||||
ShowAsList: 'lister'
|
||||
TooManyPages: 'Trop de pages'
|
||||
ValidationError: 'Erreur de validation'
|
||||
LeftAndMain_Menu_ss:
|
||||
Hello: Bonjour
|
||||
LOGOUT: 'Déconnexion'
|
||||
@ -314,10 +358,12 @@ fr:
|
||||
DATEFORMAT: 'Format de la date'
|
||||
DefaultAdminFirstname: 'Administrateur par défaut'
|
||||
DefaultDateTime: par défaut
|
||||
EMAIL: Email
|
||||
EMPTYNEWPASSWORD: 'Le champs nouveau mot de passe ne peut être vide, essayez de nouveau'
|
||||
ENTEREMAIL: 'Veuillez écrire une adresse email pour obtenir le lien de réinitialisation du mot de passe.'
|
||||
ERRORLOCKEDOUT: 'Votre compte a été désactivé temporairement à cause de multiples tentatives de connexions. Veuillez réessayer dans 20 minutes.'
|
||||
ERRORNEWPASSWORD: 'Vous avez entré votre nouveau mot de passe différemment, essayez encore'
|
||||
ERRORLOCKEDOUT2: 'Votre compte a été temporairement désactivé à cause de trop nombreux échecs d''identification. Veuillez réessayer dans {count} minutes.'
|
||||
ERRORNEWPASSWORD: 'Vous avez entré votre nouveau mot de passe différemment, réessayez'
|
||||
ERRORPASSWORDNOTMATCH: 'Votre actuel mot de passe ne correspond pas, essayez encore s''il vous plaît'
|
||||
ERRORWRONGCRED: 'Il semble que ce ne soit pas le bon email ou mot de passe. Essayez encore s''il vous plaît.'
|
||||
FIRSTNAME: 'Prénom'
|
||||
@ -344,10 +390,11 @@ fr:
|
||||
db_NumVisit: 'Nombre de visite'
|
||||
db_Password: Mot de passe
|
||||
db_PasswordExpiry: 'Date d''expiration du mot de passe'
|
||||
NoPassword: 'Ce membre n''a pas de mot de passe'
|
||||
MemberAuthenticator:
|
||||
TITLE: 'Email & Mot de passe'
|
||||
MemberDatetimeOptionsetField:
|
||||
AMORPM: 'AM (Ante meridiem) ou PM (Post meridiem)'
|
||||
AMORPM: 'AM (matin) ou PM (après-midi)'
|
||||
Custom: Personnalisé
|
||||
DATEFORMATBAD: 'Format de date incorrect'
|
||||
DAYNOLEADING: 'Le jour du mois sans zéro initial'
|
||||
@ -366,6 +413,7 @@ fr:
|
||||
TWODIGITMONTH: 'Le mois sur deux chiffres (p. ex. 01=janvier)'
|
||||
TWODIGITSECOND: 'La seconde sur deux chiffres (de 00 à 59)'
|
||||
TWODIGITYEAR: 'L’année sur deux chiffres'
|
||||
Toggle: 'Afficher l’aide de mise en forme'
|
||||
MemberImportForm:
|
||||
Help1: '<p>Importer les membres au format<em>CSV format</em> (comma-separated values). <small><a href="#" class="toggle-advanced">Afficher l''usage avancé.</a></small></p>'
|
||||
Help2: "<div class=\"advanced\">\n<h4>Utilisation avancée</h4>\n<ul>\n<li>Colonnes autorisées : <em>%s</em></li>\n<li>Les utilisateurs existants sont retrouvés avec leur <em>Code</em> unique et les registres sont mis à jour avec les nouvelles valeurs du fichier importé.</li>\n<li>Des groupes peuvent être assignés à l’aide de la colonne <em>Groups</em>. Les groupes sont identifiés par leur <em>Code</em>, plusieurs groupes peuvent être indiqués en les séparant par des virgules. L’appartenance actuelle aux groupes n’est pas modifiée.</li>\n</ul>\n</div>"
|
||||
@ -381,6 +429,7 @@ fr:
|
||||
ModelAdmin:
|
||||
DELETE: Supprime
|
||||
DELETEDRECORDS: '{count} enregistrements supprimés.'
|
||||
EMPTYBEFOREIMPORT: 'Remplacer les données'
|
||||
IMPORT: 'Importer de CSV'
|
||||
IMPORTEDRECORDS: '{count} enregistrements importés.'
|
||||
NOCSVFILE: 'Veuillez choisir un fichier CSV à importer'
|
||||
@ -391,6 +440,7 @@ fr:
|
||||
ModelAdmin_ImportSpec_ss:
|
||||
IMPORTSPECFIELDS: 'Colonnes de la base de données'
|
||||
IMPORTSPECLINK: 'Afficher la spécification de %s'
|
||||
IMPORTSPECRELATIONS: Relations
|
||||
IMPORTSPECTITLE: 'Spécification de %s'
|
||||
ModelAdmin_Tools_ss:
|
||||
FILTER: Filtrer
|
||||
@ -406,6 +456,7 @@ fr:
|
||||
NumericField:
|
||||
VALIDATION: '« {value} » n’est pas un chiffre, seule donnée acceptée dans ce champ '
|
||||
Pagination:
|
||||
Page: Page
|
||||
View: Afficher
|
||||
Permission:
|
||||
AdminGroup: Administrateur
|
||||
@ -438,6 +489,7 @@ fr:
|
||||
NOTFOUND: 'Aucun élément n’a été trouvé'
|
||||
Security:
|
||||
ALREADYLOGGEDIN: 'Vous n''avez pas accès à cette page. Si vous avez un autre identifiant pouvant accéder à cette page, vous pouvez l''utiliser ci-dessous.'
|
||||
LOSTPASSWORDHEADER: 'Mot de passe oublié'
|
||||
BUTTONSEND: 'Envoyer moi le lien pour modifier le mot de passe'
|
||||
CHANGEPASSWORDBELOW: 'Vous pouvez modifier votre mot de passe ci-dessous.'
|
||||
CHANGEPASSWORDHEADER: 'Modifier votre mot de passe'
|
||||
@ -457,6 +509,8 @@ fr:
|
||||
EDITPERMISSIONS: 'Gérer les autorisations des groupes'
|
||||
EDITPERMISSIONS_HELP: 'Possibilité d''éditer les autorisations et les adresses IP pour un groupe. Nécessite l’autorisation « Accès à la section “Securité” ».'
|
||||
GROUPNAME: 'Nom du group'
|
||||
IMPORTGROUPS: 'Importer des groupes'
|
||||
IMPORTUSERS: 'Importer des utilisateurs'
|
||||
MEMBERS: Membres
|
||||
MENUTITLE: Sécurité
|
||||
MemberListCaution: 'Attention : en supprimant des membres de cette liste vous les enlèverez de tous les groupes ainsi que de la base de données'
|
||||
@ -471,6 +525,19 @@ fr:
|
||||
FileFieldLabel: 'Fichier CSV <small>(extension autorisée : *.csv)</small>'
|
||||
SilverStripeNavigator:
|
||||
Edit: Tout modifier
|
||||
Auto: Auto
|
||||
ChangeViewMode: 'Changer de mode de vue'
|
||||
Desktop: Bureau
|
||||
DualWindowView: 'Double fenêtre'
|
||||
EditView: 'Editer le mode'
|
||||
Mobile: Mobile
|
||||
PreviewState: 'Aperçu'
|
||||
PreviewView: 'Aperçu'
|
||||
Responsive: Responsive
|
||||
SplitView: 'Mode partagé'
|
||||
Tablet: Tablette
|
||||
ViewDeviceWidth: 'Sélectionnez une largeur d''aperçu'
|
||||
Width: largeur
|
||||
SimpleImageField:
|
||||
NOUPLOAD: 'Aucune image chargée'
|
||||
SiteTree:
|
||||
@ -510,17 +577,39 @@ fr:
|
||||
DELETEINFO: 'Effacer définitivement ce fichier des archives'
|
||||
DOEDIT: Enregistrer
|
||||
DROPFILE: 'glissez-déposez un fichier'
|
||||
DROPFILES: 'glissez-déposez des fichiers'
|
||||
Dimensions: Dimensions
|
||||
EDIT: Modifier
|
||||
EDITINFO: 'Modifier ce fichier'
|
||||
FIELDNOTSET: 'Informations concernant le fichiers non-trouvées'
|
||||
FROMCOMPUTER: 'Depuis votre ordinateur'
|
||||
FROMCOMPUTERINFO: 'Choisir parmi les fichiers'
|
||||
FROMFILES: 'Depuis les fichiers'
|
||||
HOTLINKINFO: 'Note : Cette image sera liée par un « hotlink », assurez-vous d’avoir l’autorisation des ayant-droits du site web d’origine.'
|
||||
MAXNUMBEROFFILES: 'Le nombre maximal de {count} fichiers a été dépassé.'
|
||||
MAXNUMBEROFFILESSHORT: 'On ne peut pas télécharger plus de {count} fichiers'
|
||||
MAXNUMBEROFFILESONE: 'Vous ne pouvez pas uploader plus d''un fichier'
|
||||
REMOVE: Supprimer
|
||||
REMOVEERROR: 'Le fichier n’a pas pu être supprimé'
|
||||
REMOVEINFO: 'Supprimer ce fichier ici sans l’effacer des archives'
|
||||
STARTALL: 'Démarrer tout'
|
||||
STARTALLINFO: 'Démarrer tous les téléchargements'
|
||||
Saved: Enregistré
|
||||
CHOOSEANOTHERFILE: 'Choisissez un autre fichier'
|
||||
CHOOSEANOTHERINFO: 'Remplacer ce fichier par un autre depuis les archives'
|
||||
OVERWRITEWARNING: 'Un fichier avec le même nom existe déjà'
|
||||
UPLOADSINTO: 'sauvegarder dans /{path}'
|
||||
Versioned:
|
||||
has_many_Versions: Versions
|
||||
CMSPageHistoryController_versions_ss:
|
||||
PREVIEW: 'Aperçu du site'
|
||||
GridFieldEditButton_ss:
|
||||
EDIT: Editer
|
||||
ContentController:
|
||||
NOTLOGGEDIN: 'Hors-ligne'
|
||||
GridFieldItemEditView:
|
||||
Go_back: 'Retour'
|
||||
PasswordValidator:
|
||||
LOWCHARSTRENGTH: 'Veuillez augmenter la force de votre mot de passe en ajoutant certains caractères suivants : %s'
|
||||
PREVPASSWORD: 'Vous avez déjà utilisé ce mot de passe par le passé, veuillez en choisir un autre'
|
||||
TOOSHORT: 'Le mot de passe est trop court, il doit contenir au moins %s caractères'
|
||||
|
25
lang/hi.yml
25
lang/hi.yml
@ -1,6 +1,8 @@
|
||||
hi:
|
||||
AssetUploadField:
|
||||
ChooseFiles: 'फाइलें चुनें'
|
||||
ChooseFiles: 'अपलोड करने के लिए फ़ाइलों को चुनें'
|
||||
DRAGFILESHERE: 'अपलोड करने के लिए फाइलें यहाँ लाये '
|
||||
DROPAREA: 'अपलोड छेत्र '
|
||||
EDITALL: 'सभी संपादित करें'
|
||||
EDITANDORGANIZE: 'संपादित और व्यवस्थित करें '
|
||||
EDITINFO: 'फ़ाइलों को संपादित करें'
|
||||
@ -28,6 +30,27 @@ hi:
|
||||
ITALIC: 'तिरछे अक्षर'
|
||||
ITALICEXAMPLE: तिरछे अक्षर
|
||||
LINK: 'वेबसाइट लिंक'
|
||||
LINKDESCRIPTION: ' किसी दुसरे वेबसाइट या यूआरएल से जोड़े '
|
||||
BackLink_Button_ss:
|
||||
Back: वापस
|
||||
BasicAuth:
|
||||
ENTERINFO: 'कृपया उपयोगकर्ता का नाम और पासवर्ड दर्ज करें'
|
||||
ERRORNOTADMIN: 'वह उपयोगकर्ता एक व्यवस्थापक नहीं है.'
|
||||
CMSProfileController:
|
||||
MENUTITLE: 'मेरी प्रोफाइल '
|
||||
ComplexTableField_popup_ss:
|
||||
NEXT: अगला
|
||||
PREVIOUS: पिछला
|
||||
ConfirmedPasswordField:
|
||||
ATLEAST: 'पासवर्डों को कम से कम {min} अक्षर लंबा होना चाहिए.'
|
||||
SHOWONCLICKTITLE: 'पासवर्ड बदलें'
|
||||
DataObject:
|
||||
PLURALNAME: 'डेटा ऑब्जेक्ट्स'
|
||||
SINGULARNAME: 'डेटा ऑब्जेक्ट'
|
||||
DateField:
|
||||
NOTSET: 'सेट नहीं'
|
||||
TODAY: आज
|
||||
VALIDDATEFORMAT2: 'कृपया एक मान्य दिनांक स्वरूप दर्ज करें ({format})'
|
||||
Group:
|
||||
RolesAddEditLink: 'भूमिकाओं का प्रबंधन करे '
|
||||
has_many_Permissions: अनुमतियाँ
|
||||
|
38
lang/nb.yml
38
lang/nb.yml
@ -24,6 +24,7 @@ nb:
|
||||
FILES: Filer
|
||||
FROMCOMPUTER: 'Velg filer fra din pc'
|
||||
FROMCOMPUTERINFO: 'Last opp fra din pc'
|
||||
TOTAL: Total
|
||||
TOUPLOAD: 'Velg filer til opplasting ...'
|
||||
UPLOADINPROGRESS: 'Vennligst vent... opplasting pågår'
|
||||
UPLOADOR: ELLER
|
||||
@ -98,6 +99,8 @@ nb:
|
||||
FOURTH: fjerde
|
||||
SECOND: andre
|
||||
THIRD: tredje
|
||||
CurrencyField:
|
||||
CURRENCYSYMBOL: $
|
||||
DataObject:
|
||||
PLURALNAME: 'Dataobjekter'
|
||||
SINGULARNAME: 'Dataobjekt'
|
||||
@ -140,6 +143,7 @@ nb:
|
||||
AviType: 'AVI videofil'
|
||||
Content: Innhold
|
||||
CssType: 'CSS-fil'
|
||||
DmgType: 'Apple disk image'
|
||||
DocType: 'Word-dokument'
|
||||
Filename: Filnavn
|
||||
GifType: 'GIF bilde - bra til diagrammer'
|
||||
@ -162,6 +166,7 @@ nb:
|
||||
SINGULARNAME: Fil
|
||||
TOOLARGE: 'Filstørrelse for stor, maksimum {size} tillatt '
|
||||
TOOLARGESHORT: 'Filstørrelse overstiger {size}'
|
||||
TiffType: 'Tagged image format'
|
||||
Title: Tittel
|
||||
WavType: 'WAV lydfil'
|
||||
XlsType: 'Excel regneark'
|
||||
@ -188,6 +193,7 @@ nb:
|
||||
HELLO: Hei
|
||||
TEXT1: 'Her er din'
|
||||
TEXT2: 'lenke for nullstilling av passord'
|
||||
TEXT3: for
|
||||
Form:
|
||||
FIELDISREQUIRED: '{name} er påkrevet'
|
||||
SubmitBtnLabel: Utfør
|
||||
@ -196,6 +202,7 @@ nb:
|
||||
VALIDATIONPASSWORDSDONTMATCH: 'Passordene stemmer ikke overens'
|
||||
VALIDATIONPASSWORDSNOTEMPTY: 'Passord kan ikke være tomt'
|
||||
VALIDATIONSTRONGPASSWORD: 'Passord må inneholde minst ett siffer og en bokstav'
|
||||
VALIDATOR: Validator
|
||||
VALIDCURRENCY: 'Vennligst skriv inn gyldig valuta'
|
||||
CSRF_FAILED_MESSAGE: 'Det ser ut til å ha oppstått et teknisk problem. Vennligst trykk på tilbakeknappen, oppdater nettsiden og prøv på nytt.'
|
||||
FormField:
|
||||
@ -204,16 +211,21 @@ nb:
|
||||
GridAction:
|
||||
DELETE_DESCRIPTION: Slett
|
||||
Delete: Slett
|
||||
UnlinkRelation: Koble fra
|
||||
GridField:
|
||||
Add: 'Legg til {name}'
|
||||
Filter: Filtrer
|
||||
FilterBy: 'Filtrer på '
|
||||
Find: Finn
|
||||
LEVELUP: 'Opp ett nivå'
|
||||
LinkExisting: 'Eksisterende lenke'
|
||||
NewRecord: 'Ny %s'
|
||||
NoItemsFound: 'Ingen elementer ble funnet'
|
||||
PRINTEDAT: 'Skrevet ut ved'
|
||||
PRINTEDBY: 'Skrevet ut av'
|
||||
PlaceHolder: 'Finn {type}'
|
||||
PlaceHolderWithLabels: 'Finn {type} ved {name}'
|
||||
RelationSearch: 'Relasjonssøk'
|
||||
ResetFilter: Tilbakestille
|
||||
GridFieldAction_Delete:
|
||||
DeletePermissionsFailure: 'Ikke tillatt å slette'
|
||||
@ -232,6 +244,7 @@ nb:
|
||||
AddRole: 'Legg til en rolle for denne gruppen'
|
||||
Code: 'Gruppekode'
|
||||
DefaultGroupTitleAdministrators: Administratorer
|
||||
DefaultGroupTitleContentAuthors: 'Innholdsforfattere'
|
||||
Description: Beskrivelse
|
||||
GroupReminder: 'Hvis du velger en overordnet gruppe, vil denne gruppen arve alle rollene'
|
||||
Locked: 'Låst?'
|
||||
@ -244,6 +257,7 @@ nb:
|
||||
has_many_Permissions: Tillatelser
|
||||
many_many_Members: Medlemmer
|
||||
GroupImportForm:
|
||||
Help1: '<p>Importer en eller flere grupper i <em>CSV</em>-format (kommaseparerte verdier). <small><a href="#" class="toggle-advanced">Vis avanserte alternativer</a></small></p>'
|
||||
Help2: "<div class=\"advanced\">\n<h4>Avanserte alternativer</h4>\n<ul>\n<li>Tillatte kolonner: <em>%s</em></li>\n<li>Eksisterende grupper matches mot deres <em>Code</em>-verdi og oppdateres med nye verdier fra den importerte filen.</li>\n<li>Gruppehierarkier kan bli opprettet ved å benytte en <em>ParentCode</em>-kolonne.</li>\n<li>Tillatelseskoder kan bli angitt med <em>PermissionCode</em>-kolonnen. Eksisterende tillatelselskoder blir ikke fjernet.</li>\n</ul>\n</div>"
|
||||
ResultCreated: 'Opprettet {count} grupper'
|
||||
ResultDeleted: 'Slettet %d grupper'
|
||||
@ -331,6 +345,7 @@ nb:
|
||||
IP: 'IP-adresse'
|
||||
PLURALNAME: 'Innloggingsforsøk'
|
||||
SINGULARNAME: 'Innloggingsforsøk'
|
||||
Status: Status
|
||||
Member:
|
||||
ADDGROUP: 'Legg til en gruppe'
|
||||
BUTTONCHANGEPASSWORD: 'Bytt passord'
|
||||
@ -341,10 +356,13 @@ nb:
|
||||
CONFIRMNEWPASSWORD: 'Bekreft nytt passord'
|
||||
CONFIRMPASSWORD: 'Bekreft passord'
|
||||
DATEFORMAT: 'Datoformat'
|
||||
DefaultAdminFirstname: 'Standard Admin'
|
||||
DefaultDateTime: standard
|
||||
EMAIL: Epost
|
||||
EMPTYNEWPASSWORD: 'Det nye passordet kan ikke være tomt, vennligst prøv igjen'
|
||||
ENTEREMAIL: 'Vennligst skriv en epostadresse så du kan bli tilsendt en lenke til å nullstille passord.'
|
||||
ERRORLOCKEDOUT: 'Din konto har blitt sperret på grunn av for mange forsøk på å logge inn. Vennligst prøv igjen om 20 minutter.'
|
||||
ERRORLOCKEDOUT2: 'Din konto har blitt midlertidig sperret på grunn av for mange mislykkede forsøk på å logge inn. Vennligst prøv igjen om {count} minutter.'
|
||||
ERRORNEWPASSWORD: 'Du har tastet inn nye passord forskjellig, vennligst prøv igjen.'
|
||||
ERRORPASSWORDNOTMATCH: 'Passordene stemmer ikke overens, vennligst prøv igjen.'
|
||||
ERRORWRONGCRED: 'Det ser ikke ut til å være riktig epostadresse eller passord. Vennligst prøv igjen.'
|
||||
@ -372,9 +390,12 @@ nb:
|
||||
db_NumVisit: 'Antall besøk'
|
||||
db_Password: Passord
|
||||
db_PasswordExpiry: 'Utløpsdato for passord'
|
||||
NoPassword: 'Det finnes ikke noe passord for dette medlemmet.'
|
||||
MemberAuthenticator:
|
||||
TITLE: 'E-post og passord'
|
||||
MemberDatetimeOptionsetField:
|
||||
AMORPM: 'AM (Ante meridiem) or PM (Post meridiem)'
|
||||
Custom: Egendefinert
|
||||
DATEFORMATBAD: 'Datoformat er ugyldig'
|
||||
DAYNOLEADING: 'Dag i måneden uten ledende null'
|
||||
DIGITSDECFRACTIONSECOND: 'Ett eller flere siffer som representerer en desimlbrøkdel av et sekund'
|
||||
@ -411,6 +432,7 @@ nb:
|
||||
EMPTYBEFOREIMPORT: 'Erstatt data'
|
||||
IMPORT: 'Importer fra CSV'
|
||||
IMPORTEDRECORDS: 'Importerte {count} oppføringer.'
|
||||
NOCSVFILE: 'Vennligst finn en CSV-fil å importere'
|
||||
NOIMPORT: 'Ingenting å importere'
|
||||
RESET: Tilbakestill
|
||||
Title: 'Datamodeller'
|
||||
@ -437,6 +459,7 @@ nb:
|
||||
Page: Side
|
||||
View: Viser
|
||||
Permission:
|
||||
AdminGroup: Administratorer
|
||||
CMS_ACCESS_CATEGORY: 'Tilgang til publiseringssystem'
|
||||
FULLADMINRIGHTS: 'Fulle administrative rettigheter'
|
||||
FULLADMINRIGHTS_HELP: 'Overstyrer alle andre tilordnede tillatelser.'
|
||||
@ -466,6 +489,7 @@ nb:
|
||||
NOTFOUND: 'Ingen elementer ble funnet'
|
||||
Security:
|
||||
ALREADYLOGGEDIN: 'Du har ikke adgang til denne siden. Hvis du har en annen konto som har adgang til denne siden, kan du logge inn med den under.'
|
||||
LOSTPASSWORDHEADER: 'Mistet passord'
|
||||
BUTTONSEND: 'Send meg en lenke for å nullstille passordet'
|
||||
CHANGEPASSWORDBELOW: 'Du kan bytte passord under her.'
|
||||
CHANGEPASSWORDHEADER: 'Bytt passord'
|
||||
@ -485,15 +509,20 @@ nb:
|
||||
EDITPERMISSIONS: 'Administrer tillatelser for grupper'
|
||||
EDITPERMISSIONS_HELP: 'Mulighet for å endre tillatelser og IP-adresser for en gruppe. Brukere må ha adgang til sikkerhetsseksjon.'
|
||||
GROUPNAME: 'Gruppenavn'
|
||||
IMPORTGROUPS: 'Importer grupper'
|
||||
IMPORTUSERS: 'Importer brukere'
|
||||
MEMBERS: Medlemmer
|
||||
MENUTITLE: Sikkerhet
|
||||
MemberListCaution: 'Advarsel: Hvis du fjerner medlemmer fra denne listen vil det samtidig fjerne dem fra alle grupper og databasen'
|
||||
NEWGROUP: 'Ny gruppe'
|
||||
PERMISSIONS: Tilganger
|
||||
ROLES: Roller
|
||||
ROLESDESCRIPTION: 'Roller er forhåndsdefinerte lister av tillatelser og kan angis til grupper.<br />De blir arvet fra overerdnede grupper om nødvendig.'
|
||||
TABROLES: Roller
|
||||
Users: Brukere
|
||||
SecurityAdmin_MemberImportForm:
|
||||
BtnImport: 'Importer fra CSV'
|
||||
FileFieldLabel: 'CSV-fil <small>(Tillatt filtype: *.csv)</small>'
|
||||
SilverStripeNavigator:
|
||||
Edit: Rediger
|
||||
Auto: Automatisk
|
||||
@ -541,6 +570,7 @@ nb:
|
||||
LESS: mindre
|
||||
MORE: mer
|
||||
UploadField:
|
||||
ATTACHFILE: 'Legg ved en fil'
|
||||
ATTACHFILES: 'Legg ved filer'
|
||||
AttachFile: 'Legg ved fil(er)'
|
||||
DELETE: 'Slett fra filer'
|
||||
@ -575,3 +605,11 @@ nb:
|
||||
PREVIEW: 'Forhåndsvisning'
|
||||
GridFieldEditButton_ss:
|
||||
EDIT: Rediger
|
||||
ContentController:
|
||||
NOTLOGGEDIN: 'Ikke innlogget'
|
||||
GridFieldItemEditView:
|
||||
Go_back: 'Gå tilbake'
|
||||
PasswordValidator:
|
||||
LOWCHARSTRENGTH: 'Vennligst øk passordstyrken ved å legge til noen av følgende tegn: %s'
|
||||
PREVPASSWORD: 'Du har brukt passordet tidligere, vennligst velg et nytt passord'
|
||||
TOOSHORT: 'Passordet er for kort, det må være %s eller flere tegn langt'
|
||||
|
42
lang/sk.yml
42
lang/sk.yml
@ -13,8 +13,10 @@ sk:
|
||||
SIZE: 'Veľkosť'
|
||||
TITLE: Titulok
|
||||
TYPE: 'Typ'
|
||||
URL: URL
|
||||
AssetUploadField:
|
||||
DRAGFILESHERE: 'Tiahni súbory sem'
|
||||
ChooseFiles: 'Vyberte súbory'
|
||||
DRAGFILESHERE: 'Tiahni súbory tu'
|
||||
DROPAREA: 'Oblasť upustenia'
|
||||
EDITALL: 'Editovať všetko'
|
||||
EDITANDORGANIZE: 'Editovať a organizovať'
|
||||
@ -72,6 +74,7 @@ sk:
|
||||
ChangePasswordEmail_ss:
|
||||
CHANGEPASSWORDTEXT1: 'Vaše heslo bolo zmenené pre'
|
||||
CHANGEPASSWORDTEXT2: 'Teraz môžete použiť nasledujúce prihlasovacie údaje na prihlásenie:'
|
||||
EMAIL: E-mail
|
||||
HELLO: Dobrý deň
|
||||
PASSWORD: Heslo
|
||||
ComplexTableField:
|
||||
@ -96,6 +99,8 @@ sk:
|
||||
FOURTH: štvrtý
|
||||
SECOND: druhý
|
||||
THIRD: tretí
|
||||
CurrencyField:
|
||||
CURRENCYSYMBOL: $
|
||||
DataObject:
|
||||
PLURALNAME: 'Datové objekty'
|
||||
SINGULARNAME: 'Dátový objekt'
|
||||
@ -209,6 +214,7 @@ sk:
|
||||
UnlinkRelation: Odpojiť
|
||||
GridField:
|
||||
Add: 'Pridať {name}'
|
||||
Filter: Filter
|
||||
FilterBy: 'Filtrovať podľa'
|
||||
Find: Vyhľadať
|
||||
LEVELUP: 'Úroveň vyššie'
|
||||
@ -220,6 +226,7 @@ sk:
|
||||
PlaceHolder: 'Vyhľadať {type}'
|
||||
PlaceHolderWithLabels: 'Vyhľadať {type} podľa {name}'
|
||||
RelationSearch: 'Vzťah hľadania'
|
||||
ResetFilter: Reset
|
||||
GridFieldAction_Delete:
|
||||
DeletePermissionsFailure: 'Žiadne oprávnenia zmazať'
|
||||
EditPermissionsFailure: 'Žiadne oprávnenie pre odpojenie záznamu'
|
||||
@ -261,6 +268,7 @@ sk:
|
||||
ADDURL: 'Pridať URL'
|
||||
ADJUSTDETAILSDIMENSIONS: 'Detaily & rozmery'
|
||||
ANCHORVALUE: Odkaz
|
||||
BUTTONINSERT: Vložiť
|
||||
BUTTONINSERTLINK: 'Vložiť odkaz'
|
||||
BUTTONREMOVELINK: 'Odstrániť odkaz'
|
||||
BUTTONUpdate: Aktualizovať
|
||||
@ -298,6 +306,7 @@ sk:
|
||||
LINKOPENNEWWIN: 'Otvoriť odkaz v novom okne?'
|
||||
LINKTO: 'Odkázať na'
|
||||
PAGE: Stránku
|
||||
URL: URL
|
||||
URLNOTANOEMBEDRESOURCE: 'URL ''{url}'' nemôže byť vložené do zdroja médií.'
|
||||
UpdateMEDIA: 'Aktualizovať média'
|
||||
BUTTONADDURL: 'Pridať url'
|
||||
@ -314,6 +323,7 @@ sk:
|
||||
LeftAndMain:
|
||||
CANT_REORGANISE: 'Nemáte oprávnenie meniť stránky najvyššej úrovne. Vaša zmena nebola uložená.'
|
||||
DELETED: Zmazané.
|
||||
DropdownBatchActionsDefault: Akcie
|
||||
HELP: Pomoc
|
||||
PAGETYPE: 'Typ stránky:'
|
||||
PERMAGAIN: 'Boli ste odhlásený'
|
||||
@ -323,6 +333,7 @@ sk:
|
||||
PreviewButton: Náhľad
|
||||
REORGANISATIONSUCCESSFUL: 'Strom webu bol reorganizovaný úspešne.'
|
||||
SAVEDUP: Uložené.
|
||||
VersionUnknown: Neznáme
|
||||
ShowAsList: 'ukázať ako zoznam'
|
||||
TooManyPages: 'Príliž veľa stránok'
|
||||
ValidationError: 'Chyba platnosti'
|
||||
@ -334,7 +345,9 @@ sk:
|
||||
IP: 'IP adreasa'
|
||||
PLURALNAME: 'Pokusy o prihlásenie'
|
||||
SINGULARNAME: 'Pokus o prihlásenie'
|
||||
Status: Stav
|
||||
Member:
|
||||
ADDGROUP: 'Pridať skupinu'
|
||||
BUTTONCHANGEPASSWORD: 'Zmeniť heslo'
|
||||
BUTTONLOGIN: 'Prihlásiť sa'
|
||||
BUTTONLOGINOTHER: 'Prihlásiť sa ako niekto iný'
|
||||
@ -349,6 +362,7 @@ sk:
|
||||
EMPTYNEWPASSWORD: 'Nové heslo nesmie byť prázdne, skúste to prosím znova'
|
||||
ENTEREMAIL: 'Prosím zadajte emailovú adresu pre zaslanie odkazu na resetovanie hesla.'
|
||||
ERRORLOCKEDOUT: 'Váš účet bol dočasne zablokovaný, kvôli množstvu neúspešných pokusov o prihlásenie. Prosí skúste to znova za 20 minút.'
|
||||
ERRORLOCKEDOUT2: 'Váš účet bol dočasne zablokovaný, kvôli množstvu neúspešných pokusov o prihlásenie. Prosím skúste to znova za {count} minút.'
|
||||
ERRORNEWPASSWORD: 'Zadali ste rozdielne nové heslo, skúste to znovu'
|
||||
ERRORPASSWORDNOTMATCH: 'Vaše súčasné heslo nie je správne, prosím skúste to znovu'
|
||||
ERRORWRONGCRED: 'Toto nevyzerá ako správna e-mailová adresa alebo heslo. Prosím skúste to znovu.'
|
||||
@ -367,6 +381,7 @@ sk:
|
||||
TIMEFORMAT: 'Formát času'
|
||||
VALIDATIONMEMBEREXISTS: 'Člen s takýmto e-mailom už existuje'
|
||||
ValidationIdentifierFailed: 'Nemôžte prepísať existujúceho člena #{id} s identickým identifikátorm ({name} = {value}))'
|
||||
WELCOMEBACK: 'Vitajte späť, {firstname}'
|
||||
YOUROLDPASSWORD: 'Vaše staré heslo'
|
||||
belongs_many_many_Groups: Skupiny
|
||||
db_LastVisited: 'Dátum poslednej navštevy'
|
||||
@ -375,10 +390,12 @@ sk:
|
||||
db_NumVisit: 'Počet návštev'
|
||||
db_Password: Heslo
|
||||
db_PasswordExpiry: 'Dátum expirácie hesla'
|
||||
NoPassword: 'Nie je tu heslo pre tohto člena.'
|
||||
MemberAuthenticator:
|
||||
TITLE: 'E-mail & Heslo'
|
||||
MemberDatetimeOptionsetField:
|
||||
AMORPM: 'AM (pred poludním) alebo PM (popoludní)'
|
||||
Custom: Vlastné
|
||||
DATEFORMATBAD: 'Formát dátumu je neplatný'
|
||||
DAYNOLEADING: 'Deň mesiaca bez úvodnej nuly'
|
||||
DIGITSDECFRACTIONSECOND: 'Jedna alebo viac číslic zastupujúcich desatinný zlomok sekundy'
|
||||
@ -387,6 +404,7 @@ sk:
|
||||
HOURNOLEADING: 'Hodina bez úvodnej nuly'
|
||||
MINUTENOLEADING: 'Minúta bez úvodnej nuly'
|
||||
MONTHNOLEADING: 'Číslo mesiaca bez úvodnej nuly'
|
||||
Preview: Náhľad
|
||||
SHORTMONTH: 'Krátky názov mesiaca (napr. jún)'
|
||||
TOGGLEHELP: 'Prepnúť nápovedu formátovania'
|
||||
TWODIGITDAY: 'Dvojčíslie dňa mesiaca'
|
||||
@ -416,6 +434,7 @@ sk:
|
||||
IMPORTEDRECORDS: 'Importovaných {count} záznamov.'
|
||||
NOCSVFILE: 'Prosím vyhľadajte CSV súbor pre importovanie'
|
||||
NOIMPORT: 'Nie je čo importovať'
|
||||
RESET: Reset
|
||||
Title: 'Dátové modely'
|
||||
UPDATEDRECORDS: 'Aktualizovaných {count} záznamov.'
|
||||
ModelAdmin_ImportSpec_ss:
|
||||
@ -432,6 +451,8 @@ sk:
|
||||
MoneyField:
|
||||
FIELDLABELAMOUNT: Množstvo
|
||||
FIELDLABELCURRENCY: Mena
|
||||
NullableField:
|
||||
IsNullLabel: 'Je Null'
|
||||
NumericField:
|
||||
VALIDATION: '''{value}'' nie je číslo, iba čísla môžu byť akceptované pre toto pole'
|
||||
Pagination:
|
||||
@ -468,6 +489,7 @@ sk:
|
||||
NOTFOUND: 'Žiadne položky'
|
||||
Security:
|
||||
ALREADYLOGGEDIN: 'K tejto stránke nemáte prístup. Ak máte iný účet, ktorý k nej má prístup, môžete sa <a href="%s">prihlásiť</a>.'
|
||||
LOSTPASSWORDHEADER: 'Stratené heslo'
|
||||
BUTTONSEND: 'Pošlite mi odkaz na resetovanie hesla'
|
||||
CHANGEPASSWORDBELOW: 'Svoje heslo si môžete zmeniť nižšie.'
|
||||
CHANGEPASSWORDHEADER: 'Zmeniť heslo'
|
||||
@ -487,6 +509,7 @@ sk:
|
||||
EDITPERMISSIONS: 'Spravovať právomoci pre skupiny'
|
||||
EDITPERMISSIONS_HELP: 'Schopnosť upravovať právomoci a IP adresi pre skupinu. Vyžaduje prístup do sekcie "Bezpečnosť".'
|
||||
GROUPNAME: 'Názov skupiny'
|
||||
IMPORTGROUPS: 'Importovať skupiny'
|
||||
IMPORTUSERS: 'Importovať požívateľov'
|
||||
MEMBERS: Členovia
|
||||
MENUTITLE: Bezpečnosť
|
||||
@ -502,7 +525,9 @@ sk:
|
||||
FileFieldLabel: 'CSV súbor <small>(Povoléné koncovki súborov: *.csv)</small>'
|
||||
SilverStripeNavigator:
|
||||
Edit: Editovať
|
||||
Auto: Auto
|
||||
ChangeViewMode: 'Zmeniť nód zobrazenia'
|
||||
Desktop: Desktop
|
||||
DualWindowView: 'Duálne okno'
|
||||
EditView: 'Mód editácie'
|
||||
Mobile: Mobilný telefón
|
||||
@ -510,6 +535,7 @@ sk:
|
||||
PreviewView: 'Mód náhľadu'
|
||||
Responsive: Responzívny
|
||||
SplitView: 'Mód rozdelenia'
|
||||
Tablet: Tablet
|
||||
ViewDeviceWidth: 'Vyberte šírku náhľadu'
|
||||
Width: šírka
|
||||
SimpleImageField:
|
||||
@ -544,8 +570,12 @@ sk:
|
||||
LESS: menej
|
||||
MORE: viac
|
||||
UploadField:
|
||||
ATTACHFILE: 'Pripojiť súbor'
|
||||
ATTACHFILES: 'Pripojiť súbory'
|
||||
AttachFile: 'Pripojiť súbor(y)'
|
||||
DELETE: 'Zmazať zo súborov'
|
||||
DELETEINFO: 'Trvalo zmazať tento súbor z úložiska súborov'
|
||||
DOEDIT: Uložiť
|
||||
DROPFILE: 'pusť súbor'
|
||||
DROPFILES: 'pusť súbory'
|
||||
Dimensions: Rozmery
|
||||
@ -554,6 +584,7 @@ sk:
|
||||
FIELDNOTSET: 'Žiadna informácia o súbore'
|
||||
FROMCOMPUTER: 'Z vášho počitača'
|
||||
FROMCOMPUTERINFO: 'Vyberte zo súborov'
|
||||
FROMFILES: 'Zo súborov'
|
||||
HOTLINKINFO: 'Info: Tento obrázok bude "hotlinkovaný". Uistete sa prosím, že máte oprávnenie od pôvodneho tvorcu webu, aby sa tak stalo.'
|
||||
MAXNUMBEROFFILES: 'Maximálny počet {count} súbor(ov) prekročený.'
|
||||
MAXNUMBEROFFILESSHORT: 'Môžte nahrať iba {count} súborov'
|
||||
@ -563,6 +594,7 @@ sk:
|
||||
REMOVEINFO: 'Odstrániť tento súbor odtiaľ, ale nezmazať z úložiska súborov'
|
||||
STARTALL: 'Začni všetko'
|
||||
STARTALLINFO: 'Začni všetko nahrávať'
|
||||
Saved: Uložené
|
||||
CHOOSEANOTHERFILE: 'Vyberte iný súbor'
|
||||
CHOOSEANOTHERINFO: 'Nahradiť tento súbor iným z úložiska súborov'
|
||||
OVERWRITEWARNING: 'Súbor s rovnakým názvom už existuje'
|
||||
@ -573,3 +605,11 @@ sk:
|
||||
PREVIEW: 'Náhľad webu'
|
||||
GridFieldEditButton_ss:
|
||||
EDIT: Editovať
|
||||
ContentController:
|
||||
NOTLOGGEDIN: 'Neprihlásený'
|
||||
GridFieldItemEditView:
|
||||
Go_back: 'Choď späť'
|
||||
PasswordValidator:
|
||||
LOWCHARSTRENGTH: 'Prosím posilnite heslo pridaním z týchto niektorých znakov: %s'
|
||||
PREVPASSWORD: 'Už ste použili toto heslo v minulosti, vyberte nové hoslo, prosím'
|
||||
TOOSHORT: 'Heslo je príliš krátke, musí byť %s alebo viacej znakov dlhé'
|
||||
|
@ -141,6 +141,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* @config
|
||||
* @var boolean Should dataobjects be validated before they are written?
|
||||
* Caution: Validation can contain safeguards against invalid/malicious data,
|
||||
* and check permission levels (e.g. on {@link Group}). Therefore it is recommended
|
||||
* to only disable validation for very specific use cases.
|
||||
*/
|
||||
private static $validation_enabled = true;
|
||||
|
||||
@ -187,6 +190,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Set whether DataObjects should be validated before they are written.
|
||||
*
|
||||
* Caution: Validation can contain safeguards against invalid/malicious data,
|
||||
* and check permission levels (e.g. on {@link Group}). Therefore it is recommended
|
||||
* to only disable validation for very specific use cases.
|
||||
*
|
||||
* @param $enable bool
|
||||
* @see DataObject::validate()
|
||||
* @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
|
||||
|
@ -25,7 +25,16 @@ class HasManyList extends RelationList {
|
||||
|
||||
$this->foreignKey = $foreignKey;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the field name which holds the related object ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKey() {
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
protected function foreignIDFilter($id = null) {
|
||||
if ($id === null) $id = $this->getForeignID();
|
||||
|
||||
|
@ -425,8 +425,8 @@ class Image extends File {
|
||||
$cached = new Image_Cached($cacheFile);
|
||||
// Pass through the title so the templates can use it
|
||||
$cached->Title = $this->Title;
|
||||
// Pass through the parent, to store cached images in correct folder.
|
||||
$cached->ParentID = $this->ParentID;
|
||||
$cached->Parent = $this->Parent();
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
@ -451,7 +451,7 @@ class Image extends File {
|
||||
|
||||
return $folder . "_resampled/" . $filename;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate an image on the specified format. It will save the image
|
||||
* at the location specified by cacheFilename(). The image will be generated
|
||||
@ -545,13 +545,12 @@ class Image extends File {
|
||||
public function generateCroppedImage(Image_Backend $backend, $width, $height) {
|
||||
return $backend->croppedResize($width, $height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate patterns that will help to match filenames of cached images
|
||||
*
|
||||
* @param string $filename Filename of source image
|
||||
* @return array
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate patterns that will help to match filenames of cached images
|
||||
* @param string $filename Filename of source image
|
||||
* @return array
|
||||
*/
|
||||
private function getFilenamePatterns($filename) {
|
||||
$methodNames = $this->allMethodNames(true);
|
||||
$generateFuncs = array();
|
||||
@ -569,7 +568,7 @@ class Image extends File {
|
||||
'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64Match . ")\-/i"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a list of images that were generated from this image
|
||||
*/
|
||||
|
@ -1066,10 +1066,20 @@ class SQLQuery {
|
||||
* @param $alias An optional alias for the aggregate column.
|
||||
*/
|
||||
public function aggregate($column, $alias = null) {
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->setLimit($this->limit);
|
||||
$clone->setOrderBy($this->orderby);
|
||||
|
||||
// don't set an ORDER BY clause if no limit has been set. It doesn't make
|
||||
// sense to add an ORDER BY if there is no limit, and it will break
|
||||
// queries to databases like MSSQL if you do so. Note that the reason
|
||||
// this came up is because DataQuery::initialiseQuery() introduces
|
||||
// a default sort.
|
||||
if($this->limit) {
|
||||
$clone->setLimit($this->limit);
|
||||
$clone->setOrderBy($this->orderby);
|
||||
} else {
|
||||
$clone->setOrderBy(array());
|
||||
}
|
||||
|
||||
$clone->setGroupBy($this->groupby);
|
||||
if($alias) {
|
||||
$clone->setSelect(array());
|
||||
@ -1077,7 +1087,6 @@ class SQLQuery {
|
||||
} else {
|
||||
$clone->setSelect($column);
|
||||
}
|
||||
|
||||
|
||||
return $clone;
|
||||
}
|
||||
@ -1118,10 +1127,15 @@ class SQLQuery {
|
||||
// shift the first FROM table out from so we only deal with the JOINs
|
||||
$baseFrom = array_shift($from);
|
||||
$this->mergesort($from, function($firstJoin, $secondJoin) {
|
||||
if($firstJoin['order'] == $secondJoin['order']) {
|
||||
if(
|
||||
!is_array($firstJoin)
|
||||
|| !is_array($secondJoin)
|
||||
|| $firstJoin['order'] == $secondJoin['order']
|
||||
) {
|
||||
return 0;
|
||||
} else {
|
||||
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
||||
}
|
||||
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
||||
});
|
||||
|
||||
// Put the first FROM table back into the results
|
||||
|
@ -163,7 +163,7 @@ class HTMLText extends Text {
|
||||
}
|
||||
|
||||
// If it's got a content tag
|
||||
if(preg_match('/<(img|embed|object|iframe|meta|source)[^>]*>/i', $this->value)) {
|
||||
if(preg_match('/<(img|embed|object|iframe|meta|source|link)[^>]*>/i', $this->value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -212,7 +212,8 @@ class Money extends DBField implements CompositeDBField {
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasAmount() {
|
||||
return (int)$this->getAmount() != '0';
|
||||
$a = $this->getAmount();
|
||||
return (!empty($a) && is_numeric($a));
|
||||
}
|
||||
|
||||
public function isChanged() {
|
||||
|
@ -45,6 +45,18 @@ $gf_grid_x: 16px;
|
||||
.action {
|
||||
margin-bottom:$gf_grid_y;
|
||||
}
|
||||
}
|
||||
&.ss-gridfield-buttonrow-before{
|
||||
margin-bottom: 0;
|
||||
.action {
|
||||
margin-bottom:$gf_grid_y;
|
||||
}
|
||||
}
|
||||
&.ss-gridfield-buttonrow-after{
|
||||
margin-bottom: 0;
|
||||
.action {
|
||||
margin-top:$gf_grid_y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +128,8 @@ $gf_grid_x: 16px;
|
||||
width: 500px;
|
||||
}
|
||||
.grid-csv-button, .grid-print-button {
|
||||
margin-bottom: $gf_grid_y;
|
||||
margin-bottom: 0;
|
||||
font-size: $font-base-size;
|
||||
@include inline-block();
|
||||
}
|
||||
}
|
||||
@ -641,8 +654,4 @@ $gf_grid_x: 16px;
|
||||
border-right: 1px solid $gf_colour_border;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-bottom-button {
|
||||
margin-top:$gf_grid_y;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
@import 'compass';
|
||||
@import "../admin/scss/_mixins";
|
||||
|
||||
div.TreeDropdownField {
|
||||
width: 400px;
|
||||
background: #fff;
|
||||
@ -20,20 +23,49 @@ div.TreeDropdownField {
|
||||
line-height: 16px;
|
||||
overflow:hidden;
|
||||
outline: none;
|
||||
z-index:1;
|
||||
@include hide-text-overflow;
|
||||
}
|
||||
|
||||
.treedropdownfield-search{
|
||||
@extend .treedropdownfield-title;
|
||||
|
||||
//Style search box to match chosen search
|
||||
$bgImage: '../admin/thirdparty/chosen/chosen/chosen-sprite.png';
|
||||
|
||||
background:url($bgImage) no-repeat 100% -22px; //For browers that only support 1 background
|
||||
@include background(
|
||||
url($bgImage) no-repeat 100% -22px,
|
||||
linear-gradient(top, #eeeeee 1%, #ffffff 15%)
|
||||
);
|
||||
@include box-sizing(border-box);
|
||||
position:relative;
|
||||
z-index:1100; //Needed to work within modales in chrome
|
||||
border: 1px solid #aaa;
|
||||
display:inline-block;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
margin:1.5%;
|
||||
outline: 0;
|
||||
padding: 4px 20px 4px 5px;
|
||||
width:97%; //optimized for most common tree width
|
||||
}
|
||||
|
||||
&.searchable .treedropdownfield-panel.loading{
|
||||
min-height: 16px /* icon */ + 14px /* padding */ + 34px /* approx height search input */; // Ensure there's room for loading indicator
|
||||
background-position: 98% 39px;
|
||||
}
|
||||
|
||||
.treedropdownfield-panel {
|
||||
clear: left;
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
cursor: default;
|
||||
border: 1px solid #aaa;
|
||||
border-top: none;
|
||||
margin: 1px 0 0 -1px; /* account for border on container div */
|
||||
max-height:200px;
|
||||
background-color: #fff;
|
||||
z-index: 50;
|
||||
z-index: 70;
|
||||
-webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15);
|
||||
-moz-box-shadow : 0 4px 5px rgba(0,0,0,.15);
|
||||
-o-box-shadow : 0 4px 5px rgba(0,0,0,.15);
|
||||
@ -41,12 +73,33 @@ div.TreeDropdownField {
|
||||
|
||||
&.loading {
|
||||
min-height: 16px /* icon */ + 14px /* padding */; // Ensure there's room for loading indicator
|
||||
background: #fff url("../images/network-save.gif") 7px 7px no-repeat;
|
||||
background: #fff url("../images/network-save.gif") 98% 7px no-repeat;
|
||||
}
|
||||
|
||||
.tree-holder{
|
||||
position:relative;
|
||||
z-index:1;
|
||||
> ul{
|
||||
position:relative;
|
||||
max-height:200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
ul{
|
||||
overflow-x:hidden;
|
||||
float:left;
|
||||
width:100%;
|
||||
.jstree-icon{
|
||||
margin-left:5px; //move to align with possible search box
|
||||
}
|
||||
.jstree-open > ins{
|
||||
background-position:-18px 0; //move to align with possible search box
|
||||
}
|
||||
}
|
||||
|
||||
ul.tree {
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import "compass/css3";
|
||||
|
||||
body {
|
||||
background-color: #eee;
|
||||
background: #eee !important;
|
||||
margin:0;
|
||||
overflow-x: hidden;
|
||||
padding:0;
|
||||
@ -20,7 +20,8 @@ body {
|
||||
linear-gradient(darken(#003050, 5%), #003050 10%, #003050 90%, darken(#003050, 5%))
|
||||
);
|
||||
|
||||
|
||||
// try to get the info above the template with z-index
|
||||
z-index: 9999;
|
||||
h1 {
|
||||
margin: 0 0 6px 0;
|
||||
padding: 0 32px 0 0;
|
||||
@ -71,9 +72,14 @@ body {
|
||||
.build,
|
||||
.options {
|
||||
padding:6px 12px;
|
||||
|
||||
|
||||
background: #eee !important;
|
||||
// try to get the info above the template with z-index
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
li {
|
||||
font-size:14px; margin:6px 0;
|
||||
font-size:14px;
|
||||
margin:6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,8 +96,9 @@ class Group extends DataObject {
|
||||
if($this->ID) {
|
||||
$group = $this;
|
||||
$config = new GridFieldConfig_RelationEditor();
|
||||
$config->addComponents(new GridFieldExportButton('after'));
|
||||
$config->addComponents(new GridFieldPrintButton('after'));
|
||||
$config->addComponent(new GridFieldButtonRow('after'));
|
||||
$config->addComponents(new GridFieldExportButton('buttons-after-left'));
|
||||
$config->addComponents(new GridFieldPrintButton('buttons-after-left'));
|
||||
$config->getComponentByType('GridFieldAddExistingAutocompleter')
|
||||
->setResultsFormat('$Title ($Email)')->setSearchFields(array('FirstName', 'Surname', 'Email'));
|
||||
$config->getComponentByType('GridFieldDetailForm')
|
||||
@ -336,6 +337,31 @@ class Group extends DataObject {
|
||||
public function setCode($val){
|
||||
$this->setField("Code", Convert::raw2url($val));
|
||||
}
|
||||
|
||||
public function validate() {
|
||||
$result = parent::validate();
|
||||
|
||||
// Check if the new group hierarchy would add certain "privileged permissions",
|
||||
// and require an admin to perform this change in case it does.
|
||||
// This prevents "sub-admin" users with group editing permissions to increase their privileges.
|
||||
if($this->Parent()->exists() && !Permission::check('ADMIN')) {
|
||||
$inheritedCodes = Permission::get()
|
||||
->filter('GroupID', $this->Parent()->collateAncestorIDs())
|
||||
->column('Code');
|
||||
$privilegedCodes = Config::inst()->get('Permission', 'privileged_permissions');
|
||||
if(array_intersect($inheritedCodes, $privilegedCodes)) {
|
||||
$result->error(sprintf(
|
||||
_t(
|
||||
'Group.HierarchyPermsError',
|
||||
'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)'
|
||||
),
|
||||
$this->Parent()->Title
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function onBeforeWrite() {
|
||||
parent::onBeforeWrite();
|
||||
|
@ -1169,7 +1169,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$password->setCanBeEmpty(true);
|
||||
if(!$this->ID) $password->showOnClick = false;
|
||||
$mainFields->replaceField('Password', $password);
|
||||
|
||||
|
||||
$mainFields->replaceField('Locale', new DropdownField(
|
||||
"Locale",
|
||||
_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
|
||||
@ -1231,7 +1231,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
|
||||
if($permissionsTab) $permissionsTab->addExtraClass('readonly');
|
||||
|
||||
$defaultDateFormat = Zend_Locale_Format::getDateFormat($this->Locale);
|
||||
$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($this->Locale));
|
||||
$dateFormatMap = array(
|
||||
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
|
||||
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
|
||||
@ -1249,7 +1249,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
);
|
||||
$dateFormatField->setValue($this->DateFormat);
|
||||
|
||||
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat($this->Locale);
|
||||
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($this->Locale));
|
||||
$timeFormatMap = array(
|
||||
'h:mm a' => Zend_Date::now()->toString('h:mm a'),
|
||||
'H:mm' => Zend_Date::now()->toString('H:mm'),
|
||||
|
@ -86,6 +86,17 @@ class Permission extends DataObject implements TemplateGlobalProvider {
|
||||
*/
|
||||
private static $hidden_permissions = array();
|
||||
|
||||
/**
|
||||
* @config These permissions can only be applied by ADMIN users, to prevent
|
||||
* privilege escalation on group assignments and inheritance.
|
||||
* @var array
|
||||
*/
|
||||
private static $privileged_permissions = array(
|
||||
'ADMIN',
|
||||
'APPLY_ROLES',
|
||||
'EDIT_PERMISSIONS'
|
||||
);
|
||||
|
||||
/**
|
||||
* Check that the current member has the given permission.
|
||||
*
|
||||
|
@ -162,6 +162,8 @@ class PermissionCheckboxSetField extends FormField {
|
||||
$options = '';
|
||||
$globalHidden = (array)Config::inst()->get('Permission', 'hidden_permissions');
|
||||
if($this->source) {
|
||||
$privilegedPermissions = Permission::config()->privileged_permissions;
|
||||
|
||||
// loop through all available categorized permissions and see if they're assigned for the given groups
|
||||
foreach($this->source as $categoryName => $permissions) {
|
||||
$options .= "<li><h5>$categoryName</h5></li>";
|
||||
@ -194,6 +196,11 @@ class PermissionCheckboxSetField extends FormField {
|
||||
$inheritMessage = ' (' . join(', ', $uninheritedCodes[$code]).')';
|
||||
}
|
||||
|
||||
// Disallow modification of "privileged" permissions unless currently logged-in user is an admin
|
||||
if(!Permission::check('ADMIN') && in_array($code, $privilegedPermissions)) {
|
||||
$disabled = ' disabled="true"';
|
||||
}
|
||||
|
||||
// If the field is readonly, always mark as "disabled"
|
||||
if($this->readonly) $disabled = ' disabled="true"';
|
||||
|
||||
@ -246,6 +253,16 @@ class PermissionCheckboxSetField extends FormField {
|
||||
$fieldname = $this->name;
|
||||
$managedClass = $this->managedClass;
|
||||
|
||||
// Remove all "privileged" permissions if the currently logged-in user is not an admin
|
||||
$privilegedPermissions = Permission::config()->privileged_permissions;
|
||||
if(!Permission::check('ADMIN')) {
|
||||
foreach($this->value as $id => $bool) {
|
||||
if(in_array($id, $privilegedPermissions)) {
|
||||
unset($this->value[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all permissions and re-add them afterwards
|
||||
$permissions = $record->$fieldname();
|
||||
foreach ( $permissions as $permission ) {
|
||||
|
@ -76,4 +76,20 @@ class PermissionRole extends DataObject {
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
public function canView($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
|
||||
public function canEdit($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
}
|
||||
|
@ -13,4 +13,38 @@ class PermissionRoleCode extends DataObject {
|
||||
private static $has_one = array(
|
||||
"Role" => "PermissionRole",
|
||||
);
|
||||
|
||||
protected function validate() {
|
||||
$result = parent::validate();
|
||||
|
||||
// Check that new code doesn't increase privileges, unless an admin is editing.
|
||||
$privilegedCodes = Config::inst()->get('Permission', 'privileged_permissions');
|
||||
if(
|
||||
$this->Code
|
||||
&& in_array($this->Code, $privilegedCodes)
|
||||
&& !Permission::check('ADMIN')
|
||||
) {
|
||||
$result->error(sprintf(
|
||||
_t(
|
||||
'PermissionRoleCode.PermsError',
|
||||
'Can\'t assign code "%s" with privileged permissions (requires ADMIN access)'
|
||||
),
|
||||
$this->Code
|
||||
));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
|
||||
public function canEdit($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('APPLY_ROLES', 'any', $member);
|
||||
}
|
||||
}
|
||||
|
@ -280,6 +280,9 @@ class Security extends Controller {
|
||||
$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
|
||||
public function index() {
|
||||
return $this->httpError(404); // no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the login form to process according to the submitted data
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="addNewGridFieldButton ss-gridfield-buttonrow">
|
||||
<div class="ss-gridfield-buttonrow ss-gridfield-buttonrow-{$TargetFragmentName}">
|
||||
<div class="left">$LeftFragment</div>
|
||||
<div class="right">$RightFragment</div>
|
||||
</div>
|
@ -67,4 +67,3 @@
|
||||
<div class="clear"><!-- --></div>
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% if Description %><span class="description">$Description</span><% end_if %>
|
||||
|
@ -196,6 +196,74 @@ class RestfulServiceTest extends SapphireTest {
|
||||
);
|
||||
}
|
||||
|
||||
public function testExtractResponseRedirectionAndProxy() {
|
||||
// This is an example of real raw response for a request via a proxy that gets redirected.
|
||||
$rawHeaders =
|
||||
"HTTP/1.0 200 Connection established\r\n" .
|
||||
"\r\n" .
|
||||
"HTTP/1.1 301 Moved Permanently\r\n" .
|
||||
"Server: nginx\r\n" .
|
||||
"Date: Fri, 20 Sep 2013 01:53:07 GMT\r\n" .
|
||||
"Content-Type: text/html\r\n" .
|
||||
"Content-Length: 178\r\n" .
|
||||
"Connection: keep-alive\r\n" .
|
||||
"Location: https://www.foobar.org.nz/\r\n" .
|
||||
"\r\n" .
|
||||
"HTTP/1.0 200 Connection established\r\n" .
|
||||
"\r\n" .
|
||||
"HTTP/1.1 200 OK\r\n" .
|
||||
"Server: nginx\r\n" .
|
||||
"Date: Fri, 20 Sep 2013 01:53:08 GMT\r\n" .
|
||||
"Content-Type: text/html; charset=utf-8\r\n" .
|
||||
"Transfer-Encoding: chunked\r\n" .
|
||||
"Connection: keep-alive\r\n" .
|
||||
"X-Frame-Options: SAMEORIGIN\r\n" .
|
||||
"Cache-Control: no-cache, max-age=0, must-revalidate, no-transform\r\n" .
|
||||
"Vary: Accept-Encoding\r\n" .
|
||||
"\r\n"
|
||||
;
|
||||
|
||||
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
|
||||
$headerFunction->setAccessible(true);
|
||||
|
||||
$ch = curl_init();
|
||||
$response = $headerFunction->invoke(
|
||||
new RestfulService(Director::absoluteBaseURL(),0),
|
||||
$ch,
|
||||
$rawHeaders,
|
||||
''
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$response->getHeaders(),
|
||||
array(
|
||||
'Server' => "nginx",
|
||||
'Date' => "Fri, 20 Sep 2013 01:53:08 GMT",
|
||||
'Content-Type' => "text/html; charset=utf-8",
|
||||
'Transfer-Encoding' => "chunked",
|
||||
'Connection' => "keep-alive",
|
||||
'X-Frame-Options' => "SAMEORIGIN",
|
||||
'Cache-Control' => "no-cache, max-age=0, must-revalidate, no-transform",
|
||||
'Vary' => "Accept-Encoding"
|
||||
),
|
||||
'Only last header is extracted and parsed.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testExtractResponseNoHead() {
|
||||
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
|
||||
$headerFunction->setAccessible(true);
|
||||
|
||||
$ch = curl_init();
|
||||
$response = $headerFunction->invoke(
|
||||
new RestfulService(Director::absoluteBaseURL(),0),
|
||||
$ch,
|
||||
'',
|
||||
''
|
||||
);
|
||||
|
||||
$this->assertEquals($response->getHeaders(), array(), 'Headers are correctly extracted.');
|
||||
}
|
||||
}
|
||||
|
||||
class RestfulServiceTest_Controller extends Controller implements TestOnly {
|
||||
|
28
tests/behat/features/apply-formatting.feature
Normal file
28
tests/behat/features/apply-formatting.feature
Normal file
@ -0,0 +1,28 @@
|
||||
Feature: Apply rich formatting to content
|
||||
As a cms author
|
||||
I want to work with content in the way I'm used to from word processing software
|
||||
So that I make it more appealing by creating structure and highlights
|
||||
|
||||
Background:
|
||||
Given a "page" "About Us" has the "Content" "<h1>My awesome headline</h1><p>Some amazing content</p>"
|
||||
And I am logged in with "ADMIN" permissions
|
||||
And I go to "/admin/pages"
|
||||
Then I click on "About Us" in the tree
|
||||
|
||||
Scenario: I can control alignment of selected content
|
||||
Given I select "My awesome headline" in the "Content" HTML field
|
||||
When I press the "Align Right" button
|
||||
Then "My awesome headline" in the "Content" HTML field should be right aligned
|
||||
But "Some amazing content" in the "Content" HTML field should be left aligned
|
||||
Then I press the "Save draft" button
|
||||
Then "My awesome headline" in the "Content" HTML field should be right aligned
|
||||
|
||||
Scenario: I can bold selected content
|
||||
Given I select "awesome" in the "Content" HTML field
|
||||
When I press the "Bold (Ctrl+B)" button
|
||||
Then "awesome" in the "Content" HTML field should be bold
|
||||
But "My" in the "Content" HTML field should not be bold
|
||||
When I press the "Save draft" button
|
||||
Then "awesome" in the "Content" HTML field should be bold
|
||||
But "My" in the "Content" HTML field should not be bold
|
||||
|
@ -11,6 +11,8 @@ use Behat\Behat\Context\ClosuredContextInterface,
|
||||
Behat\Gherkin\Node\PyStringNode,
|
||||
Behat\Gherkin\Node\TableNode;
|
||||
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
// PHPUnit
|
||||
require_once 'PHPUnit/Autoload.php';
|
||||
require_once 'PHPUnit/Framework/Assert/Functions.php';
|
||||
@ -89,7 +91,7 @@ class CmsFormsContext extends BehatContext
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the "(?P<locator>([^"]*))" HTML field should contain "(?P<html>([^"]*))"$/
|
||||
* @Then /^the "(?P<locator>([^"]*))" HTML field should contain "(?P<html>.*)"$/
|
||||
*/
|
||||
public function theHtmlFieldShouldContain($locator, $html)
|
||||
{
|
||||
@ -101,15 +103,92 @@ class CmsFormsContext extends BehatContext
|
||||
$regex = '/'.preg_quote($html, '/').'/ui';
|
||||
if (!preg_match($regex, $actual)) {
|
||||
$message = sprintf(
|
||||
'The string "%s" was not found in the HTML of the element matching %s "%s".',
|
||||
'The string "%s" was not found in the HTML of the element matching %s "%s". Actual content: "%s"',
|
||||
$html,
|
||||
'named',
|
||||
$locator
|
||||
$locator,
|
||||
$actual
|
||||
);
|
||||
throw new ElementHtmlException($message, $this->getSession(), $element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks formatting in the HTML field, by analyzing the HTML node surrounding
|
||||
* the text for certain properties.
|
||||
*
|
||||
* Example: Given "my text" in the "Content" HTML field should be right aligned
|
||||
* Example: Given "my text" in the "Content" HTML field should not be bold
|
||||
*
|
||||
* @todo Use an actual DOM parser for more accurate assertions
|
||||
*
|
||||
* @Given /^"(?P<text>([^"]*))" in the "(?P<field>([^"]*))" HTML field should(?P<negate>(?: not)?) be (?P<formatting>(.*))$/
|
||||
*/
|
||||
public function stepContentInHtmlFieldShouldHaveFormatting($text, $field, $negate, $formatting) {
|
||||
$page = $this->getSession()->getPage();
|
||||
$inputField = $page->findField($field);
|
||||
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
||||
|
||||
$crawler = new Crawler($inputField->getValue());
|
||||
$matchedNode = null;
|
||||
foreach($crawler->filterXPath('//*') as $node) {
|
||||
if(
|
||||
$node->firstChild
|
||||
&& $node->firstChild->nodeType == XML_TEXT_NODE
|
||||
&& stripos($node->firstChild->nodeValue, $text) !== FALSE
|
||||
) {
|
||||
$matchedNode = $node;
|
||||
}
|
||||
}
|
||||
assertNotNull($matchedNode);
|
||||
|
||||
$assertFn = $negate ? 'assertNotEquals' : 'assertEquals';
|
||||
if($formatting == 'bold') {
|
||||
call_user_func($assertFn, 'strong', $matchedNode->nodeName);
|
||||
} else if($formatting == 'left aligned') {
|
||||
if($matchedNode->getAttribute('style')) {
|
||||
call_user_func($assertFn, 'text-align: left;', $matchedNode->getAttribute('style'));
|
||||
}
|
||||
} else if($formatting == 'right aligned') {
|
||||
call_user_func($assertFn, 'text-align: right;', $matchedNode->getAttribute('style'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the first textual match in the HTML editor. Does not support
|
||||
* selection across DOM node boundaries.
|
||||
*
|
||||
* @When /^I select "(?P<text>([^"]*))" in the "(?P<field>([^"]*))" HTML field$/
|
||||
*/
|
||||
public function stepIHighlightTextInHtmlField($text, $field)
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
$inputField = $page->findField($field);
|
||||
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
||||
$inputFieldId = $inputField->getAttribute('id');
|
||||
$text = addcslashes($text, "'");
|
||||
|
||||
$js = <<<JS
|
||||
// TODO <IE9 support
|
||||
// TODO Allow text matches across nodes
|
||||
var editor = jQuery('#$inputFieldId').entwine('ss').getEditor(),
|
||||
doc = editor.getDOM().doc,
|
||||
sel = editor.getInstance().selection,
|
||||
rng = document.createRange(),
|
||||
matched = false;
|
||||
jQuery(doc).find('body *').each(function() {
|
||||
if(!matched && this.firstChild && this.firstChild.nodeValue && this.firstChild.nodeValue.match('$text')) {
|
||||
rng.setStart(this.firstChild, this.firstChild.nodeValue.indexOf('$text'));
|
||||
rng.setEnd(this.firstChild, this.firstChild.nodeValue.indexOf('$text') + '$text'.length);
|
||||
sel.setRng(rng);
|
||||
editor.getInstance().nodeChanged();
|
||||
matched = true;
|
||||
}
|
||||
});
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I should see a "([^"]*)" button$/
|
||||
*/
|
||||
|
@ -140,7 +140,7 @@ class CmsUiContext extends BehatContext
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I should see "([^"]*)" in CMS Tree$/
|
||||
* @When /^I should see "([^"]*)" in the tree$/
|
||||
*/
|
||||
public function stepIShouldSeeInCmsTree($text)
|
||||
{
|
||||
@ -151,7 +151,7 @@ class CmsUiContext extends BehatContext
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I should not see "([^"]*)" in CMS Tree$/
|
||||
* @When /^I should not see "([^"]*)" in the tree$/
|
||||
*/
|
||||
public function stepIShouldNotSeeInCmsTree($text)
|
||||
{
|
||||
@ -161,6 +161,17 @@ class CmsUiContext extends BehatContext
|
||||
assertNull($element, sprintf('%s found', $text));
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I click on "([^"]*)" in the tree$/
|
||||
*/
|
||||
public function stepIClickOnElementInTheTree($text)
|
||||
{
|
||||
$treeEl = $this->getCmsTreeElement();
|
||||
$treeNode = $treeEl->findLink($text);
|
||||
assertNotNull($treeNode, sprintf('%s not found', $text));
|
||||
$treeNode->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I expand the "([^"]*)" CMS Panel$/
|
||||
*/
|
||||
@ -329,7 +340,10 @@ class CmsUiContext extends BehatContext
|
||||
$field = $this->fixStepArgument($field);
|
||||
$value = $this->fixStepArgument($value);
|
||||
|
||||
$nativeField = $this->getSession()->getPage()->findField($field);
|
||||
$nativeField = $this->getSession()->getPage()->find(
|
||||
'named',
|
||||
array('select', $this->getSession()->getSelectorsHandler()->xpathLiteral($field))
|
||||
);
|
||||
if($nativeField) {
|
||||
$nativeField->selectOption($value);
|
||||
return;
|
||||
@ -340,7 +354,9 @@ class CmsUiContext extends BehatContext
|
||||
|
||||
// Find by label
|
||||
$formField = $this->getSession()->getPage()->findField($field);
|
||||
if($formField) $formFields[] = $formField;
|
||||
if($formField && $formField->getTagName() == 'select') {
|
||||
$formFields[] = $formField;
|
||||
}
|
||||
|
||||
// Fall back to finding by title (for dropdowns without a label)
|
||||
if(!$formFields) {
|
||||
@ -358,6 +374,15 @@ class CmsUiContext extends BehatContext
|
||||
$formFields = $this->getSession()->getPage()->findAll('xpath', "//*[@name='$field']");
|
||||
}
|
||||
|
||||
// Find by label
|
||||
if(!$formFields) {
|
||||
$label = $this->getSession()->getPage()->find('xpath', "//label[.='$field']");
|
||||
if($label && $for = $label->getAttribute('for')) {
|
||||
$formField = $this->getSession()->getPage()->find('xpath', "//*[@id='$for']");
|
||||
if($formField) $formFields[] = $formField;
|
||||
}
|
||||
}
|
||||
|
||||
assertGreaterThan(0, count($formFields), sprintf(
|
||||
'Chosen.js dropdown named "%s" not found',
|
||||
$field
|
||||
|
83
tests/behat/features/insert-a-link.feature
Normal file
83
tests/behat/features/insert-a-link.feature
Normal file
@ -0,0 +1,83 @@
|
||||
@assets
|
||||
Feature: Insert links into a page
|
||||
As a cms author
|
||||
I want to insert a link into my content
|
||||
So that I can link to a external website or a page on my site
|
||||
|
||||
Background:
|
||||
Given a "page" "Home"
|
||||
And a "page" "About Us" has the "Content" "My awesome content"
|
||||
#And a "file" "assets/folder1/file1.jpg"
|
||||
And I am logged in with "ADMIN" permissions
|
||||
And I go to "/admin/pages"
|
||||
And I click on "About Us" in the tree
|
||||
|
||||
Scenario: I can link to an internal page
|
||||
Given I select "awesome" in the "Content" HTML field
|
||||
And I press the "Insert Link" button
|
||||
When I check "Page on the site"
|
||||
And I fill in the "Page" dropdown with "Home"
|
||||
And I fill in "my desc" for "Link description"
|
||||
And I press the "Insert" button
|
||||
# TODO Dynamic DB identifiers
|
||||
Then the "Content" HTML field should contain "<a title="my desc" href="[sitetree_link,id=1]">awesome</a>"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
|
||||
Scenario: I can link to an external URL
|
||||
Given I select "awesome" in the "Content" HTML field
|
||||
And I press the "Insert Link" button
|
||||
|
||||
When I check "Another website"
|
||||
And I fill in "http://silverstripe.org" for "URL"
|
||||
And I check "Open link in a new window"
|
||||
And I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "<a href="http://silverstripe.org" target="_blank">awesome</a>"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
|
||||
@todo
|
||||
Scenario: I can link to a file
|
||||
Given I select "awesome" in the "Content" HTML field
|
||||
When I press the "Insert Link" button
|
||||
When I check "Download a file"
|
||||
And I fill in the "File" dropdown with "file1.jpg"
|
||||
And I press the "Insert link" button
|
||||
Then the "Content" HTML field should contain "<a href="assets/folder1/file1.jpg">awesome</a>"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
|
||||
|
||||
@todo
|
||||
Scenario: I can link to an anchor
|
||||
Given I fill in the "Content" HTML field with "My awesome content <a name=myanchor>"
|
||||
And I select "awesome" in the "Content" HTML field
|
||||
When I press the "Insert Link" button
|
||||
When I check "Anchor on this page"
|
||||
And I fill in the "Select an anchor" dropdown with "myanchor"
|
||||
And I press the "Insert link" button
|
||||
Then the "Content" HTML field should contain "<a href="#myanchor">awesome</a>"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
|
||||
@todo
|
||||
Scenario: I can edit a link
|
||||
Given I fill in the "Content" HTML field with "My <a href="http://silverstripe.org">awesome</a> content"
|
||||
And I select "awesome"
|
||||
When I press the "Insert Link" button
|
||||
And the "URL" field should contain "http://silverstripe.org"
|
||||
When I fill in "http://wordpress.org" for "URL"
|
||||
And I press the "Insert link" button
|
||||
Then the "Content" HTML field should contain "<a href="http://wordpress.org">awesome</a>"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
|
||||
@todo
|
||||
Scenario: I can delete a link
|
||||
Given I fill in the "Content" HTML field with "My <a href="http://silverstripe.org">awesome</a> content"
|
||||
And I select "awesome"
|
||||
When I press the "Insert Link" button
|
||||
And I press the "Remove link" button
|
||||
Then the "Content" HTML field should not contain "<a href="http://silverstripe.org">awesome</a>"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
@ -1,29 +1,95 @@
|
||||
@assets
|
||||
Feature: Insert an image into a page
|
||||
As a cms author
|
||||
I want to insert an image into a page
|
||||
So that I can insert them into my content efficiently
|
||||
As a cms author
|
||||
I want to insert an image into a page
|
||||
So that I can insert them into my content efficiently
|
||||
|
||||
Background:
|
||||
Given a "page" "About Us"
|
||||
Given I am logged in with "ADMIN" permissions
|
||||
And I go to "/admin/pages"
|
||||
Then I should see "About Us" in CMS Tree
|
||||
Background:
|
||||
Given a "page" "About Us"
|
||||
#And a "file" "assets/folder1/file1.jpg"
|
||||
#And a "file" "assets/folder1/file3.jpg"
|
||||
#And a "file" "assets/folder1/folder1.1/file2.jpg"
|
||||
#And a "folder" "assets/folder2"
|
||||
And I am logged in with "ADMIN" permissions
|
||||
And I go to "/admin/pages"
|
||||
And I click on "About Us" in the tree
|
||||
|
||||
@javascript
|
||||
Scenario: I can insert images into the content
|
||||
When I follow "About Us"
|
||||
Then I should see an edit page form
|
||||
Scenario: I can insert an image from a URL
|
||||
Given I press the "Insert Media" button
|
||||
Then I should see "Choose files to upload..."
|
||||
|
||||
When I press the "Insert Media" button
|
||||
Then I should see "Choose files to upload..."
|
||||
When I press the "From the web" button
|
||||
And I fill in "RemoteURL" with "http://www.silverstripe.com/themes/sscom/images/silverstripe_logo_web.png"
|
||||
And I press the "Add url" button
|
||||
Then I should see "silverstripe_logo_web.png (www.silverstripe.com)" in the ".ss-assetuploadfield span.name" element
|
||||
|
||||
When I press the "From the web" button
|
||||
And I fill in "RemoteURL" with "http://www.silverstripe.com/themes/sscom/images/silverstripe_logo_web.png"
|
||||
And I press the "Add url" button
|
||||
Then I should see "silverstripe_logo_web.png (www.silverstripe.com)" in the ".ss-assetuploadfield span.name" element
|
||||
When I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "silverstripe_logo_web.png"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
|
||||
When I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "silverstripe_logo_web.png"
|
||||
# Required to avoid "unsaved changed" browser dialog
|
||||
Then I press the "Save draft" button
|
||||
@todo
|
||||
Scenario: I can insert an image uploaded from my own computer
|
||||
Given I press the "Insert Media" button
|
||||
And I press the "From your computer" button
|
||||
# TODO Figure out how to provide the file
|
||||
And I attach the file "testfile.jpg" to "AssetUploadField" with HTML5
|
||||
Then the upload field should have successfully uploaded "testfile.jpg"
|
||||
When I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "testfile.jpg"
|
||||
|
||||
@todo
|
||||
Scenario: I can insert an image from the CMS file store
|
||||
Given I press the "Insert Media" button
|
||||
And I press the "From the CMS" button
|
||||
And I select "folder1" in the "Find in Folder" dropdown
|
||||
And I select "file1.jpg"
|
||||
When I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "file1.jpg"
|
||||
|
||||
@todo
|
||||
Scenario: I can insert multiple images at once
|
||||
Given I press the "Insert Media" button
|
||||
And I press the "From the CMS" button
|
||||
And I select "folder1" in the "Find in Folder" dropdown
|
||||
And I select "file1.jpg"
|
||||
And I select "file3.jpg"
|
||||
When I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "file1.jpg"
|
||||
And the "Content" HTML field should contain "file1.jpg"
|
||||
|
||||
@todo
|
||||
Scenario: I can edit properties of an image before inserting it
|
||||
Given I press the "Insert Media" button
|
||||
And I press the "From the CMS" button
|
||||
And I select "folder1" in the "Find in Folder" dropdown
|
||||
And I select "file1.jpg"
|
||||
And I follow "Edit"
|
||||
When I fill in "Alternative text (alt)" with "My alt"
|
||||
And I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "file1.jpg"
|
||||
And the "Content" HTML field should contain "My alt"
|
||||
|
||||
@todo
|
||||
Scenario: I can edit dimensions of an image before inserting it
|
||||
Given I press the "Insert Media" button
|
||||
And I press the "From the CMS" button
|
||||
And I select "folder1" in the "Find in Folder" dropdown
|
||||
And I select "file1.jpg"
|
||||
And I follow "Edit"
|
||||
When I fill in "Width" with "10"
|
||||
When I fill in "Height" with "20"
|
||||
And I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "<img src=assets/folder1/file1.jpg width=10 height=20>"
|
||||
|
||||
@todo
|
||||
Scenario: I can edit dimensions of an existing image
|
||||
Given the "page" "About us" contains "<img src=assets/folder1/file1.jpg>"
|
||||
And I reload the current page
|
||||
When I highlight "<img src=assets/folder1/file1.jpg>" in the "Content" HTML field
|
||||
And I press the "Insert Media" button
|
||||
Then I should see "file1.jpg"
|
||||
When I fill in "Width" with "10"
|
||||
When I fill in "Height" with "20"
|
||||
And I press the "Insert" button
|
||||
Then the "Content" HTML field should contain "<img src=assets/folder1/file1.jpg width=10 height=20>"
|
@ -49,7 +49,7 @@ Feature: Manage files
|
||||
Scenario: I can change the folder of a file
|
||||
Given I click on "folder1" in the "Files" table
|
||||
And I click on "file1" in the "folder1" table
|
||||
And I fill in "folder2" for the "ParentID" dropdown
|
||||
And I fill in "folder2" for the "Folder" dropdown
|
||||
And I press the "Save" button
|
||||
# /show/0 is to ensure that we are on top level folder
|
||||
And I go to "/admin/assets/show/0"
|
||||
|
@ -1,12 +1,38 @@
|
||||
Feature: My Profile
|
||||
@todo
|
||||
Feature: Manage my own settings
|
||||
As a CMS user
|
||||
I want to be able to change personal settings
|
||||
In order to streamline my CMS experience
|
||||
|
||||
@javascript
|
||||
Scenario: I can see date formatting help
|
||||
Given I am logged in with "ADMIN" permissions
|
||||
# Only tests this specific field and admin UI because its got built-in tooltips
|
||||
When I go to "/admin/myprofile"
|
||||
And I follow "Show formatting help"
|
||||
Then I should see "Four-digit year"
|
||||
Background:
|
||||
Given a "member" "Joe" belongs to "Admin Group" with "Email"="joe@test.com" and "Password"="secret"
|
||||
And the "group" "Admin Group" has permissions "Full administrative rights"
|
||||
And I am logged in with "joe@test.com" and "secret"
|
||||
And I navigate to "admin/myprofile"
|
||||
|
||||
Scenario: I can edit my personal details
|
||||
Given I fill in "First Name" with "Jack"
|
||||
And I fill in "Surname" with "Johnson"
|
||||
And I fill in "Email" with "jack@test.com"
|
||||
When I press the "Save" button
|
||||
Then I should not see "John"
|
||||
But I should see "Jack"
|
||||
And I should see "Johnson"
|
||||
And I should see "jack@test.com"
|
||||
|
||||
Scenario: I can change my password
|
||||
Given I click "Change Password"
|
||||
And I fill out "Password" with "newsecret"
|
||||
And I fill out "Confirm Password" with "newsecret"
|
||||
And I press the "Save" button
|
||||
And I log out
|
||||
When I login with "joe@test.com" and "newsecret"
|
||||
Then I should see the CMS
|
||||
|
||||
Scenario: I can change the interface language
|
||||
Given I fill in "Interface Language" with "German (Germany)"
|
||||
And I press the "Save" button
|
||||
Then I should see "Sprache"
|
||||
|
||||
# TODO Date/time format - Difficult because its not exposed anywhere in the CMS?
|
||||
# TODO Group modification as ADMIN user
|
@ -236,9 +236,9 @@ class ControllerTest extends FunctionalTest {
|
||||
$this->assertEquals("admin/crm?existing=1&flush=1", Controller::join_links("admin/crm?existing=1", "?flush=1"));
|
||||
$this->assertEquals("admin/crm/MyForm?a=1&b=2&c=3",
|
||||
Controller::join_links("?a=1", "admin/crm", "?b=2", "MyForm?c=3"));
|
||||
|
||||
/* Note, however, that it doesn't deal with duplicates very well. */
|
||||
$this->assertEquals("admin/crm?flush=1&flush=1", Controller::join_links("admin/crm?flush=1", "?flush=1"));
|
||||
|
||||
// And duplicates are handled nicely
|
||||
$this->assertEquals("admin/crm?foo=2&bar=3&baz=1", Controller::join_links("admin/crm?foo=1&bar=1&baz=1", "?foo=2&bar=3"));
|
||||
|
||||
$this->assertEquals (
|
||||
'admin/action', Controller::join_links('admin/', '/', '/action'), 'Test that multiple slashes are trimmed.'
|
||||
|
@ -21,6 +21,45 @@ class FormFieldTest extends SapphireTest {
|
||||
$this->assertStringEndsWith('class2', $field->extraClass());
|
||||
}
|
||||
|
||||
public function testAddManyExtraClasses() {
|
||||
$field = new FormField('MyField');
|
||||
//test we can split by a range of spaces and tabs
|
||||
$field->addExtraClass('class1 class2 class3 class4 class5');
|
||||
$this->assertStringEndsWith(
|
||||
'class1 class2 class3 class4 class5',
|
||||
$field->extraClass()
|
||||
);
|
||||
//test that duplicate classes don't get added
|
||||
$field->addExtraClass('class1 class2');
|
||||
$this->assertStringEndsWith(
|
||||
'class1 class2 class3 class4 class5',
|
||||
$field->extraClass()
|
||||
);
|
||||
}
|
||||
|
||||
public function testRemoveManyExtraClasses() {
|
||||
$field = new FormField('MyField');
|
||||
$field->addExtraClass('class1 class2 class3 class4 class5');
|
||||
//test we can remove a single class we just added
|
||||
$field->removeExtraClass('class3');
|
||||
$this->assertStringEndsWith(
|
||||
'class1 class2 class4 class5',
|
||||
$field->extraClass()
|
||||
);
|
||||
//check we can remove many classes at once
|
||||
$field->removeExtraClass('class1 class5');
|
||||
$this->assertStringEndsWith(
|
||||
'class2 class4',
|
||||
$field->extraClass()
|
||||
);
|
||||
//check that removing a dud class is fine
|
||||
$field->removeExtraClass('dudClass');
|
||||
$this->assertStringEndsWith(
|
||||
'class2 class4',
|
||||
$field->extraClass()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAttributes() {
|
||||
$field = new FormField('MyField');
|
||||
$field->setAttribute('foo', 'bar');
|
||||
|
@ -393,6 +393,61 @@ class FormTest extends FunctionalTest {
|
||||
$this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
|
||||
}
|
||||
|
||||
public function testAddExtraClass() {
|
||||
$form = $this->getStubForm();
|
||||
$form->addExtraClass('class1');
|
||||
$form->addExtraClass('class2');
|
||||
$this->assertStringEndsWith('class1 class2', $form->extraClass());
|
||||
}
|
||||
|
||||
public function testRemoveExtraClass() {
|
||||
$form = $this->getStubForm();
|
||||
$form->addExtraClass('class1');
|
||||
$form->addExtraClass('class2');
|
||||
$this->assertStringEndsWith('class1 class2', $form->extraClass());
|
||||
$form->removeExtraClass('class1');
|
||||
$this->assertStringEndsWith('class2', $form->extraClass());
|
||||
}
|
||||
|
||||
public function testAddManyExtraClasses() {
|
||||
$form = $this->getStubForm();
|
||||
//test we can split by a range of spaces and tabs
|
||||
$form->addExtraClass('class1 class2 class3 class4 class5');
|
||||
$this->assertStringEndsWith(
|
||||
'class1 class2 class3 class4 class5',
|
||||
$form->extraClass()
|
||||
);
|
||||
//test that duplicate classes don't get added
|
||||
$form->addExtraClass('class1 class2');
|
||||
$this->assertStringEndsWith(
|
||||
'class1 class2 class3 class4 class5',
|
||||
$form->extraClass()
|
||||
);
|
||||
}
|
||||
|
||||
public function testRemoveManyExtraClasses() {
|
||||
$form = $this->getStubForm();
|
||||
$form->addExtraClass('class1 class2 class3 class4 class5');
|
||||
//test we can remove a single class we just added
|
||||
$form->removeExtraClass('class3');
|
||||
$this->assertStringEndsWith(
|
||||
'class1 class2 class4 class5',
|
||||
$form->extraClass()
|
||||
);
|
||||
//check we can remove many classes at once
|
||||
$form->removeExtraClass('class1 class5');
|
||||
$this->assertStringEndsWith(
|
||||
'class2 class4',
|
||||
$form->extraClass()
|
||||
);
|
||||
//check that removing a dud class is fine
|
||||
$form->removeExtraClass('dudClass');
|
||||
$this->assertStringEndsWith(
|
||||
'class2 class4',
|
||||
$form->extraClass()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function testAttributes() {
|
||||
$form = $this->getStubForm();
|
||||
|
@ -97,9 +97,9 @@ class i18nTest extends SapphireTest {
|
||||
|
||||
public function testGetExistingTranslations() {
|
||||
$translations = i18n::get_existing_translations();
|
||||
$this->assertTrue(isset($translations['en']), 'Checking for en translation');
|
||||
$this->assertEquals($translations['en'], 'English (United States)');
|
||||
$this->assertTrue(isset($translations['de']), 'Checking for de_DE translation');
|
||||
$this->assertTrue(isset($translations['en_US']), 'Checking for en translation');
|
||||
$this->assertEquals($translations['en_US'], 'English (United States)');
|
||||
$this->assertTrue(isset($translations['de_DE']), 'Checking for de_DE translation');
|
||||
}
|
||||
|
||||
public function testDataObjectFieldLabels() {
|
||||
|
@ -269,4 +269,37 @@ class ImageTest extends SapphireTest {
|
||||
$this->assertEquals(1, $numDeleted, 'Expected one image to be deleted, but deleted ' . $numDeleted . ' images');
|
||||
$this->assertFalse(file_exists($p), 'Resized image not existing after deletion call');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that generated images with multiple image manipulations are all deleted
|
||||
*/
|
||||
public function testMultipleGenerateManipulationCallsImageDeletion() {
|
||||
$image = $this->objFromFixture('Image', 'imageWithMetacharacters');
|
||||
|
||||
$firstImage = $image->SetWidth(200);
|
||||
$firstImagePath = $firstImage->getFullPath();
|
||||
$this->assertTrue(file_exists($firstImagePath));
|
||||
|
||||
$secondImage = $firstImage->SetHeight(100);
|
||||
$secondImagePath = $secondImage->getFullPath();
|
||||
$this->assertTrue(file_exists($secondImagePath));
|
||||
|
||||
$image->deleteFormattedImages();
|
||||
$this->assertFalse(file_exists($firstImagePath));
|
||||
$this->assertFalse(file_exists($secondImagePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests path properties of cached images with multiple image manipulations
|
||||
*/
|
||||
public function testPathPropertiesCachedImage() {
|
||||
$image = $this->objFromFixture('Image', 'imageWithMetacharacters');
|
||||
$firstImage = $image->SetWidth(200);
|
||||
$firstImagePath = $firstImage->getRelativePath();
|
||||
$this->assertEquals($firstImagePath, $firstImage->Filename);
|
||||
|
||||
$secondImage = $firstImage->SetHeight(100);
|
||||
$secondImagePath = $secondImage->getRelativePath();
|
||||
$this->assertEquals($secondImagePath, $secondImage->Filename);
|
||||
}
|
||||
}
|
||||
|
@ -277,6 +277,36 @@ class MoneyTest extends SapphireTest {
|
||||
|
||||
$this->assertEquals('£2.46', $obj->obj('MyOtherMoney')->Nice());
|
||||
}
|
||||
|
||||
public function testHasAmount() {
|
||||
$obj = new MoneyTest_DataObject();
|
||||
$m = new Money();
|
||||
$obj->MyMoney = $m;
|
||||
|
||||
$m->setValue(array('Amount' => 1));
|
||||
$this->assertTrue($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 1.00));
|
||||
$this->assertTrue($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 1.01));
|
||||
$this->assertTrue($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 0.99));
|
||||
$this->assertTrue($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 0.01));
|
||||
$this->assertTrue($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 0));
|
||||
$this->assertFalse($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 0.0));
|
||||
$this->assertFalse($obj->MyMoney->hasAmount());
|
||||
|
||||
$m->setValue(array('Amount' => 0.00));
|
||||
$this->assertFalse($obj->MyMoney->hasAmount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -132,10 +132,21 @@ class SQLQueryTest extends SapphireTest {
|
||||
$query = new SQLQuery();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('RAND()');
|
||||
|
||||
$this->assertEquals(
|
||||
'SELECT *, RAND() AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC',
|
||||
$query->sql());
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query->setFrom("MyTable");
|
||||
$query->addFrom('INNER JOIN SecondTable USING (ID)');
|
||||
$query->addFrom('INNER JOIN ThirdTable USING (ID)');
|
||||
$query->setOrderBy('MyName');
|
||||
$this->assertEquals(
|
||||
'SELECT * FROM MyTable '
|
||||
. 'INNER JOIN SecondTable USING (ID) '
|
||||
. 'INNER JOIN ThirdTable USING (ID) '
|
||||
. 'ORDER BY MyName ASC',
|
||||
$query->sql());
|
||||
}
|
||||
|
||||
public function testNullLimit() {
|
||||
@ -161,6 +172,11 @@ class SQLQueryTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testZeroLimitWithOffset() {
|
||||
if(!(DB::getConn() instanceof MySQLDatabase || DB::getConn() instanceof SQLite3Database
|
||||
|| DB::getConn() instanceof PostgreSQLDatabase)) {
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setLimit(0, 99);
|
||||
@ -414,6 +430,31 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertEquals(array(2), $result->column('cnt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that an ORDER BY is only added if a LIMIT is set.
|
||||
*/
|
||||
public function testAggregateNoOrderByIfNoLimit() {
|
||||
$query = new SQLQuery();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('Common');
|
||||
$query->setLimit(array());
|
||||
|
||||
$aggregate = $query->aggregate('MAX("ID")');
|
||||
$limit = $aggregate->getLimit();
|
||||
$this->assertEquals(array(), $aggregate->getOrderBy());
|
||||
$this->assertEquals(array(), $limit);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('Common');
|
||||
$query->setLimit(2);
|
||||
|
||||
$aggregate = $query->aggregate('MAX("ID")');
|
||||
$limit = $aggregate->getLimit();
|
||||
$this->assertEquals(array('Common' => 'ASC'), $aggregate->getOrderBy());
|
||||
$this->assertEquals(array('start' => 0, 'limit' => 2), $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that "_SortColumn0" is added for an aggregate in the ORDER BY
|
||||
* clause, in combination with a LIMIT and GROUP BY clause.
|
||||
@ -439,6 +480,20 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertEquals('2012-05-01 09:00:00', $records['0']['_SortColumn0']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test passing in a LIMIT with OFFSET clause string.
|
||||
*/
|
||||
public function testLimitSetFromClauseString() {
|
||||
$query = new SQLQuery();
|
||||
$query->setSelect('*');
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
|
||||
$query->setLimit('20 OFFSET 10');
|
||||
$limit = $query->getLimit();
|
||||
$this->assertEquals(20, $limit['limit']);
|
||||
$this->assertEquals(10, $limit['start']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SQLQueryTest_DO extends DataObject implements TestOnly {
|
||||
|
@ -109,6 +109,49 @@ class GroupTest extends FunctionalTest {
|
||||
'Grandchild groups are removed');
|
||||
}
|
||||
|
||||
public function testValidatesPrivilegeLevelOfParent() {
|
||||
$nonAdminUser = $this->objFromFixture('GroupTest_Member', 'childgroupuser');
|
||||
$adminUser = $this->objFromFixture('GroupTest_Member', 'admin');
|
||||
$nonAdminGroup = $this->objFromFixture('Group', 'childgroup');
|
||||
$adminGroup = $this->objFromFixture('Group', 'admingroup');
|
||||
|
||||
$nonAdminValidateMethod = new ReflectionMethod($nonAdminGroup, 'validate');
|
||||
$nonAdminValidateMethod->setAccessible(true);
|
||||
|
||||
// Making admin group parent of a non-admin group, effectively expanding is privileges
|
||||
$nonAdminGroup->ParentID = $adminGroup->ID;
|
||||
|
||||
$this->logInWithPermission('APPLY_ROLES');
|
||||
$result = $nonAdminValidateMethod->invoke($nonAdminGroup);
|
||||
$this->assertFalse(
|
||||
$result->valid(),
|
||||
'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions'
|
||||
);
|
||||
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$result = $nonAdminValidateMethod->invoke($nonAdminGroup);
|
||||
$this->assertTrue(
|
||||
$result->valid(),
|
||||
'Members with ADMIN can assign parent groups with direct ADMIN permissions'
|
||||
);
|
||||
$nonAdminGroup->write();
|
||||
$newlyAdminGroup = $nonAdminGroup;
|
||||
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$inheritedAdminGroup = $this->objFromFixture('Group', 'group1');
|
||||
$inheritedAdminMethod = new ReflectionMethod($inheritedAdminGroup, 'validate');
|
||||
$inheritedAdminMethod->setAccessible(true);
|
||||
$inheritedAdminGroup->ParentID = $adminGroup->ID;
|
||||
$inheritedAdminGroup->write(); // only works with ADMIN login
|
||||
|
||||
$this->logInWithPermission('APPLY_ROLES');
|
||||
$result = $inheritedAdminMethod->invoke($nonAdminGroup);
|
||||
$this->assertFalse(
|
||||
$result->valid(),
|
||||
'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GroupTest_Member extends Member implements TestOnly {
|
||||
|
@ -16,4 +16,35 @@ class PermissionRoleTest extends FunctionalTest {
|
||||
$this->assertEquals(0, DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}")->count(),
|
||||
'Permissions removed along with the role');
|
||||
}
|
||||
|
||||
public function testValidatesPrivilegedPermissions() {
|
||||
$nonAdminCode = new PermissionRoleCode(array('Code' => 'CMS_ACCESS_CMSMain'));
|
||||
$nonAdminValidateMethod = new ReflectionMethod($nonAdminCode, 'validate');
|
||||
$nonAdminValidateMethod->setAccessible(true);
|
||||
|
||||
$adminCode = new PermissionRoleCode(array('Code' => 'ADMIN'));
|
||||
$adminValidateMethod = new ReflectionMethod($adminCode, 'validate');
|
||||
$adminValidateMethod->setAccessible(true);
|
||||
|
||||
$this->logInWithPermission('APPLY_ROLES');
|
||||
$result = $nonAdminValidateMethod->invoke($nonAdminCode);
|
||||
$this->assertTrue(
|
||||
$result->valid(),
|
||||
'Members with only APPLY_ROLES can create non-privileged permission role codes'
|
||||
);
|
||||
|
||||
$this->logInWithPermission('APPLY_ROLES');
|
||||
$result = $adminValidateMethod->invoke($adminCode);
|
||||
$this->assertFalse(
|
||||
$result->valid(),
|
||||
'Members with only APPLY_ROLES can\'t create privileged permission role codes'
|
||||
);
|
||||
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$result = $adminValidateMethod->invoke($adminCode);
|
||||
$this->assertTrue(
|
||||
$result->valid(),
|
||||
'Members with ADMIN can create privileged permission role codes'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
138
thirdparty/jquery-entwine/src/domevents/jquery.entwine.domevents.addrem.js
vendored
Executable file
138
thirdparty/jquery-entwine/src/domevents/jquery.entwine.domevents.addrem.js
vendored
Executable file
@ -0,0 +1,138 @@
|
||||
(function($){
|
||||
|
||||
// Gets all the child elements of a particular elements, stores it in an array
|
||||
function getElements(store, original) {
|
||||
var node, i = store.length, next = original.firstChild;
|
||||
|
||||
while ((node = next)) {
|
||||
if (node.nodeType === 1) store[i++] = node;
|
||||
next = node.firstChild || node.nextSibling;
|
||||
while (!next && (node = node.parentNode) && node !== original) next = node.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
// This might be faster? Or slower? @todo: benchmark.
|
||||
function getElementsAlt(store, node) {
|
||||
if (node.getElementsByTagName) {
|
||||
var els = node.getElementsByTagName('*'), len = els.length, i = 0, j = store.length;
|
||||
for(; i < len; i++, j++) {
|
||||
store[j] = els[i];
|
||||
}
|
||||
}
|
||||
else if (node.childNodes) {
|
||||
var els = node.childNodes, len = els.length, i = 0;
|
||||
for(; i < len; i++) {
|
||||
getElements(store, els[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dontTrigger = false;
|
||||
|
||||
var patchDomManipCallback = function(original) {
|
||||
var patched = function(elem){
|
||||
var added = [];
|
||||
|
||||
if (!dontTrigger) {
|
||||
if (elem.nodeType == 1) added[added.length] = elem;
|
||||
getElements(added, elem);
|
||||
}
|
||||
|
||||
var rv = original.apply(this, arguments);
|
||||
|
||||
if (!dontTrigger && added.length) {
|
||||
var event = $.Event('EntwineElementsAdded');
|
||||
event.targets = added;
|
||||
$(document).triggerHandler(event);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
patched.patched = true;
|
||||
|
||||
return patched;
|
||||
}
|
||||
|
||||
var version = $.prototype.jquery.split('.');
|
||||
var callbackIdx = (version[0] > 1 || version[1] >= 10 ? 1 : 2);
|
||||
|
||||
// Monkey patch $.fn.domManip to catch all regular jQuery add element calls
|
||||
var _domManip = $.prototype.domManip;
|
||||
$.prototype.domManip = function() {
|
||||
if (!arguments[callbackIdx].patched) arguments[callbackIdx] = patchDomManipCallback(arguments[callbackIdx]);
|
||||
return _domManip.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Monkey patch $.fn.html to catch when jQuery sets innerHTML directly
|
||||
var _html = $.prototype.html;
|
||||
$.prototype.html = function(value) {
|
||||
if (value === undefined) return _html.apply(this, arguments);
|
||||
|
||||
dontTrigger = true;
|
||||
var res = _html.apply(this, arguments);
|
||||
dontTrigger = false;
|
||||
|
||||
var added = [];
|
||||
|
||||
var i = 0, length = this.length;
|
||||
for (; i < length; i++ ) getElements(added, this[i]);
|
||||
|
||||
var event = $.Event('EntwineElementsAdded');
|
||||
event.targets = added;
|
||||
$(document).triggerHandler(event);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// If this is true, we've changed something to call cleanData so that we can catch the elements, but we don't
|
||||
// want to call the underlying original $.cleanData
|
||||
var supressActualClean = false;
|
||||
|
||||
// Monkey patch $.cleanData to catch element removal
|
||||
var _cleanData = $.cleanData;
|
||||
$.cleanData = function( elems ) {
|
||||
// By default we can assume all elements passed are legitimately being removeed
|
||||
var removed = elems;
|
||||
|
||||
// Except if we're supressing actual clean - we might be being called by jQuery "being careful" about detaching nodes
|
||||
// before attaching them. So we need to check to make sure these nodes currently are in a document
|
||||
if (supressActualClean) {
|
||||
var i = 0, len = elems.length, removed = [], ri = 0;
|
||||
for(; i < len; i++) {
|
||||
var node = elems[i], current = node;
|
||||
while (current = current.parentNode) {
|
||||
if (current.nodeType == 9) { removed[ri++] = node; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed.length) {
|
||||
var event = $.Event('EntwineElementsRemoved');
|
||||
event.targets = removed;
|
||||
$(document).triggerHandler(event);
|
||||
}
|
||||
|
||||
if (!supressActualClean) _cleanData.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Monkey patch $.fn.remove to catch when we're just detaching (keepdata == 1) -
|
||||
// this doesn't call cleanData but still needs to trigger event
|
||||
var _remove = $.prototype.remove;
|
||||
$.prototype.remove = function(selector, keepdata) {
|
||||
supressActualClean = keepdata;
|
||||
var rv = _remove.call(this, selector);
|
||||
supressActualClean = false;
|
||||
return rv;
|
||||
}
|
||||
|
||||
// And on DOM ready, trigger adding once
|
||||
$(function(){
|
||||
var added = []; getElements(added, document);
|
||||
|
||||
var event = $.Event('EntwineElementsAdded');
|
||||
event.targets = added;
|
||||
$(document).triggerHandler(event);
|
||||
});
|
||||
|
||||
|
||||
})(jQuery);
|
149
thirdparty/jquery-entwine/src/domevents/jquery.entwine.domevents.maybechanged.js
vendored
Executable file
149
thirdparty/jquery-entwine/src/domevents/jquery.entwine.domevents.maybechanged.js
vendored
Executable file
@ -0,0 +1,149 @@
|
||||
(function($){
|
||||
|
||||
/** Utility function to monkey-patch a jQuery method */
|
||||
var monkey = function( /* method, method, ...., patch */){
|
||||
var methods = $.makeArray(arguments);
|
||||
var patch = methods.pop();
|
||||
|
||||
$.each(methods, function(i, method){
|
||||
var old = $.fn[method];
|
||||
|
||||
$.fn[method] = function() {
|
||||
var self = this, args = $.makeArray(arguments);
|
||||
|
||||
var rv = old.apply(self, args);
|
||||
patch.apply(self, args);
|
||||
return rv;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** What to call to run a function 'soon'. Normally setTimeout, but for syncronous mode we override so soon === now */
|
||||
var runSoon = window.setTimeout;
|
||||
|
||||
/** The timer handle for the asyncronous matching call */
|
||||
var ChangeDetails = Base.extend({
|
||||
|
||||
init: function() {
|
||||
this.global = false;
|
||||
this.attrs = {};
|
||||
this.classes = {};
|
||||
},
|
||||
|
||||
/** Fire the change event. Only fires on the document node, so bind to that */
|
||||
triggerEvent: function() {
|
||||
// If we're not the active changes instance any more, don't trigger
|
||||
if (changes != this) return;
|
||||
|
||||
// Cancel any pending timeout (if we're directly called in the mean time)
|
||||
if (this.check_id) clearTimeout(this.check_id);
|
||||
|
||||
// Reset the global changes object to be a new instance (do before trigger, in case trigger fires changes itself)
|
||||
changes = new ChangeDetails();
|
||||
|
||||
// Fire event
|
||||
$(document).triggerHandler("EntwineSubtreeMaybeChanged", [this]);
|
||||
},
|
||||
|
||||
changed: function() {
|
||||
if (!this.check_id) {
|
||||
var self = this;
|
||||
this.check_id = runSoon(function(){ self.check_id = null; self.triggerEvent(); }, 10);
|
||||
}
|
||||
},
|
||||
|
||||
addAll: function() {
|
||||
if (this.global) return this; // If we've already flagged as a global change, just skip
|
||||
|
||||
this.global = true;
|
||||
this.changed();
|
||||
return this;
|
||||
},
|
||||
|
||||
addSubtree: function(node) {
|
||||
return this.addAll();
|
||||
},
|
||||
|
||||
/* For now we don't do this. It's expensive, and jquery.entwine.ctors doesn't use this information anyway */
|
||||
addSubtreeFuture: function(node) {
|
||||
if (this.global) return this; // If we've already flagged as a global change, just skip
|
||||
|
||||
this.subtree = this.subtree ? this.subtree.add(node) : $(node);
|
||||
this.changed();
|
||||
return this;
|
||||
},
|
||||
|
||||
addAttr: function(attr, node) {
|
||||
if (this.global) return this;
|
||||
|
||||
this.attrs[attr] = (attr in this.attrs) ? this.attrs[attr].add(node) : $(node);
|
||||
this.changed();
|
||||
return this;
|
||||
},
|
||||
|
||||
addClass: function(klass, node) {
|
||||
if (this.global) return this;
|
||||
|
||||
this.classes[klass] = (klass in this.classes) ? this.classes[klass].add(node) : $(node);
|
||||
this.changed();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var changes = new ChangeDetails();
|
||||
|
||||
// Element add events trigger maybechanged events
|
||||
|
||||
$(document).bind('EntwineElementsAdded', function(e){ changes.addSubtree(e.targets); });
|
||||
|
||||
// Element remove events trigger maybechanged events, but we have to wait until after the nodes are actually removed
|
||||
// (EntwineElementsRemoved fires _just before_ the elements are removed so the data still exists), especially in syncronous mode
|
||||
|
||||
var removed = null;
|
||||
$(document).bind('EntwineElementsRemoved', function(e){ removed = e.targets; });
|
||||
|
||||
monkey('remove', 'html', 'empty', function(){
|
||||
var subtree = removed; removed = null;
|
||||
if (subtree) changes.addSubtree(subtree);
|
||||
});
|
||||
|
||||
// We also need to know when an attribute, class, etc changes. Patch the relevant jQuery methods here
|
||||
|
||||
monkey('removeAttr', function(attr){
|
||||
changes.addAttr(attr, this);
|
||||
});
|
||||
|
||||
monkey('addClass', 'removeClass', 'toggleClass', function(klass){
|
||||
if (typeof klass == 'string') changes.addClass(klass, this);
|
||||
});
|
||||
|
||||
monkey('attr', function(a, b){
|
||||
if (b !== undefined && typeof a == 'string') changes.addAttr(a, this);
|
||||
else if (typeof a != 'string') { for (var k in a) changes.addAttr(k, this); }
|
||||
});
|
||||
|
||||
// Add some usefull accessors to $.entwine
|
||||
|
||||
$.extend($.entwine, {
|
||||
/**
|
||||
* Make onmatch and onunmatch work in synchronous mode - that is, new elements will be detected immediately after
|
||||
* the DOM manipulation that made them match. This is only really useful for during testing, since it's pretty slow
|
||||
* (otherwise we'd make it the default).
|
||||
*/
|
||||
synchronous_mode: function() {
|
||||
if (changes && changes.check_id) clearTimeout(changes.check_id);
|
||||
changes = new ChangeDetails();
|
||||
|
||||
runSoon = function(func, delay){ func.call(this); return null; };
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger onmatch and onunmatch now - usefull for after DOM manipulation by methods other than through jQuery.
|
||||
* Called automatically on document.ready
|
||||
*/
|
||||
triggerMatching: function() {
|
||||
changes.addAll();
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
63
thirdparty/jquery-entwine/src/jquery.entwine.addrem.js
vendored
Executable file
63
thirdparty/jquery-entwine/src/jquery.entwine.addrem.js
vendored
Executable file
@ -0,0 +1,63 @@
|
||||
(function($) {
|
||||
|
||||
$.entwine.Namespace.addMethods({
|
||||
build_addrem_proxy: function(name) {
|
||||
var one = this.one(name, 'func');
|
||||
|
||||
return function() {
|
||||
if (this.length === 0){
|
||||
return;
|
||||
}
|
||||
else if (this.length) {
|
||||
var rv, i = this.length;
|
||||
while (i--) rv = one(this[i], arguments);
|
||||
return rv;
|
||||
}
|
||||
else {
|
||||
return one(this, arguments);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
bind_addrem_proxy: function(selector, name, func) {
|
||||
var rulelist = this.store[name] || (this.store[name] = $.entwine.RuleList());
|
||||
|
||||
var rule = rulelist.addRule(selector, name); rule.func = func;
|
||||
|
||||
if (!this.injectee.hasOwnProperty(name)) {
|
||||
this.injectee[name] = this.build_addrem_proxy(name);
|
||||
this.injectee[name].isentwinemethod = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 30,
|
||||
|
||||
bind: function(selector, k, v) {
|
||||
if ($.isFunction(v) && (k == 'onadd' || k == 'onremove')) {
|
||||
this.bind_addrem_proxy(selector, k, v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).bind('EntwineElementsAdded', function(e){
|
||||
// For every namespace
|
||||
for (var k in $.entwine.namespaces) {
|
||||
var namespace = $.entwine.namespaces[k];
|
||||
if (namespace.injectee.onadd) namespace.injectee.onadd.call(e.targets);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).bind('EntwineElementsRemoved', function(e){
|
||||
for (var k in $.entwine.namespaces) {
|
||||
var namespace = $.entwine.namespaces[k];
|
||||
if (namespace.injectee.onremove) namespace.injectee.onremove.call(e.targets);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
})(jQuery);
|
242
thirdparty/jquery-entwine/src/jquery.entwine.ctors.js
vendored
Executable file
242
thirdparty/jquery-entwine/src/jquery.entwine.ctors.js
vendored
Executable file
@ -0,0 +1,242 @@
|
||||
(function($) {
|
||||
|
||||
/* Add the methods to handle constructor & destructor binding to the Namespace class */
|
||||
$.entwine.Namespace.addMethods({
|
||||
bind_condesc: function(selector, name, func) {
|
||||
var ctors = this.store.ctors || (this.store.ctors = $.entwine.RuleList()) ;
|
||||
|
||||
var rule;
|
||||
for (var i = 0 ; i < ctors.length; i++) {
|
||||
if (ctors[i].selector.selector == selector.selector) {
|
||||
rule = ctors[i]; break;
|
||||
}
|
||||
}
|
||||
if (!rule) {
|
||||
rule = ctors.addRule(selector, 'ctors');
|
||||
}
|
||||
|
||||
rule[name] = func;
|
||||
|
||||
if (!ctors[name+'proxy']) {
|
||||
var one = this.one('ctors', name);
|
||||
var namespace = this;
|
||||
|
||||
var proxy = function(els, i, func) {
|
||||
var j = els.length;
|
||||
while (j--) {
|
||||
var el = els[j];
|
||||
|
||||
var tmp_i = el.i, tmp_f = el.f;
|
||||
el.i = i; el.f = one;
|
||||
|
||||
try { func.call(namespace.$(el)); }
|
||||
catch(e) { $.entwine.warn_exception(name, el, e); }
|
||||
finally { el.i = tmp_i; el.f = tmp_f; }
|
||||
}
|
||||
};
|
||||
|
||||
ctors[name+'proxy'] = proxy;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 30,
|
||||
|
||||
bind: function(selector, k, v) {
|
||||
if ($.isFunction(v) && (k == 'onmatch' || k == 'onunmatch')) {
|
||||
// When we add new matchers we need to trigger a full global recalc once, regardless of the DOM changes that triggered the event
|
||||
this.matchersDirty = true;
|
||||
|
||||
this.bind_condesc(selector, k, v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds all the elements that now match a different rule (or have been removed) and call onmatch on onunmatch as appropriate
|
||||
*
|
||||
* Because this has to scan the DOM, and is therefore fairly slow, this is normally triggered off a short timeout, so that
|
||||
* a series of DOM manipulations will only trigger this once.
|
||||
*
|
||||
* The downside of this is that things like:
|
||||
* $('#foo').addClass('tabs'); $('#foo').tabFunctionBar();
|
||||
* won't work.
|
||||
*/
|
||||
$(document).bind('EntwineSubtreeMaybeChanged', function(e, changes){
|
||||
// var start = (new Date).getTime();
|
||||
|
||||
// For every namespace
|
||||
for (var k in $.entwine.namespaces) {
|
||||
var namespace = $.entwine.namespaces[k];
|
||||
|
||||
// That has constructors or destructors
|
||||
var ctors = namespace.store.ctors;
|
||||
if (ctors) {
|
||||
|
||||
// Keep a record of elements that have matched some previous more specific rule.
|
||||
// Not that we _don't_ actually do that until this is needed. If matched is null, it's not been calculated yet.
|
||||
// We also keep track of any elements that have newly been taken or released by a specific rule
|
||||
var matched = null, taken = $([]), released = $([]);
|
||||
|
||||
// Updates matched to contain all the previously matched elements as if we'd been keeping track all along
|
||||
var calcmatched = function(j){
|
||||
if (matched !== null) return;
|
||||
matched = $([]);
|
||||
|
||||
var cache, k = ctors.length;
|
||||
while ((--k) > j) {
|
||||
if (cache = ctors[k].cache) matched = matched.add(cache);
|
||||
}
|
||||
}
|
||||
|
||||
// Some declared variables used in the loop
|
||||
var add, rem, res, rule, sel, ctor, dtor, full;
|
||||
|
||||
// Stepping through each selector from most to least specific
|
||||
var j = ctors.length;
|
||||
while (j--) {
|
||||
// Build some quick-access variables
|
||||
rule = ctors[j];
|
||||
sel = rule.selector.selector;
|
||||
ctor = rule.onmatch;
|
||||
dtor = rule.onunmatch;
|
||||
|
||||
/*
|
||||
Rule.cache might be stale or fresh. It'll be stale if
|
||||
- some more specific selector now has some of rule.cache in it
|
||||
- some change has happened that means new elements match this selector now
|
||||
- some change has happened that means elements no longer match this selector
|
||||
|
||||
The first we can just compare rules.cache with matched, removing anything that's there already.
|
||||
*/
|
||||
|
||||
// Reset the "elements that match this selector and no more specific selector with an onmatch rule" to null.
|
||||
// Staying null means this selector is fresh.
|
||||
res = null;
|
||||
|
||||
// If this gets changed to true, it's too hard to do a delta update, so do a full update
|
||||
full = false;
|
||||
|
||||
if (namespace.matchersDirty || changes.global) {
|
||||
// For now, just fall back to old version. We need to do something like changed.Subtree.find('*').andSelf().filter(sel), but that's _way_ slower on modern browsers than the below
|
||||
full = true;
|
||||
}
|
||||
else {
|
||||
// We don't deal with attributes yet, so any attribute change means we need to do a full recalc
|
||||
for (var k in changes.attrs) { full = true; break; }
|
||||
|
||||
/*
|
||||
If a class changes, but it isn't listed in our selector, we don't care - the change couldn't affect whether or not any element matches
|
||||
|
||||
If it is listed on our selector
|
||||
- If it is on the direct match part, it could have added or removed the node it changed on
|
||||
- If it is on the context part, it could have added or removed any node that were previously included or excluded because of a match or failure to match with the context required on that node
|
||||
- NOTE: It might be on _both_
|
||||
*/
|
||||
|
||||
var method = rule.selector.affectedBy(changes);
|
||||
|
||||
if (method.classes.context) {
|
||||
full = true;
|
||||
}
|
||||
else {
|
||||
for (var k in method.classes.direct) {
|
||||
calcmatched(j);
|
||||
var recheck = changes.classes[k].not(matched);
|
||||
|
||||
if (res === null) {
|
||||
res = rule.cache ? rule.cache.not(taken).add(released.filter(sel)) : $([]);
|
||||
}
|
||||
|
||||
res = res.not(recheck).add(recheck.filter(sel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (full) {
|
||||
calcmatched(j);
|
||||
res = $(sel).not(matched);
|
||||
}
|
||||
else {
|
||||
if (!res) {
|
||||
// We weren't stale because of any changes to the DOM that affected this selector, but more specific
|
||||
// onmatches might have caused stale-ness
|
||||
|
||||
// Do any of the previous released elements match this selector?
|
||||
add = released.length && released.filter(sel);
|
||||
|
||||
if (add && add.length) {
|
||||
// Yes, so we're stale as we need to include them. Filter for any possible taken value at the same time
|
||||
res = rule.cache ? rule.cache.not(taken).add(add) : add;
|
||||
}
|
||||
else {
|
||||
// Do we think we own any of the elements now taken by more specific rules?
|
||||
rem = taken.length && rule.cache && rule.cache.filter(taken);
|
||||
|
||||
if (rem && rem.length) {
|
||||
// Yes, so we're stale as we need to exclude them.
|
||||
res = rule.cache.not(rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Res will be null if we know we are fresh (no full needed, selector not affectedBy changes)
|
||||
if (res === null) {
|
||||
// If we are tracking matched, add ourselves
|
||||
if (matched && rule.cache) matched = matched.add(rule.cache);
|
||||
}
|
||||
else {
|
||||
// If this selector has a list of elements it matched against last time
|
||||
if (rule.cache) {
|
||||
// Find the ones that are extra this time
|
||||
add = res.not(rule.cache);
|
||||
rem = rule.cache.not(res);
|
||||
}
|
||||
else {
|
||||
add = res; rem = null;
|
||||
}
|
||||
|
||||
if ((add && add.length) || (rem && rem.length)) {
|
||||
if (rem && rem.length) {
|
||||
released = released.add(rem);
|
||||
|
||||
if (dtor && !rule.onunmatchRunning) {
|
||||
rule.onunmatchRunning = true;
|
||||
ctors.onunmatchproxy(rem, j, dtor);
|
||||
rule.onunmatchRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the constructor on the newly matched ones
|
||||
if (add && add.length) {
|
||||
taken = taken.add(add);
|
||||
released = released.not(add);
|
||||
|
||||
if (ctor && !rule.onmatchRunning) {
|
||||
rule.onmatchRunning = true;
|
||||
ctors.onmatchproxy(add, j, ctor);
|
||||
rule.onmatchRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are tracking matched, add ourselves
|
||||
if (matched) matched = matched.add(res);
|
||||
|
||||
// And remember this list of matching elements again this selector, so next matching we can find the unmatched ones
|
||||
rule.cache = res;
|
||||
}
|
||||
}
|
||||
|
||||
namespace.matchersDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log((new Date).getTime() - start);
|
||||
});
|
||||
|
||||
|
||||
})(jQuery);
|
118
thirdparty/jquery-entwine/src/jquery.entwine.eventcapture.js
vendored
Executable file
118
thirdparty/jquery-entwine/src/jquery.entwine.eventcapture.js
vendored
Executable file
@ -0,0 +1,118 @@
|
||||
(function($) {
|
||||
|
||||
$.entwine.Namespace.addMethods({
|
||||
bind_capture: function(selector, event, name, capture) {
|
||||
var store = this.captures || (this.captures = {});
|
||||
var rulelists = store[event] || (store[event] = {});
|
||||
var rulelist = rulelists[name] || (rulelists[name] = $.entwine.RuleList());
|
||||
|
||||
rule = rulelist.addRule(selector, event);
|
||||
rule.handler = name;
|
||||
|
||||
this.bind_proxy(selector, name, capture);
|
||||
}
|
||||
});
|
||||
|
||||
var bindings = $.entwine.capture_bindings = {};
|
||||
|
||||
var event_proxy = function(event) {
|
||||
return function(e) {
|
||||
var namespace, capturelists, forevent, capturelist, rule, handler, sel;
|
||||
|
||||
for (var k in $.entwine.namespaces) {
|
||||
namespace = $.entwine.namespaces[k];
|
||||
capturelists = namespace.captures;
|
||||
|
||||
if (capturelists && (forevent = capturelists[event])) {
|
||||
for (var k in forevent) {
|
||||
var capturelist = forevent[k];
|
||||
var triggered = namespace.$([]);
|
||||
|
||||
// Stepping through each selector from most to least specific
|
||||
var j = capturelist.length;
|
||||
while (j--) {
|
||||
rule = capturelist[j];
|
||||
handler = rule.handler;
|
||||
sel = rule.selector.selector;
|
||||
|
||||
var matching = namespace.$(sel).not(triggered);
|
||||
matching[handler].apply(matching, arguments);
|
||||
|
||||
triggered = triggered.add(matching);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var selector_proxy = function(selector, handler, includechildren) {
|
||||
var matcher = $.selector(selector);
|
||||
return function(e){
|
||||
if (matcher.matches(e.target)) return handler.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
var document_proxy = function(selector, handler, includechildren) {
|
||||
return function(e){
|
||||
if (e.target === document) return handler.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
var window_proxy = function(selector, handler, includechildren) {
|
||||
return function(e){
|
||||
if (e.target === window) return handler.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
var property_proxy = function(property, handler, includechildren) {
|
||||
var matcher;
|
||||
|
||||
return function(e){
|
||||
var match = this['get'+property]();
|
||||
|
||||
if (typeof(match) == 'string') {
|
||||
var matcher = (matcher && match == matcher.selector) ? matcher : $.selector(match);
|
||||
if (matcher.matches(e.target)) return handler.apply(this, arguments);
|
||||
}
|
||||
else {
|
||||
if ($.inArray(e.target, match) !== -1) return handler.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 10,
|
||||
|
||||
bind: function(selector, k, v) {
|
||||
var match;
|
||||
if ($.isPlainObject(v) && (match = k.match(/^from\s*(.*)/))) {
|
||||
var from = match[1];
|
||||
var proxyGen;
|
||||
|
||||
if (from.match(/[^\w]/)) proxyGen = selector_proxy;
|
||||
else if (from == 'Window' || from == 'window') proxyGen = window_proxy;
|
||||
else if (from == 'Document' || from == 'document') proxyGen = document_proxy;
|
||||
else proxyGen = property_proxy;
|
||||
|
||||
for (var onevent in v) {
|
||||
var handler = v[onevent];
|
||||
match = onevent.match(/^on(.*)/);
|
||||
var event = match[1];
|
||||
|
||||
this.bind_capture(selector, event, k + '_' + event, proxyGen(from, handler));
|
||||
|
||||
if (!bindings[event]) {
|
||||
var namespaced = event.replace(/(\s+|$)/g, '.entwine$1');
|
||||
bindings[event] = event_proxy(event);
|
||||
|
||||
$(proxyGen == window_proxy ? window : document).bind(namespaced, bindings[event]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
249
thirdparty/jquery-entwine/src/jquery.entwine.events.js
vendored
Executable file
249
thirdparty/jquery-entwine/src/jquery.entwine.events.js
vendored
Executable file
@ -0,0 +1,249 @@
|
||||
(function($) {
|
||||
|
||||
/** Taken from jQuery 1.5.2 for backwards compatibility */
|
||||
if ($.support.changeBubbles == undefined) {
|
||||
$.support.changeBubbles = true;
|
||||
|
||||
var el = document.createElement("div");
|
||||
eventName = "onchange";
|
||||
|
||||
if (el.attachEvent) {
|
||||
var isSupported = (eventName in el);
|
||||
if (!isSupported) {
|
||||
el.setAttribute(eventName, "return;");
|
||||
isSupported = typeof el[eventName] === "function";
|
||||
}
|
||||
|
||||
$.support.changeBubbles = isSupported;
|
||||
}
|
||||
}
|
||||
|
||||
/* Return true if node b is the same as, or is a descendant of, node a */
|
||||
if (document.compareDocumentPosition) {
|
||||
var is_or_contains = function(a, b) {
|
||||
return a && b && (a == b || !!(a.compareDocumentPosition(b) & 16));
|
||||
};
|
||||
}
|
||||
else {
|
||||
var is_or_contains = function(a, b) {
|
||||
return a && b && (a == b || (a.contains ? a.contains(b) : true));
|
||||
};
|
||||
}
|
||||
|
||||
/* Add the methods to handle event binding to the Namespace class */
|
||||
$.entwine.Namespace.addMethods({
|
||||
build_event_proxy: function(name) {
|
||||
var one = this.one(name, 'func');
|
||||
|
||||
var prxy = function(e, data) {
|
||||
// For events that do not bubble we manually trigger delegation (see delegate_submit below)
|
||||
// If this event is a manual trigger, the event we actually want to bubble is attached as a property of the passed event
|
||||
e = e.delegatedEvent || e;
|
||||
|
||||
var el = e.target;
|
||||
while (el && el.nodeType == 1 && !e.isPropagationStopped()) {
|
||||
var ret = one(el, arguments);
|
||||
if (ret !== undefined) e.result = ret;
|
||||
if (ret === false) { e.preventDefault(); e.stopPropagation(); }
|
||||
|
||||
el = el.parentNode;
|
||||
}
|
||||
};
|
||||
|
||||
return prxy;
|
||||
},
|
||||
|
||||
build_mouseenterleave_proxy: function(name) {
|
||||
var one = this.one(name, 'func');
|
||||
|
||||
var prxy = function(e) {
|
||||
var el = e.target;
|
||||
var rel = e.relatedTarget;
|
||||
|
||||
while (el && el.nodeType == 1 && !e.isPropagationStopped()) {
|
||||
/* We know el contained target. If it also contains relatedTarget then we didn't mouseenter / leave. What's more, every ancestor will also
|
||||
contan el and rel, and so we can just stop bubbling */
|
||||
if (is_or_contains(el, rel)) break;
|
||||
|
||||
var ret = one(el, arguments);
|
||||
if (ret !== undefined) e.result = ret;
|
||||
if (ret === false) { e.preventDefault(); e.stopPropagation(); }
|
||||
|
||||
el = el.parentNode;
|
||||
}
|
||||
};
|
||||
|
||||
return prxy;
|
||||
},
|
||||
|
||||
build_change_proxy: function(name) {
|
||||
var one = this.one(name, 'func');
|
||||
|
||||
/*
|
||||
This change bubble emulation code is taken mostly from jQuery 1.6 - unfortunately we can't easily reuse any of
|
||||
it without duplication, so we'll have to re-migrate any bugfixes
|
||||
*/
|
||||
|
||||
// Get the value of an item. Isn't supposed to be interpretable, just stable for some value, and different
|
||||
// once the value changes
|
||||
var getVal = function( elem ) {
|
||||
var type = elem.type, val = elem.value;
|
||||
|
||||
if (type === "radio" || type === "checkbox") {
|
||||
val = elem.checked;
|
||||
}
|
||||
else if (type === "select-multiple") {
|
||||
val = "";
|
||||
if (elem.selectedIndex > -1) {
|
||||
val = jQuery.map(elem.options, function(elem){ return elem.selected; }).join("-");
|
||||
}
|
||||
}
|
||||
else if (jQuery.nodeName(elem, "select")) {
|
||||
val = elem.selectedIndex;
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
// Test if a node name is a form input
|
||||
var rformElems = /^(?:textarea|input|select)$/i;
|
||||
|
||||
// Check if this event is a change, and bubble the change event if it is
|
||||
var testChange = function(e) {
|
||||
var elem = e.target, data, val;
|
||||
|
||||
if (!rformElems.test(elem.nodeName) || elem.readOnly) return;
|
||||
|
||||
data = jQuery.data(elem, "_entwine_change_data");
|
||||
val = getVal(elem);
|
||||
|
||||
// the current data will be also retrieved by beforeactivate
|
||||
if (e.type !== "focusout" || elem.type !== "radio") {
|
||||
jQuery.data(elem, "_entwine_change_data", val);
|
||||
}
|
||||
|
||||
if (data === undefined || val === data) return;
|
||||
|
||||
if (data != null || val) {
|
||||
e.type = "change";
|
||||
|
||||
while (elem && elem.nodeType == 1 && !e.isPropagationStopped()) {
|
||||
var ret = one(elem, arguments);
|
||||
if (ret !== undefined) e.result = ret;
|
||||
if (ret === false) { e.preventDefault(); e.stopPropagation(); }
|
||||
|
||||
elem = elem.parentNode;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The actual proxy - responds to several events, some of which triger a change check, some
|
||||
// of which just store the value for future change checks
|
||||
var prxy = function(e) {
|
||||
var event = e.type, elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : "";
|
||||
|
||||
switch (event) {
|
||||
case 'focusout':
|
||||
case 'beforedeactivate':
|
||||
testChange.apply(this, arguments);
|
||||
break;
|
||||
|
||||
case 'click':
|
||||
if ( type === "radio" || type === "checkbox" || jQuery.nodeName( elem, "select" ) ) {
|
||||
testChange.apply(this, arguments);
|
||||
}
|
||||
break;
|
||||
|
||||
// Change has to be called before submit
|
||||
// Keydown will be called before keypress, which is used in submit-event delegation
|
||||
case 'keydown':
|
||||
if (
|
||||
(e.keyCode === 13 && !jQuery.nodeName( elem, "textarea" ) ) ||
|
||||
(e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
|
||||
type === "select-multiple"
|
||||
) {
|
||||
testChange.apply(this, arguments);
|
||||
}
|
||||
break;
|
||||
|
||||
// Beforeactivate happens also before the previous element is blurred
|
||||
// with this event you can't trigger a change event, but you can store
|
||||
// information
|
||||
case 'focusin':
|
||||
case 'beforeactivate':
|
||||
jQuery.data( elem, "_entwine_change_data", getVal(elem) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return prxy;
|
||||
},
|
||||
|
||||
bind_event: function(selector, name, func, event) {
|
||||
var funcs = this.store[name] || (this.store[name] = $.entwine.RuleList()) ;
|
||||
var proxies = funcs.proxies || (funcs.proxies = {});
|
||||
|
||||
var rule = funcs.addRule(selector, name); rule.func = func;
|
||||
|
||||
if (!proxies[name]) {
|
||||
switch (name) {
|
||||
case 'onmouseenter':
|
||||
proxies[name] = this.build_mouseenterleave_proxy(name);
|
||||
event = 'mouseover';
|
||||
break;
|
||||
case 'onmouseleave':
|
||||
proxies[name] = this.build_mouseenterleave_proxy(name);
|
||||
event = 'mouseout';
|
||||
break;
|
||||
case 'onchange':
|
||||
if (!$.support.changeBubbles) {
|
||||
proxies[name] = this.build_change_proxy(name);
|
||||
event = 'click keydown focusin focusout beforeactivate beforedeactivate';
|
||||
}
|
||||
break;
|
||||
case 'onsubmit':
|
||||
event = 'delegatedSubmit';
|
||||
break;
|
||||
case 'onfocus':
|
||||
case 'onblur':
|
||||
$.entwine.warn('Event '+event+' not supported - using focusin / focusout instead', $.entwine.WARN_LEVEL_IMPORTANT);
|
||||
}
|
||||
|
||||
// If none of the special handlers created a proxy, use the generic proxy
|
||||
if (!proxies[name]) proxies[name] = this.build_event_proxy(name);
|
||||
|
||||
$(document).bind(event.replace(/(\s+|$)/g, '.entwine$1'), proxies[name]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 40,
|
||||
|
||||
bind: function(selector, k, v){
|
||||
var match, event;
|
||||
if ($.isFunction(v) && (match = k.match(/^on(.*)/))) {
|
||||
event = match[1];
|
||||
this.bind_event(selector, k, v, event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find all forms and bind onsubmit to trigger on the document too.
|
||||
// This is the only event that can't be grabbed via delegation
|
||||
|
||||
var delegate_submit = function(e, data){
|
||||
var delegationEvent = $.Event('delegatedSubmit'); delegationEvent.delegatedEvent = e;
|
||||
return $(document).trigger(delegationEvent, data);
|
||||
};
|
||||
|
||||
$(document).bind('EntwineElementsAdded', function(e){
|
||||
var forms = $(e.targets).filter('form');
|
||||
if (!forms.length) return;
|
||||
|
||||
forms.bind('submit.entwine_delegate_submit', delegate_submit);
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
|
240
thirdparty/jquery-entwine/src/jquery.entwine.inspector.js
vendored
Executable file
240
thirdparty/jquery-entwine/src/jquery.entwine.inspector.js
vendored
Executable file
@ -0,0 +1,240 @@
|
||||
|
||||
jQuery(function($){
|
||||
// Create a new style element
|
||||
var styleEl = document.createElement('style');
|
||||
styleEl.setAttribute('type', 'text/css');
|
||||
(document.head || document.getElementsByTagName('head')[0]).appendChild(styleEl);
|
||||
|
||||
var inspectorCSS = [
|
||||
'#entwine-inspector { position: fixed; z-index: 1000001; left: 0; right: 0; height: 400px; background: white; -webkit-box-shadow: 0 5px 40px 0 black; -moz-box-shadow: 0 5px 40px 0 black; }',
|
||||
'#entwine-inspector li { list-style: none; margin: 2px 0; padding: 2px 0; }',
|
||||
'#entwine-inspector li:hover { background: #eee; }',
|
||||
'#entwine-inspector li.selected { background: #ddd; }',
|
||||
|
||||
'#ei-columns { overflow: hidden; display: -webkit-box; display: -moz-box; width: 100%; height: 380px; }',
|
||||
|
||||
'.ei-column { height: 380px; width: 1px; -webkit-box-flex: 1; -moz-box-flex: 1; }',
|
||||
'#entwine-inspector .ei-column h1 { display: block; margin: 0; padding: 5px 2px; height: 20px; text-align: center; background: #444; color: #eee; font-size: 14px; font-weight: bold; }',
|
||||
'#entwine-inspector .ei-column ul { overflow-y: scroll; height: 350px; }',
|
||||
|
||||
'#ei-options { overflow: hidden; height: 20px; background: #444; color: #eee; }',
|
||||
'#ei-options label { padding-right: 5px; border-right: 1px solid #eee; }',
|
||||
|
||||
'.ei-entwined:hover, .ei-selected { background: rgba(128,0,0,0.2); }',
|
||||
'.ei-hovernode { position: absolute; z-index: 1000000; background: rgba(0,0,0,0.3); border: 1px solid white; outline: 1px solid white; }',
|
||||
|
||||
'#ei-selectors li { color: #aaa; display: none; }',
|
||||
'#ei-selectors li.matching, #entwine-inspector.show-unmatched #ei-selectors li { display: block; }',
|
||||
'#ei-selectors li.matching { color: black; }'
|
||||
].join("\n");
|
||||
|
||||
// Set the style element to style up the inspector panel
|
||||
if(styleEl.styleSheet){
|
||||
styleEl.styleSheet.cssText = inspectorCSS;
|
||||
}else{
|
||||
styleEl.appendChild(document.createTextNode(inspectorCSS));
|
||||
}
|
||||
|
||||
var inspectorPanel = $('<div id="entwine-inspector" class="show-unmatched"></div>').appendTo('body');
|
||||
var columnHolder = $('<div id="ei-columns"></div>').appendTo(inspectorPanel);
|
||||
var optionsHolder = $('<div id="ei-options"></div>').appendTo(inspectorPanel);
|
||||
|
||||
inspectorPanel.css({
|
||||
top: -400,
|
||||
visibility: 'hidden'
|
||||
});
|
||||
|
||||
$('body').bind('keypress', function(e){
|
||||
if (e.ctrlKey && e.which == 96) {
|
||||
if (inspectorPanel.css('visibility') != 'visible') {
|
||||
inspectorPanel.css({top: 0, visibility: 'visible'});
|
||||
$('body').css({marginTop: 400});
|
||||
initialise();
|
||||
}
|
||||
else {
|
||||
inspectorPanel.css({top: -400, visibility: 'hidden'});
|
||||
$('body').css({marginTop: 0});
|
||||
reset();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
var showUnmatching = $('<input id="ei-option-showunmatching" type="checkbox" checked="checked" />').appendTo(optionsHolder);
|
||||
var showUnmatchingLabel = $('<label>Show selectors that dont match</label>').appendTo(optionsHolder);
|
||||
|
||||
showUnmatching.bind('click', function(){
|
||||
inspectorPanel.toggleClass('show-unmatched', $(this).val());
|
||||
});
|
||||
|
||||
var hovernode;
|
||||
|
||||
var reset = function() {
|
||||
$('.ei-entwined').unbind('.entwine-inspector').removeClass('ei-entwined');
|
||||
if (hovernode) hovernode.remove();
|
||||
}
|
||||
|
||||
var initialise = function(){
|
||||
reset();
|
||||
|
||||
$.each($.entwine.namespaces, function(name, namespace){
|
||||
$.each(namespace.store, function(name, list){
|
||||
$.each(list, function(i, rule){
|
||||
var match = $(rule.selector.selector);
|
||||
match.addClass('ei-entwined').bind('click.entwine-inspector', displaydetails);
|
||||
})
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var dumpElement = function(el) {
|
||||
var frag = document.createDocumentFragment();
|
||||
var div = document.createElement('div'); frag.appendChild(div);
|
||||
|
||||
var clone = el.cloneNode(false); $(clone).removeClass('ei-entwined').removeAttr('style');
|
||||
|
||||
var i = clone.attributes.length;
|
||||
while (i--) {
|
||||
var attr = clone.attributes.item(i);
|
||||
if (attr.name != 'class' && attr.name != 'id' && attr.value.length > 20) attr.value = attr.value.substr(0, 18)+'..'+attr.value.substr(-2);
|
||||
}
|
||||
|
||||
div.appendChild(clone);
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
var displaydetails = function(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
|
||||
columnHolder.empty();
|
||||
|
||||
var columns = {};
|
||||
$.each(['elements', 'namespaces', 'methods', 'selectors'], function(i, col){
|
||||
columns[col] = $('<div id="ei-'+col+'" class="ei-column"><h1>'+col+'</h1></div>').appendTo(columnHolder);
|
||||
})
|
||||
|
||||
var lists = {};
|
||||
|
||||
var ctr = 0;
|
||||
|
||||
lists.elements = $('<ul></ul>').appendTo(columns.elements);
|
||||
|
||||
var displayelement = function(){
|
||||
var target = $(this);
|
||||
|
||||
var li = $('<li></li>');
|
||||
li.text(dumpElement(this)).attr('data-id', ++ctr).data('el', target).prependTo(lists.elements);
|
||||
|
||||
var namespaces = $('<ul data-element="'+ctr+'"></ul>').appendTo(columns.namespaces);
|
||||
|
||||
$.each($.entwine.namespaces, function(name, namespace){
|
||||
var methods = $('<ul data-namespace="'+ctr+'-'+name+'"></ul>');
|
||||
|
||||
$.each(namespace.store, function(method, list){
|
||||
|
||||
if (method == 'ctors') {
|
||||
var matchselectors = $('<ul data-method="'+ctr+'-'+name+'-onmatch"></ul>');
|
||||
var unmatchselectors = $('<ul data-method="'+ctr+'-'+name+'-onunmatch"></ul>');
|
||||
|
||||
$.each(list, function(i, rule){
|
||||
var matchitem = $('<li>'+rule.selector.selector+'</li>').prependTo(matchselectors);
|
||||
var unmatchitem = rule.onunmatch ? $('<li>'+rule.selector.selector+'</li>').prependTo(unmatchselectors) : null;
|
||||
|
||||
if (target.is(rule.selector.selector)) {
|
||||
matchitem.addClass('matching'); unmatchitem && unmatchitem.addClass('matching');
|
||||
|
||||
if (!methods.parent().length) {
|
||||
$('<li data-namespace="'+ctr+'-'+name+'">'+name+'</li>').prependTo(namespaces);
|
||||
methods.appendTo(columns.methods);
|
||||
}
|
||||
|
||||
if (!matchselectors.parent().length) {
|
||||
$('<li data-method="'+ctr+'-'+name+'-onmatch">onmatch</li>').prependTo(methods);
|
||||
matchselectors.appendTo(columns.selectors);
|
||||
}
|
||||
|
||||
if (rule.onunmatch && !unmatchselectors.parent().length) {
|
||||
$('<li data-method="'+ctr+'-'+name+'-onunmatch">onunmatch</li>').prependTo(methods);
|
||||
unmatchselectors.appendTo(columns.selectors);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
var selectors = $('<ul data-method="'+ctr+'-'+name+'-'+method+'"></ul>');
|
||||
|
||||
$.each(list, function(i, rule){
|
||||
var ruleitem = $('<li>'+rule.selector.selector+'</li>').prependTo(selectors);
|
||||
|
||||
if (target.is(rule.selector.selector)){
|
||||
ruleitem.addClass('matching');
|
||||
|
||||
if (!methods.parent().length) {
|
||||
$('<li data-namespace="'+ctr+'-'+name+'">'+name+'</li>').prependTo(namespaces);
|
||||
methods.appendTo(columns.methods);
|
||||
}
|
||||
|
||||
if (!selectors.parent().length) {
|
||||
$('<li data-method="'+ctr+'-'+name+'-'+method+'">'+method+'</li>').prependTo(methods);
|
||||
selectors.appendTo(columns.selectors);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$.each($(e.target).parents().andSelf().filter('.ei-entwined'), displayelement);
|
||||
$('#ei-elements > ul:first > li:first').click();
|
||||
}
|
||||
|
||||
var activatelist = function(list) {
|
||||
list = $(list);
|
||||
|
||||
list.siblings('ul').css('display', 'none');
|
||||
|
||||
list.css('display', 'block');
|
||||
list.children().first().click();
|
||||
}
|
||||
|
||||
$('#entwine-inspector').live('mouseleave', function(){
|
||||
if (hovernode) hovernode.hide();
|
||||
})
|
||||
|
||||
$('#entwine-inspector').live('mouseenter', function(){
|
||||
if (hovernode) hovernode.show();
|
||||
})
|
||||
|
||||
$('#ei-elements > ul > li').live('click', function(e){
|
||||
var target = $(e.target), id = target.attr('data-id');
|
||||
target.addClass('selected').siblings().removeClass('selected');
|
||||
|
||||
if (!hovernode) {
|
||||
hovernode = $('<div class="ei-hovernode"></div>').appendTo('body');
|
||||
}
|
||||
|
||||
var hover = target.data('el');
|
||||
hovernode.css({width: hover.outerWidth()-2, height: hover.outerHeight()-2, top: hover.offset().top, left: hover.offset().left});
|
||||
|
||||
$('.ei-selected').removeClass('ei-selected');
|
||||
|
||||
activatelist('#ei-namespaces ul[data-element="'+id+'"]');
|
||||
});
|
||||
|
||||
$('#ei-namespaces > ul > li').live('click', function(e){
|
||||
var target = $(e.target), namespace = target.attr('data-namespace');
|
||||
target.addClass('selected').siblings().removeClass('selected');
|
||||
|
||||
activatelist('#ei-methods ul[data-namespace="'+namespace+'"]');
|
||||
});
|
||||
|
||||
$('#ei-methods > ul > li').live('click', function(e){
|
||||
var target = $(e.target), method = target.attr('data-method');
|
||||
target.addClass('selected').siblings().removeClass('selected');
|
||||
|
||||
activatelist('#ei-selectors ul[data-method="'+method+'"]');
|
||||
});
|
||||
|
||||
});
|
354
thirdparty/jquery-entwine/src/jquery.entwine.js
vendored
Executable file
354
thirdparty/jquery-entwine/src/jquery.entwine.js
vendored
Executable file
@ -0,0 +1,354 @@
|
||||
try {
|
||||
console.log;
|
||||
}
|
||||
catch (e) {
|
||||
window.console = undefined;
|
||||
}
|
||||
|
||||
(function($) {
|
||||
|
||||
/* Create a subclass of the jQuery object. This was introduced in jQuery 1.5, but removed again in 1.9 */
|
||||
var sub = function() {
|
||||
function jQuerySub( selector, context ) {
|
||||
return new jQuerySub.fn.init( selector, context );
|
||||
}
|
||||
|
||||
jQuery.extend( true, jQuerySub, $ );
|
||||
jQuerySub.superclass = $;
|
||||
jQuerySub.fn = jQuerySub.prototype = $();
|
||||
jQuerySub.fn.constructor = jQuerySub;
|
||||
jQuerySub.fn.init = function init( selector, context ) {
|
||||
if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
|
||||
context = jQuerySub( context );
|
||||
}
|
||||
|
||||
return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
|
||||
};
|
||||
jQuerySub.fn.init.prototype = jQuerySub.fn;
|
||||
var rootjQuerySub = jQuerySub(document);
|
||||
return jQuerySub;
|
||||
};
|
||||
|
||||
var namespaces = {};
|
||||
|
||||
$.entwine = function() {
|
||||
$.fn.entwine.apply(null, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* A couple of utility functions for accessing the store outside of this closure, and for making things
|
||||
* operate in a little more easy-to-test manner
|
||||
*/
|
||||
$.extend($.entwine, {
|
||||
/**
|
||||
* Get all the namespaces. Useful for introspection? Internal interface of Namespace not guaranteed consistant
|
||||
*/
|
||||
namespaces: namespaces,
|
||||
|
||||
/**
|
||||
* Remove all entwine rules
|
||||
*/
|
||||
clear_all_rules: function() {
|
||||
// Remove proxy functions
|
||||
for (var k in $.fn) { if ($.fn[k].isentwinemethod) delete $.fn[k]; }
|
||||
// Remove bound events - TODO: Make this pluggable, so this code can be moved to jquery.entwine.events.js
|
||||
$(document).unbind('.entwine');
|
||||
$(window).unbind('.entwine');
|
||||
// Remove namespaces, and start over again
|
||||
for (var k in namespaces) delete namespaces[k];
|
||||
for (var k in $.entwine.capture_bindings) delete $.entwine.capture_bindings[k];
|
||||
},
|
||||
|
||||
WARN_LEVEL_NONE: 0,
|
||||
WARN_LEVEL_IMPORTANT: 1,
|
||||
WARN_LEVEL_BESTPRACTISE: 2,
|
||||
|
||||
/**
|
||||
* Warning level. Set to a higher level to get warnings dumped to console.
|
||||
*/
|
||||
warningLevel: 0,
|
||||
|
||||
/** Utility to optionally display warning messages depending on level */
|
||||
warn: function(message, level) {
|
||||
if (level <= $.entwine.warningLevel && console && console.warn) {
|
||||
console.warn(message);
|
||||
if (console.trace) console.trace();
|
||||
}
|
||||
},
|
||||
|
||||
warn_exception: function(where, /* optional: */ on, e) {
|
||||
if ($.entwine.WARN_LEVEL_IMPORTANT <= $.entwine.warningLevel && console && console.warn) {
|
||||
if (arguments.length == 2) { e = on; on = null; }
|
||||
|
||||
if (on) console.warn('Uncaught exception',e,'in',where,'on',on);
|
||||
else console.warn('Uncaught exception',e,'in',where);
|
||||
|
||||
if (e.stack) console.warn("Stack Trace:\n" + e.stack);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/** Stores a count of definitions, so that we can sort identical selectors by definition order */
|
||||
var rulecount = 0;
|
||||
|
||||
var Rule = Base.extend({
|
||||
init: function(selector, name) {
|
||||
this.selector = selector;
|
||||
this.specifity = selector.specifity();
|
||||
this.important = 0;
|
||||
this.name = name;
|
||||
this.rulecount = rulecount++;
|
||||
}
|
||||
});
|
||||
|
||||
Rule.compare = function(a, b) {
|
||||
var as = a.specifity, bs = b.specifity;
|
||||
|
||||
return (a.important - b.important) ||
|
||||
(as[0] - bs[0]) ||
|
||||
(as[1] - bs[1]) ||
|
||||
(as[2] - bs[2]) ||
|
||||
(a.rulecount - b.rulecount) ;
|
||||
};
|
||||
|
||||
$.entwine.RuleList = function() {
|
||||
var list = [];
|
||||
|
||||
list.addRule = function(selector, name){
|
||||
var rule = Rule(selector, name);
|
||||
|
||||
list[list.length] = rule;
|
||||
list.sort(Rule.compare);
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
var handlers = [];
|
||||
|
||||
/**
|
||||
* A Namespace holds all the information needed for adding entwine methods to a namespace (including the _null_ namespace)
|
||||
*/
|
||||
$.entwine.Namespace = Base.extend({
|
||||
init: function(name){
|
||||
if (name && !name.match(/^[A-Za-z0-9.]+$/)) $.entwine.warn('Entwine namespace '+name+' is not formatted as period seperated identifiers', $.entwine.WARN_LEVEL_BESTPRACTISE);
|
||||
name = name || '__base';
|
||||
|
||||
this.name = name;
|
||||
this.store = {};
|
||||
|
||||
namespaces[name] = this;
|
||||
|
||||
if (name == "__base") {
|
||||
this.injectee = $.fn;
|
||||
this.$ = $;
|
||||
}
|
||||
else {
|
||||
// We're in a namespace, so we build a Class that subclasses the jQuery Object Class to inject namespace functions into
|
||||
this.$ = $.sub ? $.sub() : sub();
|
||||
// Work around bug in sub() - subclass must share cache with root or data won't get cleared by cleanData
|
||||
this.$.cache = $.cache;
|
||||
|
||||
this.injectee = this.$.prototype;
|
||||
|
||||
// We override entwine to inject the name of this namespace when defining blocks inside this namespace
|
||||
var entwine_wrapper = this.injectee.entwine = function(spacename) {
|
||||
var args = arguments;
|
||||
|
||||
if (!spacename || typeof spacename != 'string') { args = $.makeArray(args); args.unshift(name); }
|
||||
else if (spacename.charAt(0) != '.') args[0] = name+'.'+spacename;
|
||||
|
||||
return $.fn.entwine.apply(this, args);
|
||||
};
|
||||
|
||||
this.$.entwine = function() {
|
||||
entwine_wrapper.apply(null, arguments);
|
||||
};
|
||||
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
var handler = handlers[i], builder;
|
||||
|
||||
// Inject jQuery object method overrides
|
||||
if (builder = handler.namespaceMethodOverrides) {
|
||||
var overrides = builder(this);
|
||||
for (var k in overrides) this.injectee[k] = overrides[k];
|
||||
}
|
||||
|
||||
// Inject $.entwine function overrides
|
||||
if (builder = handler.namespaceStaticOverrides) {
|
||||
var overrides = builder(this);
|
||||
for (var k in overrides) this.$.entwine[k] = overrides[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a function that does selector matching against the function list for a function name
|
||||
* Used by proxy for all calls, and by ctorProxy to handle _super calls
|
||||
* @param {String} name - name of the function as passed in the construction object
|
||||
* @param {String} funcprop - the property on the Rule object that gives the actual function to call
|
||||
* @param {function} basefunc - the non-entwine function to use as the catch-all function at the bottom of the stack
|
||||
*/
|
||||
one: function(name, funcprop, basefunc) {
|
||||
var namespace = this;
|
||||
var funcs = this.store[name];
|
||||
|
||||
var one = function(el, args, i){
|
||||
if (i === undefined) i = funcs.length;
|
||||
while (i--) {
|
||||
if (funcs[i].selector.matches(el)) {
|
||||
var ret, tmp_i = el.i, tmp_f = el.f;
|
||||
el.i = i; el.f = one;
|
||||
try { ret = funcs[i][funcprop].apply(namespace.$(el), args); }
|
||||
finally { el.i = tmp_i; el.f = tmp_f; }
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
// If we didn't find a entwine-defined function, but there is a non-entwine function to use as a base, try that
|
||||
if (basefunc) return basefunc.apply(namespace.$(el), args);
|
||||
};
|
||||
|
||||
return one;
|
||||
},
|
||||
|
||||
/**
|
||||
* A proxy is a function attached to a callable object (either the base jQuery.fn or a subspace object) which handles
|
||||
* finding and calling the correct function for each member of the current jQuery context
|
||||
* @param {String} name - name of the function as passed in the construction object
|
||||
* @param {function} basefunc - the non-entwine function to use as the catch-all function at the bottom of the stack
|
||||
*/
|
||||
build_proxy: function(name, basefunc) {
|
||||
var one = this.one(name, 'func', basefunc);
|
||||
|
||||
var prxy = function() {
|
||||
var rv, ctx = $(this);
|
||||
|
||||
var i = ctx.length;
|
||||
while (i--) rv = one(ctx[i], arguments);
|
||||
return rv;
|
||||
};
|
||||
|
||||
return prxy;
|
||||
},
|
||||
|
||||
bind_proxy: function(selector, name, func) {
|
||||
var rulelist = this.store[name] || (this.store[name] = $.entwine.RuleList());
|
||||
|
||||
var rule = rulelist.addRule(selector, name); rule.func = func;
|
||||
|
||||
if (!this.injectee.hasOwnProperty(name) || !this.injectee[name].isentwinemethod) {
|
||||
this.injectee[name] = this.build_proxy(name, this.injectee.hasOwnProperty(name) ? this.injectee[name] : null);
|
||||
this.injectee[name].isentwinemethod = true;
|
||||
}
|
||||
|
||||
if (!this.injectee[name].isentwinemethod) {
|
||||
$.entwine.warn('Warning: Entwine function '+name+' clashes with regular jQuery function - entwine function will not be callable directly on jQuery object', $.entwine.WARN_LEVEL_IMPORTANT);
|
||||
}
|
||||
},
|
||||
|
||||
add: function(selector, data) {
|
||||
// For every item in the hash, try ever method handler, until one returns true
|
||||
for (var k in data) {
|
||||
var v = data[k];
|
||||
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
if (handlers[i].bind && handlers[i].bind.call(this, selector, k, v)) break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
has: function(ctx, name) {
|
||||
var rulelist = this.store[name];
|
||||
if (!rulelist) return false;
|
||||
|
||||
/* We go forward this time, since low specifity is likely to knock out a bunch of elements quickly */
|
||||
for (var i = 0 ; i < rulelist.length; i++) {
|
||||
ctx = ctx.not(rulelist[i].selector);
|
||||
if (!ctx.length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A handler is some javascript code that adds support for some time of key / value pair passed in the hash to the Namespace add method.
|
||||
* The default handlers provided (and included by default) are event, ctor and properties
|
||||
*/
|
||||
$.entwine.Namespace.addHandler = function(handler) {
|
||||
for (var i = 0; i < handlers.length && handlers[i].order < handler.order; i++) { /* Pass */ }
|
||||
handlers.splice(i, 0, handler);
|
||||
};
|
||||
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 50,
|
||||
|
||||
bind: function(selector, k, v){
|
||||
if ($.isFunction(v)) {
|
||||
this.bind_proxy(selector, k, v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.extend($.fn, {
|
||||
/**
|
||||
* Main entwine function. Used for new definitions, calling into a namespace (or forcing the base namespace) and entering a using block
|
||||
*
|
||||
*/
|
||||
entwine: function(spacename) {
|
||||
var i = 0;
|
||||
/* Don't actually work out selector until we try and define something on it - we might be opening a namespace on an function-traveresed object
|
||||
which have non-standard selectors like .parents(.foo).slice(0,1) */
|
||||
var selector = null;
|
||||
|
||||
/* By default we operator on the base namespace */
|
||||
var namespace = namespaces.__base || $.entwine.Namespace();
|
||||
|
||||
/* If the first argument is a string, then it's the name of a namespace. Look it up */
|
||||
if (typeof spacename == 'string') {
|
||||
if (spacename.charAt('0') == '.') spacename = spacename.substr(1);
|
||||
if (spacename) namespace = namespaces[spacename] || $.entwine.Namespace(spacename);
|
||||
i=1;
|
||||
}
|
||||
|
||||
/* All remaining arguments should either be using blocks or definition hashs */
|
||||
while (i < arguments.length) {
|
||||
var res = arguments[i++];
|
||||
|
||||
// If it's a function, call it - either it's a using block or it's a namespaced entwine definition
|
||||
if ($.isFunction(res)) {
|
||||
if (res.length != 1) $.entwine.warn('Function block inside entwine definition does not take $ argument properly', $.entwine.WARN_LEVEL_IMPORTANT);
|
||||
res = res.call(namespace.$(this), namespace.$);
|
||||
}
|
||||
|
||||
// If we have a entwine definition hash, inject it into namespace
|
||||
if (res) {
|
||||
if (selector === null) selector = this.selector ? $.selector(this.selector) : false;
|
||||
|
||||
if (selector) namespace.add(selector, res);
|
||||
else $.entwine.warn('Entwine block given to entwine call without selector. Make sure you call $(selector).entwine when defining blocks', $.entwine.WARN_LEVEL_IMPORTANT);
|
||||
}
|
||||
}
|
||||
|
||||
/* Finally, return the jQuery object 'this' refers to, wrapped in the new namespace */
|
||||
return namespace.$(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls the next most specific version of the current entwine method
|
||||
*/
|
||||
_super: function(){
|
||||
var rv, i = this.length;
|
||||
while (i--) {
|
||||
var el = this[0];
|
||||
rv = el.f(el, arguments, el.i);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
21
thirdparty/jquery-entwine/src/jquery.entwine.legacy.js
vendored
Executable file
21
thirdparty/jquery-entwine/src/jquery.entwine.legacy.js
vendored
Executable file
@ -0,0 +1,21 @@
|
||||
(function($) {
|
||||
|
||||
// Adds back concrete methods for backwards compatibility
|
||||
$.concrete = $.entwine;
|
||||
$.fn.concrete = $.fn.entwine;
|
||||
$.fn.concreteData = $.fn.entwineData;
|
||||
|
||||
// Use addHandler to hack in the namespace.$.concrete equivilent to the namespace.$.entwine namespace-injection
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 100,
|
||||
bind: function(selector, k, v) { return false; },
|
||||
|
||||
namespaceMethodOverrides: function(namespace){
|
||||
namespace.$.concrete = namespace.$.entwine;
|
||||
namespace.injectee.concrete = namespace.injectee.entwine;
|
||||
namespace.injectee.concreteData = namespace.injectee.entwineData;
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
85
thirdparty/jquery-entwine/src/jquery.entwine.properties.js
vendored
Executable file
85
thirdparty/jquery-entwine/src/jquery.entwine.properties.js
vendored
Executable file
@ -0,0 +1,85 @@
|
||||
(function($) {
|
||||
|
||||
var entwine_prepend = '__entwine!';
|
||||
|
||||
var getEntwineData = function(el, namespace, property) {
|
||||
return el.data(entwine_prepend + namespace + '!' + property);
|
||||
};
|
||||
|
||||
var setEntwineData = function(el, namespace, property, value) {
|
||||
return el.data(entwine_prepend + namespace + '!' + property, value);
|
||||
};
|
||||
|
||||
var getEntwineDataAsHash = function(el, namespace) {
|
||||
var hash = {};
|
||||
var id = jQuery.data(el[0]);
|
||||
|
||||
var matchstr = entwine_prepend + namespace + '!';
|
||||
var matchlen = matchstr.length;
|
||||
|
||||
var cache = jQuery.cache[id];
|
||||
for (var k in cache) {
|
||||
if (k.substr(0,matchlen) == matchstr) hash[k.substr(matchlen)] = cache[k];
|
||||
}
|
||||
|
||||
return hash;
|
||||
};
|
||||
|
||||
var setEntwineDataFromHash = function(el, namespace, hash) {
|
||||
for (var k in hash) setEntwineData(namespace, k, hash[k]);
|
||||
};
|
||||
|
||||
var entwineData = function(el, namespace, args) {
|
||||
switch (args.length) {
|
||||
case 0:
|
||||
return getEntwineDataAsHash(el, namespace);
|
||||
case 1:
|
||||
if (typeof args[0] == 'string') return getEntwineData(el, namespace, args[0]);
|
||||
else return setEntwineDataFromHash(el, namespace, args[0]);
|
||||
default:
|
||||
return setEntwineData(el, namespace, args[0], args[1]);
|
||||
}
|
||||
};
|
||||
|
||||
$.extend($.fn, {
|
||||
entwineData: function() {
|
||||
return entwineData(this, '__base', arguments);
|
||||
}
|
||||
});
|
||||
|
||||
$.entwine.Namespace.addHandler({
|
||||
order: 60,
|
||||
|
||||
bind: function(selector, k, v) {
|
||||
if (k.charAt(0) != k.charAt(0).toUpperCase()) $.entwine.warn('Entwine property '+k+' does not start with a capital letter', $.entwine.WARN_LEVEL_BESTPRACTISE);
|
||||
|
||||
// Create the getters and setters
|
||||
|
||||
var getterName = 'get'+k;
|
||||
var setterName = 'set'+k;
|
||||
|
||||
this.bind_proxy(selector, getterName, function() { var r = this.entwineData(k); return r === undefined ? v : r; });
|
||||
this.bind_proxy(selector, setterName, function(v){ return this.entwineData(k, v); });
|
||||
|
||||
// Get the get and set proxies we just created
|
||||
|
||||
var getter = this.injectee[getterName];
|
||||
var setter = this.injectee[setterName];
|
||||
|
||||
// And bind in the jQuery-style accessor
|
||||
|
||||
this.bind_proxy(selector, k, function(v){ return (arguments.length == 1 ? setter : getter).call(this, v) ; });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
namespaceMethodOverrides: function(namespace){
|
||||
return {
|
||||
entwineData: function() {
|
||||
return entwineData(this, namespace.name, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
52
thirdparty/jquery-entwine/src/jquery.focusinout.js
vendored
Executable file
52
thirdparty/jquery-entwine/src/jquery.focusinout.js
vendored
Executable file
@ -0,0 +1,52 @@
|
||||
(function($){
|
||||
|
||||
/**
|
||||
* Add focusin and focusout support to bind and live for browers other than IE. Designed to be usable in a delegated fashion (like $.live)
|
||||
* Copyright (c) 2007 Jörn Zaefferer
|
||||
*/
|
||||
if ($.support.focusinBubbles === undefined) {
|
||||
$.support.focusinBubbles = !!($.browser.msie);
|
||||
}
|
||||
|
||||
if (!$.support.focusinBubbles && !$.event.special.focusin) {
|
||||
// Emulate focusin and focusout by binding focus and blur in capturing mode
|
||||
$.each({focus: 'focusin', blur: 'focusout'}, function(original, fix){
|
||||
$.event.special[fix] = {
|
||||
setup: function(){
|
||||
if (!this.addEventListener) return false;
|
||||
this.addEventListener(original, $.event.special[fix].handler, true);
|
||||
},
|
||||
teardown: function(){
|
||||
if (!this.removeEventListener) return false;
|
||||
this.removeEventListener(original, $.event.special[fix].handler, true);
|
||||
},
|
||||
handler: function(e){
|
||||
arguments[0] = $.event.fix(e);
|
||||
arguments[0].type = fix;
|
||||
return $.event.handle.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(function(){
|
||||
//IE has some trouble with focusout with select and keyboard navigation
|
||||
var activeFocus = null;
|
||||
|
||||
$(document)
|
||||
.bind('focusin', function(e){
|
||||
var target = e.realTarget || e.target;
|
||||
if (activeFocus && activeFocus !== target) {
|
||||
e.type = 'focusout';
|
||||
$(activeFocus).trigger(e);
|
||||
e.type = 'focusin';
|
||||
e.target = target;
|
||||
}
|
||||
activeFocus = target;
|
||||
})
|
||||
.bind('focusout', function(e){
|
||||
activeFocus = null;
|
||||
});
|
||||
})();
|
||||
|
||||
})(jQuery);
|
58
thirdparty/jquery-entwine/src/jquery.selector.affectedby.js
vendored
Executable file
58
thirdparty/jquery-entwine/src/jquery.selector.affectedby.js
vendored
Executable file
@ -0,0 +1,58 @@
|
||||
(function($) {
|
||||
|
||||
// TODO:
|
||||
// Make attributes & IDs work
|
||||
|
||||
var DIRECT = /DIRECT/g;
|
||||
var CONTEXT = /CONTEXT/g;
|
||||
var EITHER = /DIRECT|CONTEXT/g;
|
||||
|
||||
$.selector.SelectorBase.addMethod('affectedBy', function(props) {
|
||||
this.affectedBy = new Function('props', ([
|
||||
'var direct_classes, context_classes, direct_attrs, context_attrs, t;',
|
||||
this.ABC_compile().replace(DIRECT, 'direct').replace(CONTEXT, 'context'),
|
||||
'return {classes: {context: context_classes, direct: direct_classes}, attrs: {context: context_attrs, direct: direct_attrs}};'
|
||||
]).join("\n"));
|
||||
|
||||
// DEBUG: Print out the compiled funciton
|
||||
// console.log(this.selector, ''+this.affectedBy);
|
||||
|
||||
return this.affectedBy(props);
|
||||
});
|
||||
|
||||
$.selector.SimpleSelector.addMethod('ABC_compile', function() {
|
||||
var parts = [];
|
||||
|
||||
$.each(this.classes, function(i, cls){
|
||||
parts[parts.length] = "if (t = props.classes['"+cls+"']) (DIRECT_classes || (DIRECT_classes = {}))['"+cls+"'] = t;";
|
||||
});
|
||||
|
||||
$.each(this.nots, function(i, not){
|
||||
parts[parts.length] = not.ABC_compile();
|
||||
});
|
||||
|
||||
return parts.join("\n");
|
||||
});
|
||||
|
||||
$.selector.Selector.addMethod('ABC_compile', function(arg){
|
||||
var parts = [];
|
||||
var i = this.parts.length-1;
|
||||
|
||||
parts[parts.length] = this.parts[i].ABC_compile();
|
||||
while ((i = i - 2) >= 0) parts[parts.length] = this.parts[i].ABC_compile().replace(EITHER, 'CONTEXT');
|
||||
|
||||
return parts.join("\n");
|
||||
});
|
||||
|
||||
$.selector.SelectorsGroup.addMethod('ABC_compile', function(){
|
||||
var parts = [];
|
||||
|
||||
$.each(this.parts, function(i,part){
|
||||
parts[parts.length] = part.ABC_compile();
|
||||
});
|
||||
|
||||
return parts.join("\n");
|
||||
});
|
||||
|
||||
|
||||
})(jQuery);
|
1
thirdparty/tinymce/plugins/advimagescale/editor_plugin.js
vendored
Normal file
1
thirdparty/tinymce/plugins/advimagescale/editor_plugin.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
(function(){function e(e,t){var n=e.dom;var r=n.getAttrib(t,"mce_advimageresize_id");if(!e.originalDimensions[r]){e.originalDimensions[r]=e.lastDimensions[r]={width:n.getAttrib(t,"width",t.width),height:n.getAttrib(t,"height",t.height)}}return true}function t(t,n){var r=t.dom;var i=r.getAttrib(n,"mce_advimageresize_id");if(!i){var i=t.id+"_"+t.dom.uniqueId();r.setAttrib(n,"mce_advimageresize_id",i);e(t,n)}return i}function n(n,o,u){var l=n.dom;var c=t(n,o);var h=l.getAttrib(o,"width")!=n.lastDimensions[c].width||l.getAttrib(o,"height")!=n.lastDimensions[c].height;if(!h)return;if(l.getAttrib(o,"mce_noresize")||l.hasClass(o,n.getParam("advimagescale_noresize_class","noresize"))||n.getParam("advimagescale_noresize_all")){l.setAttrib(o,"width",n.lastDimensions[c].width);l.setAttrib(o,"height",n.lastDimensions[c].height);if(tinymce.isGecko)i(n);return}if(n.getParam("advimagescale_fix_border_glitch",true)){r(n,o);e(n,o)}var p=n.getParam("advimagescale_filter_src");if(p){var d=new RegExp(p);if(!o.src.match(d)){return}}var v=n.getParam("advimagescale_filter_class");if(v){if(!l.hasClass(o,v)){return}}var m={width:l.getAttrib(o,"width",o.width),height:l.getAttrib(o,"height",o.height)};if(n.getParam("advimagescale_maintain_aspect_ratio",true)){m=a(n,o,m.width,m.height)}m=f(n,o,m.width,m.height);var g=l.getAttrib(o,"width",o.width)!=m.width||l.getAttrib(o,"height",o.height)!=m.height;if(g){l.setAttrib(o,"width",m.width);l.setAttrib(o,"height",m.height);if(tinymce.isGecko)i(n)}if(n.getParam("advimagescale_append_to_url")){s(n,o,l.getAttrib(o,"width",o.width),l.getAttrib(o,"height",o.height))}if(n.lastDimensions[c].width!=l.getAttrib(o,"width",o.width)||n.lastDimensions[c].height!=l.getAttrib(o,"height",o.height)){if(n.getParam("advimagescale_resize_callback")){n.getParam("advimagescale_resize_callback")(n,o)}}n.lastDimensions[c]={width:l.getAttrib(o,"width",o.width),height:l.getAttrib(o,"height",o.height)}}function r(e,t){var n=e.dom;var r=n.getAttrib(t,"mce_advimageresize_id");var s=n.getAttrib(t,"width",t.width);var o=n.getAttrib(t,"height",t.height);var u=false;if(s!=e.lastDimensions[r].width){var a=0;a+=parseInt(n.getStyle(t,"borderLeftWidth","borderLeftWidth"));a+=parseInt(n.getStyle(t,"borderRightWidth","borderRightWidth"));if(a>0){n.setAttrib(t,"width",s-a);u=true}}if(o!=e.lastDimensions[r].height){var f=0;f+=parseInt(n.getStyle(t,"borderTopWidth","borderTopWidth"));f+=parseInt(n.getStyle(t,"borderBottomWidth","borderBottomWidth"));if(f>0){n.setAttrib(t,"height",o-f);u=true}}if(u&&tinymce.isGecko)i(e)}function i(e){e.execCommand("mceRepaint",false)}function s(e,t,n,r){var i=e.dom;var s=i.getAttrib(t,"src");var a=e.getParam("advimagescale_url_width_key","w");s=u(s,a,n);var f=e.getParam("advimagescale_url_height_key","h");s=u(s,f,r);if(s==i.getAttrib(t,"src")){return}if(e.getParam("advimagescale_loading_callback")){e.getParam("advimagescale_loading_callback")(t)}if(e.getParam("advimagescale_loaded_callback")){tinymce.dom.Event.add(t,"load",o,{el:t,ed:e})}i.setAttrib(t,"src",s)}function o(e){var t=this.el;var n=this.ed;var r=n.getParam("advimagescale_loaded_callback");r(t);tinymce.dom.Event.remove(t,"load",o)}function u(e,t,n){if(!e.match(/\?/))e+="?";if(!e.match(new RegExp("([?&])"+t+"="))){if(!e.match(/[&\?]$/))e+="&";e+=t+"="+escape(n)}else{e=e.replace(new RegExp("([?&])"+t+"=[^&]*"),"$1"+t+"="+escape(n))}return e}function a(e,t,n,r){var i=e.dom.getAttrib(t,"mce_advimageresize_id");var s=e.originalDimensions[i].width/e.originalDimensions[i].height;var o=e.lastDimensions[i].width;var u=e.lastDimensions[i].height;var a=Math.abs(o-n);var f=Math.abs(u-r);var l=Math.abs(a/o);var c=Math.abs(f/u);if(a||f){if(l>c){return{width:n,height:Math.round(n/s)}}else{return{width:Math.round(r*s),height:r}}}return{width:n,height:r}}function f(e,t,n,r){var i=e.dom.getAttrib(t,"mce_advimageresize_id");var s=e.getParam("advimagescale_max_width");var o=e.getParam("advimagescale_max_height");var u=e.getParam("advimagescale_min_width");var a=e.getParam("advimagescale_min_height");var f=e.getParam("advimagescale_maintain_aspect_ratio",true);var l=e.originalDimensions[i].width;var c=e.originalDimensions[i].height;var h=l/c;if(s&&n>s){n=s;r=f?Math.round(n/h):r}if(o&&r>o){r=o;n=f?Math.round(r*h):n}if(u&&n<u){n=u;r=f?Math.round(n/h):r}if(a&&r<a){r=a;n=f?Math.round(r*h):r}return{width:n,height:r}}tinymce.create("tinymce.plugins.AdvImageScale",{init:function(e,r){e.originalDimensions=new Array;e.lastDimensions=new Array;e.edMouseDown=false;e.onMouseDown.add(function(e,n){var r=tinyMCE.activeEditor.selection.getNode();if(r!=null&&r.nodeName=="IMG"){t(e,n.target)}return true});e.onMouseUp.add(function(e,t){var r=tinyMCE.activeEditor.selection.getNode();if(r!=null&&r.nodeName=="IMG"){setTimeout(function(){n(e,r)},100)}return true});e.onPreProcess.add(function(e,t){if(!t.set)return;tinymce.each(e.dom.select("img",t.node),function(t){n(e,t)})});e.onInit.add(function(e){e.selection.onSetContent.add(function(t,r){var i=t.getNode();tinymce.each(e.dom.select("img",i),function(t){if(t.id!="__mce_tmp")n(e,t)})})});if(e.getParam("advimagescale_reject_external_dragdrop",true)){e.onMouseDown.add(function(t){e.edMouseDown=true});e.onMouseUp.add(function(t){e.edMouseDown=false});e.onInit.add(function(e,t){tinymce.dom.Event.add(e.getBody().parentNode,"dragdrop",function(t){e.edMouseDown=false})});var i=tinymce.isIE?"dragenter":"dragover";e.onInit.add(function(e,t){tinymce.dom.Event.add(e.getBody().parentNode,i,function(t){if(!e.edMouseDown){return tinymce.dom.Event.cancel(t)}})})}},getInfo:function(){return{longname:"Advanced Image Resize Helper",author:"Marc Hodgins",authorurl:"http://www.hodginsmedia.com",infourl:"http://code.google.com/p/tinymce-plugin-advimagescale",version:"1.1.3"}}});tinymce.PluginManager.add("advimagescale",tinymce.plugins.AdvImageScale)})();
|
454
thirdparty/tinymce/plugins/advimagescale/editor_plugin_src.js
vendored
Normal file
454
thirdparty/tinymce/plugins/advimagescale/editor_plugin_src.js
vendored
Normal file
@ -0,0 +1,454 @@
|
||||
/**
|
||||
* TinyMCE Advanced Image Resize Helper Plugin
|
||||
*
|
||||
* Forces images to maintain aspect ratio while scaling - also optionally enforces
|
||||
* min/max image dimensions, and appends width/height to the image URL for server-side
|
||||
* resizing
|
||||
*
|
||||
* @author Marc Hodgins (modified to remove global variables: http://sourceforge.net/p/tinymce/plugins/186/)
|
||||
* @link http://www.hodginsmedia.com Hodgins Media Ventures Inc.
|
||||
* @copyright Copyright (C) 2008-2010 Hodgins Media Ventures Inc., All right reserved.
|
||||
* @license http://www.opensource.org/licenses/lgpl-3.0.html LGPLv3
|
||||
*/
|
||||
(function() {
|
||||
|
||||
tinymce.create('tinymce.plugins.AdvImageScale', {
|
||||
/**
|
||||
* Initializes the plugin, this will be executed after the plugin has been created.
|
||||
*
|
||||
* @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
|
||||
* @param {string} url Absolute URL to where the plugin is located.
|
||||
*/
|
||||
init : function(ed, url) {
|
||||
|
||||
/**
|
||||
* Stores pre-resize image dimensions
|
||||
* @var {array} (w,h)
|
||||
*/
|
||||
ed.originalDimensions = new Array();
|
||||
|
||||
/**
|
||||
* Stores last dimensions before a resize
|
||||
* @var {array} (w,h)
|
||||
*/
|
||||
ed.lastDimensions = new Array();
|
||||
|
||||
/**
|
||||
* Track mousedown status in editor
|
||||
* @var {boolean}
|
||||
*/
|
||||
ed.edMouseDown = false;
|
||||
|
||||
// Watch for mousedown (as a fall through to ensure that prepareImage() definitely
|
||||
// got called on an image tag before mouseup).
|
||||
//
|
||||
// Normally this should have happened via the onPreProcess/onSetContent listeners, but
|
||||
// for completeness we check once more here in case there are edge cases we've missed.
|
||||
ed.onMouseDown.add(function(ed, e) {
|
||||
var el = tinyMCE.activeEditor.selection.getNode();
|
||||
if (el != null && el.nodeName == 'IMG') {
|
||||
// prepare image for resizing
|
||||
prepareImage(ed, e.target);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Watch for mouseup (catch image resizes)
|
||||
ed.onMouseUp.add(function(ed, e) {
|
||||
var el = tinyMCE.activeEditor.selection.getNode();
|
||||
if (el != null && el.nodeName == 'IMG') {
|
||||
// setTimeout is necessary to allow the browser to complete the resize so we have new dimensions
|
||||
setTimeout(function() {
|
||||
constrainSize(ed, el);
|
||||
}, 100);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/*****************************************************
|
||||
* ENFORCE CONSTRAINTS ON CONTENT INSERTED INTO EDITOR
|
||||
*****************************************************/
|
||||
|
||||
// Catch editor.setContent() events via onPreProcess (because onPreProcess allows us to
|
||||
// modify the DOM before it is inserted, unlike onSetContent)
|
||||
ed.onPreProcess.add(function(ed, o) {
|
||||
if (!o.set) return; // only 'set' operations let us modify the nodes
|
||||
|
||||
// loop in each img node and run constrainSize
|
||||
tinymce.each(ed.dom.select('img', o.node), function(currentNode) {
|
||||
constrainSize(ed, currentNode);
|
||||
});
|
||||
});
|
||||
|
||||
// To be complete, we also need to watch for setContent() calls on the selection object so that
|
||||
// constraints are enforced (i.e. in case an <img> tag is inserted via mceInsertContent).
|
||||
// So, catch all insertions using the editor's selection object
|
||||
ed.onInit.add(function(ed) {
|
||||
// http://wiki.moxiecode.com/index.php/TinyMCE:API/tinymce.dom.Selection/onSetContent
|
||||
ed.selection.onSetContent.add(function(se, o) {
|
||||
// @todo This seems to grab the entire editor contents - it works but could
|
||||
// perform poorly on large documents
|
||||
var currentNode = se.getNode();
|
||||
tinymce.each(ed.dom.select('img', currentNode), function (currentNode) {
|
||||
// IF condition required as tinyMCE inserts 24x24 placeholders uner some conditions
|
||||
if (currentNode.id != "__mce_tmp")
|
||||
constrainSize(ed, currentNode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*****************************
|
||||
* DISALLOW EXTERNAL IMAGE DRAG/DROPS
|
||||
*****************************/
|
||||
// This is a hack. Listening for drag events wasn't working.
|
||||
//
|
||||
// Watches for mousedown and mouseup/dragdrop events within the editor. If a mouseup or
|
||||
// dragdrop occurs in the editor without a preceeding mousedown, we assume it is an external
|
||||
// dragdrop that should be rejected.
|
||||
if (ed.getParam('advimagescale_reject_external_dragdrop', true)) {
|
||||
|
||||
// catch mousedowns mouseups and dragdrops (which are basically mouseups too..)
|
||||
ed.onMouseDown.add(function(e) { ed.edMouseDown = true; });
|
||||
ed.onMouseUp.add(function(e) { ed.edMouseDown = false; });
|
||||
ed.onInit.add(function(ed, o) {
|
||||
tinymce.dom.Event.add(ed.getBody().parentNode, 'dragdrop', function(e) { ed.edMouseDown = false; });
|
||||
});
|
||||
|
||||
// watch for drag attempts
|
||||
var evt = (tinymce.isIE) ? 'dragenter' : 'dragover'; // IE allows dragdrop reject on dragenter (more efficient)
|
||||
ed.onInit.add(function(ed, o) {
|
||||
// use parentNode to go above editor content, to cover entire editor area
|
||||
tinymce.dom.Event.add(ed.getBody().parentNode, evt, function (e) {
|
||||
if (!ed.edMouseDown) {
|
||||
// disallow drop
|
||||
return tinymce.dom.Event.cancel(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns information about the plugin as a name/value array.
|
||||
* The current keys are longname, author, authorurl, infourl and version.
|
||||
*
|
||||
* @return {Object} Name/value array containing information about the plugin.
|
||||
*/
|
||||
getInfo : function() {
|
||||
return {
|
||||
longname : 'Advanced Image Resize Helper',
|
||||
author : 'Marc Hodgins',
|
||||
authorurl : 'http://www.hodginsmedia.com',
|
||||
infourl : 'http://code.google.com/p/tinymce-plugin-advimagescale',
|
||||
version : '1.1.3'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Register plugin
|
||||
tinymce.PluginManager.add('advimagescale', tinymce.plugins.AdvImageScale);
|
||||
|
||||
/**
|
||||
* Store image dimensions, pre-resize
|
||||
*
|
||||
* @param {object} el HTMLDomNode
|
||||
*/
|
||||
function storeDimensions(ed, el) {
|
||||
var dom = ed.dom;
|
||||
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
|
||||
|
||||
// store original dimensions if this is the first resize of this element
|
||||
if (!ed.originalDimensions[elId]) {
|
||||
ed.originalDimensions[elId] = ed.lastDimensions[elId] = {width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height)};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare image for resizing
|
||||
* Check to see if we've seen this IMG tag before; does tasks such as adding
|
||||
* unique IDs to image tags, saving "original" image dimensions, etc.
|
||||
* @param {object} e is optional
|
||||
*/
|
||||
function prepareImage(ed, el) {
|
||||
var dom = ed.dom;
|
||||
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
|
||||
|
||||
// is this the first time this image tag has been seen?
|
||||
if (!elId) {
|
||||
var elId = ed.id + "_" + ed.dom.uniqueId();
|
||||
dom.setAttrib(el, 'mce_advimageresize_id', elId);
|
||||
storeDimensions(ed, el);
|
||||
}
|
||||
|
||||
return elId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts width and height to keep within min/max bounds and also maintain aspect ratio
|
||||
* If mce_noresize attribute is set to image tag, then image resize is disallowed
|
||||
*/
|
||||
function constrainSize(ed, el, e) {
|
||||
var dom = ed.dom;
|
||||
var elId = prepareImage(ed, el); // also calls storeDimensions
|
||||
var resized = (dom.getAttrib(el, 'width') != ed.lastDimensions[elId].width || dom.getAttrib(el, 'height') != ed.lastDimensions[elId].height);
|
||||
|
||||
if (!resized)
|
||||
return; // nothing to do
|
||||
|
||||
// disallow image resize if mce_noresize or the noresize class is set on the image tag
|
||||
if (dom.getAttrib(el, 'mce_noresize') || dom.hasClass(el, ed.getParam('advimagescale_noresize_class', 'noresize')) || ed.getParam('advimagescale_noresize_all')) {
|
||||
dom.setAttrib(el, 'width', ed.lastDimensions[elId].width);
|
||||
dom.setAttrib(el, 'height', ed.lastDimensions[elId].height);
|
||||
if (tinymce.isGecko)
|
||||
fixGeckoHandles(ed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Both IE7 and Gecko (as of FF3.0.03) has a "expands image by border width" bug before doing anything else
|
||||
if (ed.getParam('advimagescale_fix_border_glitch', true /* default to true */)) {
|
||||
fixImageBorderGlitch(ed, el);
|
||||
storeDimensions(ed, el); // store adjusted dimensions
|
||||
}
|
||||
|
||||
// filter by regexp so only some images get constrained
|
||||
var src_filter = ed.getParam('advimagescale_filter_src');
|
||||
if (src_filter) {
|
||||
var r = new RegExp(src_filter);
|
||||
if (!el.src.match(r)) {
|
||||
return; // skip this element
|
||||
}
|
||||
}
|
||||
|
||||
// allow filtering by classname
|
||||
var class_filter = ed.getParam('advimagescale_filter_class');
|
||||
if (class_filter) {
|
||||
if (!dom.hasClass(el, class_filter)) {
|
||||
return; // skip this element, doesn't have the class we want
|
||||
}
|
||||
}
|
||||
|
||||
// populate new dimensions object
|
||||
var newDimensions = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
|
||||
|
||||
// adjust w/h to maintain aspect ratio
|
||||
if (ed.getParam('advimagescale_maintain_aspect_ratio', true /* default to true */)) {
|
||||
newDimensions = maintainAspect(ed, el, newDimensions.width, newDimensions.height);
|
||||
}
|
||||
|
||||
// enforce minW/minH/maxW/maxH
|
||||
newDimensions = checkBoundaries(ed, el, newDimensions.width, newDimensions.height);
|
||||
|
||||
// was an adjustment made?
|
||||
var adjusted = (dom.getAttrib(el, 'width', el.width) != newDimensions.width || dom.getAttrib(el, 'height', el.height) != newDimensions.height);
|
||||
|
||||
// apply new w/h
|
||||
if (adjusted) {
|
||||
dom.setAttrib(el, 'width', newDimensions.width);
|
||||
dom.setAttrib(el, 'height', newDimensions.height);
|
||||
if (tinymce.isGecko) fixGeckoHandles(ed);
|
||||
}
|
||||
|
||||
if (ed.getParam('advimagescale_append_to_url')) {
|
||||
appendToUri(ed, el, dom.getAttrib(el, 'width', el.width), dom.getAttrib(el, 'height', el.height));
|
||||
}
|
||||
|
||||
// was the image resized?
|
||||
if (ed.lastDimensions[elId].width != dom.getAttrib(el, 'width', el.width) || ed.lastDimensions[elId].height != dom.getAttrib(el, 'height', el.height)) {
|
||||
// call "image resized" callback (if set)
|
||||
if (ed.getParam('advimagescale_resize_callback')) {
|
||||
ed.getParam('advimagescale_resize_callback')(ed, el);
|
||||
}
|
||||
}
|
||||
|
||||
// remember "last dimensions" for next time
|
||||
ed.lastDimensions[elId] = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes IE7 and Gecko border width glitch
|
||||
*
|
||||
* Both "add" the border width to an image after the resize handles have been
|
||||
* dropped. This reverses it by looking at the "previous" known size and comparing
|
||||
* to the current size. If they don't match, then a resize has taken place and the browser
|
||||
* has (probably) messed it up. So, we reverse it. Note, this will probably need to be
|
||||
* wrapped in a conditional statement if/when each browser fixes this bug.
|
||||
*/
|
||||
function fixImageBorderGlitch(ed, el) {
|
||||
var dom = ed.dom;
|
||||
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
|
||||
var currentWidth = dom.getAttrib(el, 'width', el.width);
|
||||
var currentHeight = dom.getAttrib(el, 'height', el.height);
|
||||
var adjusted = false;
|
||||
|
||||
// if current dimensions do not match what we last saw, then a resize has taken place
|
||||
if (currentWidth != ed.lastDimensions[elId].width) {
|
||||
var adjustWidth = 0;
|
||||
|
||||
// get computed border left/right widths
|
||||
adjustWidth += parseInt(dom.getStyle(el, 'borderLeftWidth', 'borderLeftWidth'));
|
||||
adjustWidth += parseInt(dom.getStyle(el, 'borderRightWidth', 'borderRightWidth'));
|
||||
|
||||
// reset the width height to NOT include these amounts
|
||||
if (adjustWidth > 0) {
|
||||
dom.setAttrib(el, 'width', (currentWidth - adjustWidth));
|
||||
adjusted = true;
|
||||
}
|
||||
}
|
||||
if (currentHeight != ed.lastDimensions[elId].height) {
|
||||
var adjustHeight = 0;
|
||||
|
||||
// get computed border top/bottom widths
|
||||
adjustHeight += parseInt(dom.getStyle(el, 'borderTopWidth', 'borderTopWidth'));
|
||||
adjustHeight += parseInt(dom.getStyle(el, 'borderBottomWidth', 'borderBottomWidth'));
|
||||
|
||||
if (adjustHeight > 0) {
|
||||
dom.setAttrib(el, 'height', (currentHeight - adjustHeight));
|
||||
adjusted = true;
|
||||
}
|
||||
}
|
||||
if (adjusted && tinymce.isGecko) fixGeckoHandles(ed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix gecko resize handles glitch
|
||||
*/
|
||||
function fixGeckoHandles(ed) {
|
||||
ed.execCommand('mceRepaint', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set image dimensions on into a uri as querystring params
|
||||
*/
|
||||
function appendToUri(ed, el, w, h) {
|
||||
var dom = ed.dom;
|
||||
var uri = dom.getAttrib(el, 'src');
|
||||
var wKey = ed.getParam('advimagescale_url_width_key', 'w');
|
||||
uri = setQueryParam(uri, wKey, w);
|
||||
var hKey = ed.getParam('advimagescale_url_height_key', 'h');
|
||||
uri = setQueryParam(uri, hKey, h);
|
||||
|
||||
// no need to continue if URL didn't change
|
||||
if (uri == dom.getAttrib(el, 'src')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// trigger image loading callback (if set)
|
||||
if (ed.getParam('advimagescale_loading_callback')) {
|
||||
// call loading callback
|
||||
ed.getParam('advimagescale_loading_callback')(el);
|
||||
}
|
||||
// hook image load(ed) callback (if set)
|
||||
if (ed.getParam('advimagescale_loaded_callback')) {
|
||||
// hook load event on the image tag to call the loaded callback
|
||||
tinymce.dom.Event.add(el, 'load', imageLoadedCallback, {el: el, ed: ed});
|
||||
}
|
||||
|
||||
// set new src
|
||||
dom.setAttrib(el, 'src', uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback event when an image is (re)loaded
|
||||
* @param {object} e Event (use e.target or this.el to access element, this.ed to access editor instance)
|
||||
*/
|
||||
function imageLoadedCallback(e) {
|
||||
var el = this.el; // image element
|
||||
var ed = this.ed; // editor
|
||||
var callback = ed.getParam('advimagescale_loaded_callback'); // user specified callback
|
||||
|
||||
// call callback, pass img as param
|
||||
callback(el);
|
||||
|
||||
// remove callback event
|
||||
tinymce.dom.Event.remove(el, 'load', imageLoadedCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets URL querystring parameters by appending or replacing existing params of same name
|
||||
*/
|
||||
function setQueryParam(uri, key, value) {
|
||||
if (!uri.match(/\?/)) uri += '?';
|
||||
if (!uri.match(new RegExp('([\?&])' + key + '='))) {
|
||||
if (!uri.match(/[&\?]$/)) uri += '&';
|
||||
uri += key + '=' + escape(value);
|
||||
} else {
|
||||
uri = uri.replace(new RegExp('([\?\&])' + key + '=[^&]*'), '$1' + key + '=' + escape(value));
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns w/h that maintain aspect ratio
|
||||
*/
|
||||
function maintainAspect(ed, el, w, h) {
|
||||
var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');
|
||||
|
||||
// calculate aspect ratio of original so we can maintain it
|
||||
var ratio = ed.originalDimensions[elId].width / ed.originalDimensions[elId].height;
|
||||
|
||||
// decide which dimension changed more (percentage), because that's the
|
||||
// one we'll respect (the other we'll adjust to keep aspect ratio)
|
||||
var lastW = ed.lastDimensions[elId].width;
|
||||
var lastH = ed.lastDimensions[elId].height;
|
||||
var deltaW = Math.abs(lastW - w); // absolute
|
||||
var deltaH = Math.abs(lastH - h); // absolute
|
||||
var pctW = Math.abs(deltaW / lastW); // percentage
|
||||
var pctH = Math.abs(deltaH / lastH); // percentage
|
||||
|
||||
if (deltaW || deltaH) {
|
||||
if (pctW > pctH) {
|
||||
// width changed more - use that as the locked point and adjust height
|
||||
return { width: w, height: Math.round(w / ratio) };
|
||||
} else {
|
||||
// height changed more - use that as the locked point and adjust width
|
||||
return { width: Math.round(h * ratio), height: h };
|
||||
}
|
||||
}
|
||||
|
||||
// nothing changed
|
||||
return { width: w, height: h };
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce min/max boundaries
|
||||
*
|
||||
* Returns true if an adjustment was made
|
||||
*/
|
||||
function checkBoundaries(ed, el, w, h) {
|
||||
|
||||
var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');
|
||||
var maxW = ed.getParam('advimagescale_max_width');
|
||||
var maxH = ed.getParam('advimagescale_max_height');
|
||||
var minW = ed.getParam('advimagescale_min_width');
|
||||
var minH = ed.getParam('advimagescale_min_height');
|
||||
var maintainAspect = ed.getParam('advimagescale_maintain_aspect_ratio', true);
|
||||
var oW = ed.originalDimensions[elId].width;
|
||||
var oH = ed.originalDimensions[elId].height;
|
||||
var ratio = oW/oH;
|
||||
|
||||
// max
|
||||
if (maxW && w > maxW) {
|
||||
w = maxW;
|
||||
h = maintainAspect ? Math.round(w / ratio) : h;
|
||||
}
|
||||
if (maxH && h > maxH) {
|
||||
h = maxH;
|
||||
w = maintainAspect ? Math.round(h * ratio) : w;
|
||||
}
|
||||
|
||||
// min
|
||||
if (minW && w < minW) {
|
||||
w = minW;
|
||||
h = maintainAspect ? Math.round(w / ratio) : h;
|
||||
}
|
||||
if (minH && h < minH) {
|
||||
h = minH;
|
||||
w = maintainAspect ? Math.round(h * ratio) : h;
|
||||
}
|
||||
|
||||
return { width: w, height:h };
|
||||
}
|
||||
|
||||
})();
|
Loading…
x
Reference in New Issue
Block a user