ENHANCEMENT Inserting image via new dialog with ajax field retrieval and GridField file selection. Rewritten to jQuery.entwine and using the new HTML editor abstraction layer.

This commit is contained in:
Ingo Schommer 2012-02-09 17:17:39 +01:00
parent 2c5d71dc29
commit 5220a46fd0
19 changed files with 962 additions and 725 deletions

View File

@ -18,7 +18,6 @@ HtmlEditorConfig::get('cms')->setOptions(array(
'body_class' => 'typography',
'document_base_url' => Director::absoluteBaseURL(),
'setupcontent_callback' => "sapphiremce_setupcontent",
'cleanup_callback' => "sapphiremce_cleanup",
'use_native_selects' => true, // fancy selects are bug as of SS 2.3.0

View File

@ -230,8 +230,6 @@ class LeftAndMain extends Controller {
Requirements::combine_files(
'lib.js',
array(
THIRDPARTY_DIR . '/prototype/prototype.js',
THIRDPARTY_DIR . '/behaviour/behaviour.js',
SAPPHIRE_DIR . '/javascript/prototype_improvements.js',
THIRDPARTY_DIR . '/jquery/jquery.js',
SAPPHIRE_DIR . '/javascript/jquery_improvements.js',
@ -255,15 +253,13 @@ class LeftAndMain extends Controller {
SAPPHIRE_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js',
SAPPHIRE_ADMIN_DIR . '/javascript/jquery-changetracker/lib/jquery.changetracker.js',
SAPPHIRE_DIR . '/javascript/TreeDropdownField.js',
SAPPHIRE_DIR ."/thirdparty/jquery-form/jquery.form.js",
SAPPHIRE_DIR . '/javascript/DateField.js',
SAPPHIRE_DIR . '/javascript/HtmlEditorField.js',
SAPPHIRE_DIR . '/javascript/TabSet.js',
SAPPHIRE_DIR . '/javascript/Validator.js',
SAPPHIRE_DIR . '/javascript/i18n.js',
SAPPHIRE_ADMIN_DIR . '/javascript/ssui.core.js',
SAPPHIRE_DIR . '/javascript/tiny_mce_improvements.js',
CMS_DIR . '/javascript/ThumbnailStripField.js',
SAPPHIRE_DIR . '/javascript/GridField.js',
)
);
@ -294,6 +290,7 @@ class LeftAndMain extends Controller {
Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
Requirements::css(SAPPHIRE_DIR . '/css/TreeDropdownField.css');
Requirements::css(SAPPHIRE_ADMIN_DIR . '/css/screen.css');
Requirements::css(SAPPHIRE_DIR . '/css/GridField.css');
// Browser-specific requirements
$ie = isset($_SERVER['HTTP_USER_AGENT']) ? strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') : false;

View File

@ -425,6 +425,16 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
/** -------------------------------------------- "Insert X" forms -------------------------------------------- */
.htmleditorfield-linkform .step2 { margin-bottom: 16px; }
.htmleditorfield-mediaform .ss-gridfield tbody td:first-child img { max-height: 30px; }
.htmleditorfield-mediaform .ss-htmleditorfield-file { border: 1px solid #b3b3b3; -moz-border-radius: 5px; -webkit-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; -khtml-border-radius: 5px; border-radius: 5px; -moz-background-clip: padding; -webkit-background-clip: padding; -o-background-clip: padding-box; -ms-background-clip: padding-box; -khtml-background-clip: padding-box; background-clip: padding-box; background: #E2E2E2; margin-bottom: 16px; }
.htmleditorfield-mediaform .ss-htmleditorfield-file.loading { width: 100%; height: 100px; background-image: url(../images/spinner.gif); background-position: 50% 50%; background-repeat: no-repeat; }
.htmleditorfield-mediaform .ss-htmleditorfield-file .overview { background-color: #5db4df; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5db4df), color-stop(8%, #5db1dd), color-stop(50%, #439bcb), color-stop(54%, #3f99cd), color-stop(96%, #207db6), color-stop(100%, #1e7cba)); background-image: -webkit-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -moz-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -o-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -ms-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); }
.htmleditorfield-mediaform .ss-htmleditorfield-file .overview .thumbnail { display: inline-block; vertical-align: middle; padding: 4px; }
.htmleditorfield-mediaform .ss-htmleditorfield-file .overview .thumbnail img { max-height: 24px; }
.htmleditorfield-mediaform .ss-htmleditorfield-file .overview .title { display: inline-block; vertical-align: middle; background: #fff; border: 1px solid #b3b3b3; -moz-border-radius: 5px; -webkit-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; -khtml-border-radius: 5px; border-radius: 5px; margin-left: 16px; padding: 4px; }
.htmleditorfield-mediaform .ss-htmleditorfield-file .overview .action-delete { display: inline-block; }
.htmleditorfield-mediaform .ss-htmleditorfield-file .details { padding: 16px; }
/** -------------------------------------------- Step labels -------------------------------------------- */
.step-label > * { display: inline-block; vertical-align: top; }
.step-label .flyout { height: 18px; font-size: 14px; font-weight: bold; -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; -o-border-top-left-radius: 3px; -ms-border-top-left-radius: 3px; -khtml-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; -o-border-bottom-left-radius: 3px; -ms-border-bottom-left-radius: 3px; -khtml-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; background-color: #667980; padding: 4px 3px 4px 6px; text-align: center; text-shadow: none; color: #fff; }

View File

@ -38,8 +38,6 @@
onmatch: function() {
var self = this, typeDropdown = this.find(':input[name=PageType]');
Observable.applyTo(this[0]);
var tree = $('.cms-tree');
this.setTree(tree);

View File

@ -220,7 +220,7 @@
if(status == 'success') {
var form = this.replaceForm(oldForm, data);
Behaviour.apply(); // refreshes ComplexTableField
if(typeof(Behaviour) != 'undefined') Behaviour.apply(); // refreshes ComplexTableField
this.trigger('reloadeditform', {form: form, origData: origData, xmlhttp: xmlhttp});
}

View File

@ -182,88 +182,6 @@
return false;
}
});
/**
* Class: .cms-edit-form textarea.htmleditor
*
* Add tinymce to HtmlEditorFields within the CMS. Works in combination
* with a TinyMCE.init() call which is prepopulated with the used HTMLEditorConfig settings,
* and included in the page as an inline <script> tag.
*/
$('.cms-edit-form textarea.htmleditor').entwine({
/**
* Constructor: onmatch
*/
onmatch : function() {
var self = this;
this.closest('form').bind('beforesave', function() {
if(typeof tinyMCE == 'undefined') return;
// TinyMCE modifies input, so change tracking might get false
// positives when comparing string values - don't save if the editor doesn't think its dirty.
if(self.isChanged()) {
tinyMCE.triggerSave();
// TinyMCE assigns value attr directly, which doesn't trigger change event
self.trigger('change');
}
});
// Only works after TinyMCE.init() has been invoked, see $(window).bind() call below for details.
this.redraw();
this._super();
},
redraw: function() {
// Using a global config (generated through HTMLEditorConfig PHP logic)
var config = ssTinyMceConfig, self = this;
// Avoid flicker (also set in CSS to apply as early as possible)
self.css('visibility', '');
// Create editor instance and render it.
// Similar logic to adapter/jquery/jquery.tinymce.js, but doesn't rely on monkey-patching
// jQuery methods, and avoids replicate the script lazyloading which is already in place with jQuery.ondemand.
var ed = new tinymce.Editor(this.attr('id'), config);
ed.onInit.add(function() {
self.css('visibility', 'visible');
});
ed.render();
// Handle editor de-registration by hooking into state changes.
// TODO Move to onunmatch for less coupling (once we figure out how to work with detached DOM nodes in TinyMCE)
$('.cms-container').bind('beforestatechange', function() {
self.css('visibility', 'hidden');
var ed = tinyMCE.get(self.attr('id'));
if(ed) ed.remove();
});
this._super();
},
isChanged: function() {
if(typeof tinyMCE == 'undefined') return;
var inst = tinyMCE.getInstanceById(this.attr('id'));
return inst ? inst.isDirty() : false;
},
resetChanged: function() {
if(typeof tinyMCE == 'undefined') return;
var inst = tinyMCE.getInstanceById(this.attr('id'));
if (inst) inst.startContent = tinymce.trim(inst.getContent({format : 'raw', no_events : 1}));
},
onunmatch: function() {
// TODO Throws exceptions in Firefox, most likely due to the element being removed from the DOM at this point
// var ed = tinyMCE.get(this.attr('id'));
// if(ed) ed.remove();
this._super();
}
});
$('.cms-edit-form .ss-gridfield .action-edit').entwine({
onclick: function(e) {

View File

@ -1065,6 +1065,66 @@ body.cms-dialog {
}
}
.htmleditorfield-mediaform {
.ss-gridfield {
// Set thumbnail size
tbody td:first-child img {
max-height: 30px;
}
}
// TODO Consolidate with .assetuploadfield and .ss-uploadfield styles
.ss-htmleditorfield-file {
border: 1px solid lighten($color-medium-separator, 20%);
@include border-radius(5px);
@include background-clip(padding-box);
background: #E2E2E2;
margin-bottom: $grid-horizontal*2;
&.loading {
width: 100%;
height: 100px;
background-image: url(../images/spinner.gif);
background-position: 50% 50%;
background-repeat: no-repeat;
}
.overview {
background-color: #5db4df;
@include background-image(linear-gradient(top, #5db4df 0%,#5db1dd 8%,#439bcb 50%,#3f99cd 54%,#207db6 96%,#1e7cba 100%));
.thumbnail {
display: inline-block;
vertical-align: middle;
padding: $grid-horizontal/2;
img {
max-height: $grid-horizontal*3;
}
}
.title {
display: inline-block;
vertical-align: middle;
background: #fff;
border: 1px solid lighten($color-medium-separator, 20%);
@include border-radius(5px);
margin-left: $grid-horizontal*2;
padding: $grid-horizontal/2;
}
.action-delete {
display: inline-block;
}
}
.details {
padding: $grid-horizontal*2;
}
}
}
/** --------------------------------------------
* Step labels
* -------------------------------------------- */

View File

@ -23,14 +23,12 @@
</div>
<% cached %>
<div id="cms-editor-dialogs">
<% control EditorToolbar %>
$MediaForm
$LinkForm
<% end_control %>
</div>
<% end_cached %>
<!-- <div class="ss-cms-bottom-bar">
<div class="holder">

View File

@ -2,7 +2,7 @@
.cms fieldset.ss-gridfield > div { margin-bottom: 35px; }
.cms fieldset.ss-gridfield[data-selectable] tr.ui-selected, .cms fieldset.ss-gridfield[data-selectable] tr.ui-selecting { background: #FFFAD6 !important; }
.cms fieldset.ss-gridfield[data-selectable] td { cursor: pointer; }
.cms table.ss-gridfield.field { box-shadow: none; padding: 0; margin: 20px 0 0 0; border-collapse: separate; border-bottom: 0 none; }
.cms table.ss-gridfield.field { display: table; box-shadow: none; padding: 0; margin: 20px 0 0 0; border-collapse: separate; border-bottom: 0 none; width: 100%; }
.cms table.ss-gridfield.field thead { color: #1d2224; background: transparent; }
.cms table.ss-gridfield.field tbody { background: #FFF; }
.cms table.ss-gridfield.field tbody td { /* Emulate a link by default */ }

View File

@ -224,8 +224,9 @@ class HtmlEditorField_Readonly extends ReadonlyField {
}
/**
* External toolbar for the HtmlEditorField.
* This is used by the CMS
* Toolbar shared by all instances of {@link HTMLEditorField}, to avoid too much markup duplication.
* Needs to be inserted manually into the template in order to function - see {@link LeftAndMain->EditorToolbar()}.
*
* @package forms
* @subpackage fields-formattedinput
*/
@ -234,9 +235,14 @@ class HtmlEditorField_Toolbar extends RequestHandler {
static $allowed_actions = array(
'LinkForm',
'MediaForm',
'browse',
'viewfile'
);
/**
* @var string
*/
protected $templateViewFile = 'HtmlEditorField_viewfile';
protected $controller, $name;
function __construct($controller, $name) {
@ -246,9 +252,6 @@ class HtmlEditorField_Toolbar extends RequestHandler {
Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ssui.core.js');
Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/behaviour/behaviour.js");
Requirements::javascript(SAPPHIRE_DIR . "/javascript/tiny_mce_improvements.js");
Requirements::javascript(SAPPHIRE_DIR ."/thirdparty/jquery-form/jquery.form.js");
Requirements::javascript(SAPPHIRE_DIR ."/javascript/HtmlEditorField.js");
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
@ -337,10 +340,6 @@ class HtmlEditorField_Toolbar extends RequestHandler {
* @return Form
*/
function MediaForm() {
if(!class_exists('ThumbnailStripField')) {
throw new Exception('ThumbnailStripField class required for HtmlEditorField->ImageForm()');
}
// TODO Handle through GridState within field - currently this state set too late to be useful here (during request handling)
$parentID = $this->controller->getRequest()->requestVar('ParentID');
@ -348,12 +347,15 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$fileFieldConfig->addComponent(new GridFieldSortableHeader());
$fileFieldConfig->addComponent(new GridFieldFilter());
$fileFieldConfig->addComponent(new GridFieldDefaultColumns());
$fileFieldConfig->addComponent(new GridFieldPaginator(10));
$fileField = new GridField('Files', false, false, $fileFieldConfig);
$fileFieldConfig->addComponent(new GridFieldPaginator(5));
$fileField = new GridField('Files', false, null, $fileFieldConfig);
$fileField->setList($this->getFiles($parentID));
$fileField->setAttribute('data-selectable', true);
$fileField->setAttribute('data-multiselect', true);
$fileField->setDisplayFields(array(
'CMSThumbnail' => false,
'Name' => _t('File.Name'),
));
$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span><strong class="title">%s</strong></span>';
$fields = new FieldList(
@ -363,33 +365,22 @@ class HtmlEditorField_Toolbar extends RequestHandler {
),
$contentComposite = new CompositeField(
new LiteralField('header1', '<h4 class="field">' . sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.Find', 'Find')) . '</h4>'),
new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder'),
$fileField,
new LiteralField('header2', '<h4 class="field edit-details">' . sprintf($numericLabelTmpl, '2', _t('HtmlEditorField.EditDetails', 'Edit details')) . '</h4>')
// new TextField('AltText', _t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image cannot be displayed'), '', 80),
// new TextField('ImageTitle', _t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')),
// new TextField('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
// new DropdownField(
// 'CSSClass',
// _t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
// array(
// 'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
// 'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
// 'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.'),
// 'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
// )
// ),
// new FieldGroup(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
// new TextField('Width', _t('HtmlEditorField.IMAGEWIDTHPX', 'Width'), 100),
// new TextField('Height', " x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'), 100)
// )
new LiteralField('headerSelect', '<h4 class="field header-select">' . sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.Find', 'Find')) . '</h4>'),
$selectComposite = new CompositeField(
new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder'),
$fileField
),
new LiteralField('headerEdit', '<h4 class="field header-edit">' . sprintf($numericLabelTmpl, '2', _t('HtmlEditorField.EditDetails', 'Edit details')) . '</h4>'),
$editComposite = new CompositeField(
new LiteralField('contentEdit', '<div class="content-edit"></div>')
)
)
);
$actions = new FieldList(
$insertAction = new FormAction('insertimage', _t('HtmlEditorField.BUTTONINSERTIMAGE', 'Insert image'))
$insertAction = new FormAction('insertimage', _t('HtmlEditorField.BUTTONINSERT', 'Insert'))
);
$insertAction->addExtraClass('ss-ui-action-constructive');
@ -401,20 +392,139 @@ class HtmlEditorField_Toolbar extends RequestHandler {
);
$contentComposite->addExtraClass('content');
// Allow other people to extend the fields being added to the imageform
$this->extend('updateMediaForm', $form);
$selectComposite->addExtraClass('content-select');
$form->unsetValidator();
$form->disableSecurityToken();
$form->loadDataFrom($this);
$form->addExtraClass('htmleditorfield-form htmleditorfield-mediaform cms-dialog-content');
// TODO Re-enable once we remove $.metadata dependency which currently breaks the JS due to $.ui.widget
// $form->setAttribute('data-urlViewfile', $this->controller->Link($this->name));
// Allow other people to extend the fields being added to the imageform
$this->extend('updateMediaForm', $form);
return $form;
}
public function browse($request) {
/**
* View of a single file, either on the filesystem or on the web.
*/
public function viewfile($request) {
// TODO Would be cleaner to consistently pass URL for both local and remote files,
// but GridField doesn't allow for this kind of metadata customization at the moment.
if($url = $request->getVar('FileURL')) {
if(Director::is_absolute_url($url)) {
$url = $url;
$file = null;
} else {
$url = Director::makeRelative($request->getVar('FileURL'));
$url = ereg_replace('_resampled/[^-]+-','',$url);
$file = DataList::create('File')->filter('Filename', $url)->first();
if(!$file) $file = new File(array('Title' => basename($url)));
}
} elseif($id = $request->getVar('ID')) {
$file = DataObject::get_by_id('File', $id);
$url = $file->RelativeLink();
} else {
throw new LogicException('Need either "ID" or "FileURL" parameter to identify the file');
}
// Instanciate file wrapper and get fields based on its type
if($file && $file->appCategory() == 'image') {
$fileWrapper = new HtmlEditorField_Image($url, $file);
} else {
$fileWrapper = new HtmlEditorField_File($url, $file);
}
$fields = $this->getFieldsForFile($url, $fileWrapper);
$this->extend('updateFieldsForFile', $fields, $url, $fileWrapper);
return $fileWrapper->customise(array(
'Fields' => $fields,
))->renderWith($this->templateViewFile);
}
/**
* Similar to {@link File->getCMSFields()}, but only returns fields
* for manipulating the instance of the file as inserted into the HTML content,
* not the "master record" in the database - hence there's no form or saving logic.
*
* @param String Relative or absolute URL to file
* @return FieldList
*/
protected function getFieldsForFile($url, $file) {
$fields = $this->extend('getFieldsForFile', $url, $file);
if(!$fields) {
if($file->Extension == 'swf') {
$fields = $this->getFieldsForFlash($url, $file);
} else {
$fields = $this->getFieldsForImage($url, $file);
}
$fields->push(new HiddenField('URL', false, $url));
}
$this->extend('updateFieldsForFile', $fields, $url, $file);
return $fields;
}
/**
* @return FieldList
*/
protected function getFieldsForFlash($url, $file) {
$fields = new FieldList(
$dimensionsField = new FieldGroup(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
$widthField = new TextField('Width', _t('HtmlEditorField.IMAGEWIDTHPX', 'Width'), $file->Width),
$heightField = new TextField('Height', " x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'), $file->Height)
)
);
$dimensionsField->addExtraClass('dimensions');
$widthField->setMaxLength(5);
$heightField->setMaxLength(5);
$this->extend('updateFieldsForFlash', $fields, $url, $file);
return $fields;
}
/**
* @return FieldList
*/
protected function getFieldsForImage($url, $file) {
$fields = new FieldList(
new TextField(
'AltText',
_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image cannot be displayed'),
$file->Title,
80
),
new TextField(
'Title',
_t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')
),
new TextField('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
new DropdownField(
'CSSClass',
_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
array(
'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.'),
'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
)
),
$dimensionsField = new FieldGroup(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
$widthField = new TextField('Width', _t('HtmlEditorField.IMAGEWIDTHPX', 'Width'), $file->Width),
$heightField = new TextField('Height', " x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'), $file->Height)
)
);
$dimensionsField->addExtraClass('dimensions');
$widthField->setMaxLength(5);
$heightField->setMaxLength(5);
$this->extend('updateFieldsForImage', $fields, $url, $file);
return $fields;
}
/**
@ -423,7 +533,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
*/
protected function getFiles($parentID = null) {
// TODO Use array('Filename:EndsWith' => $exts) once that's supported
$exts = array('jpg', 'gif', 'png', 'swf');
$exts = $this->getAllowedExtensions();
$wheres = array();
foreach($exts as $ext) $wheres[] = '"Filename" LIKE \'%.' . $ext . '\'';
@ -434,4 +544,117 @@ class HtmlEditorField_Toolbar extends RequestHandler {
return $files;
}
/**
* @return Array All extensions which can be handled by the different views.
*/
protected function getAllowedExtensions() {
$exts = array('jpg', 'gif', 'png', 'swf');
$this->extend('updateAllowedExtensions', $exts);
return $exts;
}
}
/**
* Encapsulation of a file which can either be a remote URL
* or a {@link File} on the local filesystem, exhibiting common properties
* such as file name or the URL.
*
* @todo Remove once core has support for remote files
*/
class HtmlEditorField_File extends ViewableData {
/** @var String */
protected $url;
/** @var File */
protected $file;
/**
* @param String
* @param File
*/
function __construct($url, $file = null) {
$this->url = $url;
$this->file = $file;
parent::__construct();
}
/**
* @return File Might not be set (for remote files)
*/
function getFile() {
return $this->file;
}
function getURL() {
return $this->url;
}
function getName() {
return ($this->file) ? $this->file->Name : preg_replace('/\?.*/', '', basename($this->url));
}
/**
* @return String HTML
*/
function getPreview() {
$preview = $this->extend('getPreview');
if($preview) return $preview;
if($this->file) {
return $this->file->CMSThumbnail();
} else {
// Hack to use the framework's built-in thumbnail support without creating a local file representation
$tmpFile = new File(array('Name' => $this->Name, 'Filename' => $this->Name));
return $tmpFile->CMSThumbnail();
}
}
function getExtension() {
return strtolower(($this->file) ? $this->file->Extension : pathinfo($this->Name, PATHINFO_EXTENSION));
}
function appCategory() {
if($this->file) {
return $this->file->appCategory();
} else {
// Hack to use the framework's built-in thumbnail support without creating a local file representation
$tmpFile = new File(array('Name' => $this->Name, 'Filename' => $this->Name));
return $tmpFile->appCategory();
}
}
}
class HtmlEditorField_Image extends HtmlEditorField_File {
protected $width;
protected $height;
function __construct($url, $file = null) {
parent::__construct($url, $file);
// Get dimensions for remote file
$info = @getimagesize($url);
if($info) {
$this->width = $info[0];
$this->height = $info[1];
}
}
function getWidth() {
return ($this->file) ? $this->file->Width : $this->width;
}
function getHeight() {
return ($this->file) ? $this->file->Height : $this->height;
}
function getPreview() {
return ($this->file) ? $this->file->CMSThumbnail() : sprintf('<img src="%s" />', $this->url);
}
}

View File

@ -352,7 +352,7 @@ class GridField extends FormField {
$this->getAttributes(),
array('value' => false, 'type' => false, 'name' => false)
);
$attrs['data-name'] = $this->Name();
$attrs['data-name'] = $this->getName();
$tableAttrs = array(
'id' => isset($this->id) ? $this->id : null,
'class' => "field CompositeField {$this->extraClass()}",
@ -502,7 +502,7 @@ class GridField extends FormField {
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
$html = '';
$data = $request->requestVars();
$fieldData = @$data[$this->Name()];
$fieldData = @$data[$this->getName()];
// Update state from client
$state = $this->getState(false);
@ -572,7 +572,7 @@ class GridField extends FormField {
$this->request = $request;
$this->setModel($model);
$fieldData = $this->request->requestVar($this->Name());
$fieldData = $this->request->requestVar($this->getName());
if($fieldData && $fieldData['GridState']) $this->getState(false)->setValue($fieldData['GridState']);
foreach($this->components as $component) {
@ -706,12 +706,12 @@ class GridField_Action extends FormAction {
* @return string HTML tag
*/
public function Field() {
Requirements::css('sapphire/css/GridField.css');
Requirements::css(SAPPHIRE_DIR . '/css/GridField.css');
Requirements::javascript(SAPPHIRE_DIR.'/thirdparty/jquery/jquery.js');
Requirements::javascript(SAPPHIRE_DIR.'/thirdparty/json-js/json2.js');
Requirements::javascript(SAPPHIRE_DIR.'/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript('sapphire/javascript/GridField.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/json-js/json2.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(SAPPHIRE_DIR . '/javascript/GridField.js');
// Store state in session, and pass ID to client side
$state = array(

View File

@ -39,7 +39,7 @@ class GridState extends HiddenField {
if ($value) $this->setValue($value);
parent::__construct($grid->Name() . '[GridState]');
parent::__construct($grid->getName() . '[GridState]');
}
/**

View File

@ -19,7 +19,10 @@
dataType: 'html',
success: function(data) {
// Replace the grid field with response, not the form.
self.replaceWith(data);
// TODO Only replaces all its children, to avoid replacing the current scope
// of the executing method. Means that it doesn't retrigger the onmatch() on the main container.
self.empty().append($(data).children());
form.removeClass('loading');
if(successCallback) successCallback.apply(this, arguments);
},

View File

@ -6,167 +6,248 @@
* ajax / iframe submissions
*/
(function($) {
$(document).ready(function() {
// jQuery('#Form_EditorToolbarLinkForm').dialog('open')
var ss = ss || {};
/**
* Wrapper for HTML WYSIWYG libraries, which abstracts library internals
* from interface concerns like inserting and editing links.
* Caution: Incomplete and unstable API.
*/
ss.editorWrappers = {};
ss.editorWrappers.tinyMCE = (function() {
var bookmark;
return {
/**
* On page refresh load the initial images (in root)
* @return Mixed Implementation specific object
*/
if($("#FolderImages").length > 0 && $("body.CMSMain").length > 0) loadImages(false);
getInstance: function() {
return tinyMCE.activeEditor;
},
/**
* On folder change - lookup the new images
* Invoked when a content-modifying UI is opened.
*/
$("#Form_EditorToolbarMediaForm_Files-0").change(function() {
$(".cms-editor-dialogs #Form_EditorToolbarMediaForm").ajaxForm({
url: 'admin/assets/UploadForm?action_doUpload=1',
iframe: true,
dataType: 'json',
beforeSubmit: function(data) {
$("#UploadFormResponse").text("Uploading File...").addClass("loading").show();
$("#Form_EditorToolbarMediaForm_Files-0").parents('.file').hide();
},
success: function(data) {
$("#UploadFormResponse").text("").removeClass("loading");
$("#Form_EditorToolbarMediaForm_Files-0").val("").parents('.file').show();
$("#FolderImages").html('<h2>'+ ss.i18n._t('HtmlEditorField.Loading', 'Loading') + '</h2>');
loadImages(data);
}
}).submit();
});
onopen: function() {
bookmark = this.getInstance().selection.getBookmark();
},
/**
* Loads images from getimages() to the thumbnail view. It's called on
* Invoked when a content-modifying UI is closed.
*/
function loadImages(params) {
$.get('admin/EditorToolbar/MediaForm', {
action_callfieldmethod: "1",
fieldName: "FolderImages",
ajax: "1",
methodName: "getimages",
folderID: $("#Form_EditorToolbarMediaForm_ParentID").val(),
searchText: $("#Form_EditorToolbarMediaForm_getimagesSearch").val(),
cacheKillerDate: parseInt((new Date()).getTime()),
cacheKillerRand: parseInt(10000 * Math.random())
},
function(data) {
$("#FolderImages").html(data);
$("#FolderImages").each(function() {
Behaviour.apply(this);
});
if(params) {
$("#FolderImages a[href*="+ params.Filename +"]").click();
}
});
}
});
onclose: function() {
bookmark = null;
},
/**
* Write the HTML back to the original text area field.
*/
save: function() {
tinyMCE.triggerSave();
},
/**
* Create a new instance based on a textarea field.
*
* @param String
* @param Object Implementation specific configuration
* @param Function
*/
create: function(domID, config, onSuccess) {
var ed = new tinymce.Editor(domID, config);
ed.onInit.add(onSuccess);
ed.render();
},
/**
* Redraw the editor contents
*/
repaint: function() {
tinyMCE.execCommand("mceRepaint");
},
/**
* @return boolean
*/
isDirty: function() {
return this.getInstance().isDirty();
},
/**
* HTML representation of the edited content.
*
* Returns: {String}
*/
getContent: function() {
return this.getInstance().getContent();
},
/**
* DOM tree of the edited content
*
* Returns: DOMElement
*/
getDOM: function() {
return this.getInstance().dom;
},
/**
* Returns: DOMElement
*/
getContainer: function() {
return this.getInstance().getContainer();
},
/**
* Get the closest node matching the current selection.
*
* Returns: {jQuery} DOMElement
*/
getSelectedNode: function() {
return this.getInstance().selection.getNode();
},
/**
* Select the given node within the editor DOM
*
* Parameters: {DOMElement}
*/
selectNode: function(node) {
this.getInstance().selection.select(node)
},
/**
* @param String HTML
*/
insertContent: function(html) {
// Workaround for IE losing focus
this.getInstance().selection.moveToBookmark(bookmark);
this.getInstance().execCommand('mceInsertContent', false, html);
},
/**
* Insert or update a link in the content area (based on current editor selection)
*
* Parameters: {Object} attrs
*/
insertLink: function(attrs) {
// Workaround for IE losing focus
this.getInstance().selection.moveToBookmark(bookmark);
this.getInstance().execCommand("mceInsertLink", false, attrs);
},
/**
* Remove the link from the currently selected node (if any).
*/
removeLink: function() {
this.getInstance().execCommand('unlink', false);
},
/**
* Strip any editor-specific notation from link in order to make it presentable in the UI.
*
* Parameters:
* {Object}
* {DOMElement}
*/
cleanLink: function(href, node) {
var cb = tinyMCE.settings['urlconverter_callback'];
if(cb) href = eval(cb + "(href, node, true);");
/**
* Wrapper for HTML WYSIWYG libraries, which abstracts library internals
* from interface concerns like inserting and editing links.
*/
var editorWrapper_TinyMCE = (function() {
var bookmark;
return {
getInstance: function() {
return tinyMCE.activeEditor;
},
/**
* Invoked when a content-modifying UI is opened.
*/
onopen: function() {
bookmark = this.getInstance().selection.getBookmark();
},
/**
* Invoked when a content-modifying UI is closed.
*/
onclose: function() {
bookmark = null;
},
/**
* HTML representation of the edited content.
*
* Returns: {String}
*/
getContent: function() {
return this.getInstance().getContent();
},
/**
* DOM tree of the edited content
*
* Returns: DOMElement
*/
getDOM: function() {
return this.getInstance().dom;
},
/**
* Get the closest node matching the current selection.
*
* Returns: {jQuery} DOMElement
*/
getSelectedNode: function() {
return this.getInstance().selection.getNode();
},
/**
* Select the given node within the editor DOM
*
* Parameters: {DOMElement}
*/
selectNode: function(node) {
this.getInstance().selection.select(node)
},
/**
* Insert or update a link in the content area (based on current editor selection)
*
* Parameters: {Object} attrs
*/
insertLink: function(attrs) {
// Workaround for IE losing focus
this.getInstance().selection.moveToBookmark(bookmark);
this.getInstance().execCommand("mceInsertLink", false, attrs);
},
/**
* Remove the link from the currently selected node (if any).
*/
removeLink: function() {
this.getInstance().execCommand('unlink', false);
},
/**
* Strip any editor-specific notation from link in order to make it presentable in the UI.
*
* Parameters:
* {Object}
* {DOMElement}
*/
cleanLink: function(href, node) {
href = eval(tinyMCE.settings['urlconverter_callback'] + "(href, node, true);");
// Turn into relative
if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) {
href = RegExp.$1;
}
// Get rid of TinyMCE's temporary URLs
if(href.match(/^javascript:\s*mctmp/)) href = '';
return href;
// Turn into relative
if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) {
href = RegExp.$1;
}
// Get rid of TinyMCE's temporary URLs
if(href.match(/^javascript:\s*mctmp/)) href = '';
return href;
}
});
}
});
// Override this to switch editor wrappers
ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
(function($) {
$.entwine('ss', function($) {
/**
* Class: textarea.htmleditor
*
* Add tinymce to HtmlEditorFields within the CMS. Works in combination
* with a TinyMCE.init() call which is prepopulated with the used HTMLEditorConfig settings,
* and included in the page as an inline <script> tag.
*/
$('textarea.htmleditor').entwine({
Editor: null,
/**
* Constructor: onmatch
*/
onmatch : function() {
var self = this, ed = ss.editorWrappers['default']();
this.setEditor(ed);
this.closest('form').bind('beforesave', function() {
// TinyMCE modifies input, so change tracking might get false
// positives when comparing string values - don't save if the editor doesn't think its dirty.
if(self.isChanged()) {
ed.save();
// TinyMCE assigns value attr directly, which doesn't trigger change event
self.trigger('change');
}
});
// Only works after TinyMCE.init() has been invoked, see $(window).bind() call below for details.
this.redraw();
this._super();
},
redraw: function() {
// Using a global config (generated through HTMLEditorConfig PHP logic)
var config = ssTinyMceConfig, self = this, ed = this.getEditor();
// Avoid flicker (also set in CSS to apply as early as possible)
self.css('visibility', '');
// Create editor instance and render it.
// Similar logic to adapter/jquery/jquery.tinymce.js, but doesn't rely on monkey-patching
// jQuery methods, and avoids replicate the script lazyloading which is already in place with jQuery.ondemand.
ed.create(this.attr('id'), config, function() {
self.css('visibility', 'visible');
});
// Handle editor de-registration by hooking into state changes.
// TODO Move to onunmatch for less coupling (once we figure out how to work with detached DOM nodes in TinyMCE)
$('.cms-container').bind('beforestatechange', function() {
self.css('visibility', 'hidden');
ed.getContainer();
if(ed) $(ed).remove();
});
this._super();
},
isChanged: function() {
var ed = this.getEditor();
return (ed && ed.isDirty());
},
resetChanged: function() {
var ed = this.getEditor();
if(typeof tinyMCE == 'undefined') return;
// TODO Abstraction layer
var inst = tinyMCE.getInstanceById(this.attr('id'));
if (inst) inst.startContent = tinymce.trim(inst.getContent({format : 'raw', no_events : 1}));
},
onunmatch: function() {
// TODO Throws exceptions in Firefox, most likely due to the element being removed from the DOM at this point
// var ed = tinyMCE.get(this.attr('id'));
// if(ed) ed.remove();
this._super();
}
});
/**
* Base form implementation for interactions with an editor instance,
* mostly geared towards modification and insertion of content.
*/
$('form.htmleditorfield-form').entwine({
// Wrapper for various HTML editors, defaults to editorWrapper_TinyMCE
// Wrapper for various HTML editors
Editor: null,
onmatch: function() {
@ -176,9 +257,9 @@
titleEl.remove();
// Create jQuery dialog
this.dialog({autoOpen: false, bgiframe: true, modal: true, height: 500, width: 500, ghost: true});
this.dialog({autoOpen: false, bgiframe: true, modal: true, height: 500, width: '80%', ghost: true});
this.setEditor(editorWrapper_TinyMCE());
this.setEditor(ss.editorWrappers['default']());
},
redraw: function() {
},
@ -191,20 +272,25 @@
this.getEditor().onclose();
},
open: function() {
this.dialog('open');
this.redraw();
this.getEditor().onopen();
}
});
$('form.htmleditorfield-linkform').entwine({
open: function() {
this.respondToNodeChange();
this.updateFromEditor();
this.dialog('open');
this.redraw();
this.getEditor().onopen();
},
/**
* Update the view state based on the current editor selection.
*/
updateFromEditor: function() {
}
});
/**
* Inserts and edits links in an html editor, including internal/external web links,
* links to files on the webserver, email addresses, and anchors in the existing html content.
* Every variation has its own fields (e.g. a "target" attribute doesn't make sense for an email link),
* which are toggled through a type dropdown. Variations share fields, so there's only one "title" field in the form.
*/
$('form.htmleditorfield-linkform').entwine({
close: function() {
this._super();
@ -291,7 +377,7 @@
// Add the new link
ed.insertLink(attributes);
this.trigger('onafterinsert', attributes);
this.respondToNodeChange();
this.updateFromEditor();
},
removeLink: function() {
@ -351,7 +437,7 @@
}
},
respondToNodeChange: function() {
updateFromEditor: function() {
var htmlTagPattern = /<\S[^><]*>/g, fieldName, data = this.getCurrentLink();
if(data) {
@ -466,16 +552,344 @@
}
});
/**
* Responsible for inserting media files, although only images are supported so far.
* Allows to select one or more files, and load form fields for each file via ajax.
* This allows us to tailor the form fields to the file type (e.g. different ones for images and flash),
* as well as add new form fields via framework extensions.
* The inputs on each of those files are used for constructing the HTML to insert into
* the rich text editor. Also allows editing the properties of existing files if any are selected in the editor.
* Note: Not each file has a representation on the webserver filesystem, supports insertion and editing
* of remove files as well.
*/
$('form.htmleditorfield-mediaform').entwine({
onsubmit: function() {
var self = this, ed = this.getEditor();
// HACK: See ondialogopen()
// if($.browser.msie) jQuery(ed.getContainer()).show();
this.find('.ss-htmleditorfield-file').each(function(el) {
ed.insertContent($(this).getHTML());
});
ed.repaint();
this.close();
return false;
},
ondialogopen: function() {
this.redraw();
var self = this, ed = this.getEditor(), node = $(ed.getSelectedNode());
// TODO Depends on managed mime type
if(node.is('img')) {
this.showFileView(node.attr('src'), function() {
$(this).updateFromNode(node);
self.redraw();
});
}
this.redraw();
// HACK: Hide selected node in IE because its drag handles on potentially selected elements
// don't respect the z-index of the dialog overlay.
// if($.browser.msie) jQuery(ed.getContainer()).hide();
},
ondialogclose: function() {
var ed = this.getEditor(), node = $(ed.getSelectedNode());
// HACK: See ondialogopen()
// if($.browser.msie) jQuery(ed.getContainer()).show();
this.find('.ss-htmleditorfield-file').remove(); // Remove any existing views
this.find('.ss-gridfield-items .ui-selected').removeClass('ui-selected'); // Unselect all items
this.redraw();
},
redraw: function() {
this._super();
var ed = this.getEditor(), node = $(ed.getSelectedNode()),
hasItems = Boolean(this.find('.ss-htmleditorfield-file').length),
editingSelected = node.is('img');
// Only show second step if files are selected
this.find('.header-edit')[(hasItems) ? 'show' : 'hide']();
// Disable "insert" button if no files are selected
this.find('.Actions :submit')
.button(hasItems ? 'enable' : 'disable')
.toggleClass('ui-state-disabled', !hasItems);
// Hide file selection and step labels when editing an existing file
this.find('.header-select,.content-select,.header-edit')[editingSelected ? 'hide' : 'show']();
},
getFileView: function(idOrUrl) {
return this.find('.ss-htmleditorfield-file[data-id=' + idOrUrl + ']');
},
showFileView: function(idOrUrl, successCallback) {
var self = this, params = (Number(idOrUrl) == idOrUrl) ? '?ID=' + idOrUrl : '?FileURL=' + idOrUrl,
item = $('<div class="ss-htmleditorfield-file" />');
item.addClass('loading');
this.find('.content-edit').append(item)
$.ajax({
// url: this.data('urlViewfile') + '?ID=' + id,
url: this.attr('action').replace(/MediaForm/, 'viewfile') + params,
success: function(html, status, xhr) {
var newItem = $(html);
item.replaceWith(newItem);
self.redraw();
if(successCallback) successCallback.call(newItem, html, status, xhr);
},
error: function() {
item.remove();
}
});
}
});
$('form.htmleditorfield-mediaform .ss-gridfield-items').entwine({
onselectableselected: function(e, ui) {
var form = this.closest('form'), item = $(ui.selected);
if(!item.is('.ss-gridfield-item')) return;
form.closest('form').showFileView(item.data('id'));
form.redraw();
},
onselectableunselected: function(e, ui) {
var form = this.closest('form'), item = $(ui.unselected);
if(!item.is('.ss-gridfield-item')) return;
form.getFileView(item.data('id')).remove();
form.redraw();
}
});
/**
* Represents a single selected file, together with a set of form fields to edit its properties.
* Overload this based on the media type to determine how the HTML should be created.
*/
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file').entwine({
/**
* @return {Object} Map of HTML attributes which can be set on the created DOM node.
*/
getAttributes: function() {
},
/**
* @return {Object} Map of additional properties which can be evaluated
* by the specific media type.
*/
getExtraData: function() {
},
/**
* @return {String} HTML suitable for insertion into the rich text editor
*/
getHTML: function() {
},
/**
* Updates the form values from an existing node in the editor.
*
* @param {DOMElement}
*/
updateFromNode: function(node) {
},
/**
* Transforms values set on the dimensions form fields based on two constraints:
* An aspect ration, and max width/height values. Writes back to the field properties as required.
*
* @param {String} The dimension to constrain the other value by, if any ("Width" or "Height")
* @param {Int} Optional max width
* @param {Int} Optional max height
*/
updateDimensions: function(constrainBy, maxW, maxH) {
var widthEl = this.find(':input[name=Width]'),
heightEl = this.find(':input[name=Height]'),
w = widthEl.val(),
h = heightEl.val(),
aspect;
// Proportionate updating of heights, using the original values
if(w && h) {
if(constrainBy) {
aspect = heightEl.getOrigVal() / widthEl.getOrigVal();
// Uses floor() and ceil() to avoid both fields constantly lowering each other's values in rounding situations
if(constrainBy == 'Width') {
if(maxW && w > maxW) w = maxW;
h = Math.floor(w * aspect);
} else if(constrainBy == 'Height') {
if(maxH && h > maxH) h = maxH;
w = Math.ceil(h / aspect);
}
} else {
if(maxW && w > maxW) w = maxW;
if(maxH && h > maxH) h = maxH;
}
widthEl.val(w);
heightEl.val(h);
}
}
});
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file.image').entwine({
getAttributes: function() {
var width = this.find(':input[name=Width]').val(),
height = this.find(':input[name=Height]').val();
return {
'src' : this.find(':input[name=URL]').val(),
'alt' : this.find(':input[name=AltText]').val(),
'width' : width ? parseInt(width, 10) : null,
'height' : height ? parseInt(height, 10) : null,
'title' : this.find(':input[name=Title]').val(),
'class' : this.find(':input[name=CSSClass]').val()
};
},
getExtraData: function() {
return {
'CaptionText': this.find(':input[name=CaptionText]').val()
};
},
getHTML: function() {
var el,
attrs = this.getAttributes(),
extraData = this.getExtraData(),
imgEl = $('<img id="__mce_tmp" />').attr(attrs);
if(extraData.CaptionText) {
el = $('<div style="width: ' + attrs['width'] + 'px;" class="captionImage ' + attrs['class'] + '"><p class="caption">' + extraData.CaptionText + '</p></div>').prepend(imgEl);
} else {
el = imgEl;
}
return $('<div />').append(el).html(); // Little hack to get outerHTML string
},
updateFromNode: function(node) {
this.find(':input[name=AltText]').val(node.attr('alt'));
this.find(':input[name=Title]').val(node.attr('title'));
this.find(':input[name=CSSClass]').val(node.attr('class')).attr('disabled', 'disabled');
this.find(':input[name=Width]').val(node.width());
this.find(':input[name=Height]').val(node.height());
this.find(':input[name=CaptionText]').val(node.siblings('.caption:first').text());
}
});
/**
* Insert a flash object tag into the content.
* Requires the 'media' plugin for serialization of tags into <img> placeholders.
*/
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file.flash').entwine({
getAttributes: function() {
var width = this.find(':input[name=Width]').val(),
height = this.find(':input[name=Height]').val();
return {
'src' : this.find(':input[name=URL]').val(),
'width' : width ? parseInt(width, 10) : null,
'height' : height ? parseInt(height, 10) : null,
};
},
getExtraData: function() {
return {
'CaptionText': this.find(':input[name=CaptionText]').val()
};
},
getHTML: function() {
var attrs = this.getAttributes();
// Emulate serialization from 'media' plugin
var el = tinyMCE.activeEditor.plugins.media.dataToImg({
'type': 'flash',
'width': attrs.width,
'height': attrs.height,
'params': {'src': attrs.src},
'video': {'sources': []}
});
return $('<div />').append(el).html(); // Little hack to get outerHTML string
},
updateFromNode: function(node) {
// TODO Not implemented
}
});
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file .dimensions :input').entwine({
OrigVal: null,
onmatch: function () {
this._super();
this.setOrigVal(parseInt(this.val(), 10));
// Default to a managable size for the HTML view. Can be overwritten by user after initialization
if(this.attr('name') == 'Width') this.closest('.ss-htmleditorfield-file').updateDimensions('Width', 600);
},
onfocusout: function(e) {
this.closest('.ss-htmleditorfield-file').updateDimensions(this.attr('name'));
}
});
/**
* Deselect item and remove the 'edit' view
*/
$('form.htmleditorfield-mediaform .ss-htmleditorfield-file .action-delete').entwine({
onclick: function(e) {
var form = this.closest('form'), file = this.closest('.ss-htmleditorfield-file');
form.find('.ss-gridfield-item[data-id=' + file.data('id') + ']').removeClass('ui-selected');
this.closest('.ss-htmleditorfield-file').remove();
form.redraw();
e.preventDefault();
}
});
$('form.htmleditorfield-mediaform #ParentID .TreeDropdownField').entwine({
onchange: function() {
var fileList = this.closest('form').find('fieldset.ss-gridfield');
fileList.setState('ParentID', this.getValue());
fileList.reload();
onmatch: function() {
this._super();
// TODO Custom event doesn't fire in IE if registered through object literal
var self = this;
this.bind('change', function() {
var fileList = self.closest('form').find('fieldset.ss-gridfield');
fileList.setState('ParentID', self.getValue());
fileList.reload();
});
}
});
});
})(jQuery);
})(jQuery);
/**
* These callback globals hook it into tinymce. They need to be referenced in the TinyMCE config.
*/
function sapphiremce_cleanup(type, value) {
if(type == 'get_from_editor') {
// replace indented text with a <blockquote>
value = value.replace(/<p [^>]*margin-left[^>]*>([^\n|\n\015|\015\n]*)<\/p>/ig,"<blockquote><p>$1</p></blockquote>");
// replace VML pixel image references with image tags - experimental
value = value.replace(/<[a-z0-9]+:imagedata[^>]+src="?([^> "]+)"?[^>]*>/ig,"<img src=\"$1\">");
// Word comments
value = value.replace(new RegExp('<(!--)([^>]*)(--)>', 'g'), "");
// kill class=mso??? and on mouse* tags
value = value.replace(/([ \f\r\t\n\'\"])class=mso[a-z0-9]+[^ >]+/ig, "$1");
value = value.replace(/([ \f\r\t\n\'\"]class=")mso[a-z0-9]+[^ ">]+ /ig, "$1");
value = value.replace(/([ \f\r\t\n\'\"])class="mso[a-z0-9]+[^">]+"/ig, "$1");
value = value.replace(/([ \f\r\t\n\'\"])on[a-z]+=[^ >]+/ig, "$1");
value = value.replace(/ >/ig, ">");
// remove everything that's in a closing tag
value = value.replace(/<(\/[A-Za-z0-9]+)[ \f\r\t\n]+[^>]*>/ig,"<$1>");
}
if(type == 'get_from_editor_dom') {
jQuery(value).find('img').each(function() {
this.onresizestart = null;
this.onresizeend = null;
this.removeAttribute('onresizestart');
this.removeAttribute('onresizeend');
});
}
return value;
}

View File

@ -1,402 +0,0 @@
ToolbarForm = Class.create();
ToolbarForm.prototype = {
toggle: function(ed) {
if(this.style.display == 'block') this.close(ed);
else this.open(ed);
},
close: function(ed) {
jQuery(this).dialog('close');
},
open: function(ed) {
jQuery(this).dialog('open');
},
onsubmit: function() {
return false;
}
}
SideFormAction = Class.create();
SideFormAction.prototype = {
initialize: function() {
this.parentForm = this.parentNode;
while(this.parentForm && this.parentForm.tagName.toLowerCase() != 'form') {
this.parentForm = this.parentForm.parentNode;
}
},
destroy: function() {
this.parentForm = null;
this.onclick = null;
},
onclick: function() {
if(this.parentForm['handle' + this.name]) {
try {
this.parentForm['handle' + this.name]();
} catch(er) {
alert("An error occurred. Please try again, or reload the CMS if the problem persists.\n\nError details: " + er.message);
}
jQuery(this).parents('form').dialog('close');
} else {
alert("Couldn't find form method handle" + this.name);
}
return false;
}
}
MediaForm = Class.extend('ToolbarForm');
MediaForm.prototype = {
initialize: function() {
var __form = this;
this.elements.AltText.onkeyup = function() { __form.update_params('AltText'); };
this.elements.ImageTitle.onkeyup = function() { __form.update_params('ImageTitle'); };
this.elements.CaptionText.onkeyup = function() { __form.update_params('CaptionText'); };
this.elements.AltText.onchange = function() { __form.update_params('AltText'); };
this.elements.Width.onchange = function() { __form.update_params('Width'); };
this.elements.Height.onchange = function() { __form.update_params('Height'); };
},
toggle: function(ed) {
this.ToolbarForm.toggle(ed);
this.resetFields();
},
resetFields: function() {
this.elements.AltText.value = '';
this.elements.ImageTitle.value = '';
this.elements.CSSClass.value = 'left';
this.elements.CaptionText.value = '';
this.elements.CaptionText.disabled = '';
this.elements.CSSClass.disabled = '';
},
destroy: function() {
this.ToolbarForm = null;
this.onsubmit = null;
this.elements.AltText.onkeyup = null;
this.elements.ImageTitle.onkeyup = null;
this.elements.CSSClass.onkeyup = null;
this.elements.CSSClass.onclick = null;
this.elements.Width.onchange = null;
this.elements.Height.onchange = null;
},
update_params: function(updatedFieldName) {
var ed = tinyMCE.activeEditor;
var imgElement = ed.selection.getNode();
if (!imgElement || imgElement.tagName != 'IMG') {
imgElement = this.selectedNode;
}
if(imgElement && imgElement.tagName == 'IMG') {
imgElement.alt = this.elements.AltText.value;
imgElement.title = this.elements.ImageTitle.value;
imgElement.className = this.elements.CSSClass.value;
var captionElement = imgElement.nextSibling;
if (captionElement && captionElement.tagName == 'P') {
if (typeof(captionElement.textContent) != 'undefined') {
captionElement.textContent = this.elements.CaptionText.value;
} else {
captionElement.innerText = this.elements.CaptionText.value;
}
}
// Proportionate updating of heights
if(updatedFieldName == 'Width') {
imgElement.width = this.elements.Width.value;
imgElement.removeAttribute('height');
this.elements.Height.value = imgElement.height;
} else if(updatedFieldName == 'Height') {
imgElement.height = this.elements.Height.value;
imgElement.removeAttribute('width');
this.elements.Width.value = imgElement.width;
}
} else if (this.selectedImageWidth && this.selectedImageHeight) {
// Proportionate updating of heights
var w = this.elements.Width.value, h = this.elements.Height.value;
var aspect = this.selectedImageHeight / this.selectedImageWidth;
if(updatedFieldName == 'Width') {
this.elements.Height.value = Math.floor(w * aspect);
} else if(updatedFieldName == 'Height') {
this.elements.Width.value = Math.floor(h / aspect);
}
}
},
respondToNodeChange: function(ed) {
var imgElement = ed.selection.getNode();
if(imgElement && imgElement.tagName == 'IMG') {
this.selectedNode = imgElement;
this.elements.AltText.value = imgElement.alt;
var captionElement = imgElement.nextSibling;
if (captionElement && captionElement.tagName == 'P') {
this.elements.CaptionText.value = captionElement.innerText || captionElement.textContent;
} else {
this.elements.CaptionText.disabled = 'disabled';
}
this.elements.ImageTitle.value = imgElement.title;
this.elements.CSSClass.value = imgElement.className;
this.elements.CSSClass.disabled = 'disabled';
this.elements.Width.value = imgElement.style.width ? parseInt(imgElement.style.width) : imgElement.width;
this.elements.Height.value = imgElement.style.height ? parseInt(imgElement.style.height) : imgElement.height;
} else {
this.selectedNode = null;
}
},
selectImage: function(image) {
if(this.selectedImage) {
this.selectedImage.setAttribute("class", "");
this.selectedImage.className = "";
}
this.selectedImage = image;
this.selectedImage.setAttribute("class", "selectedImage");
this.selectedImage.className = "selectedImage";
try {
var imgTag = image.getElementsByTagName('img')[0];
this.selectedImageWidth = $('Form_EditorToolbarMediaForm_Width').value = imgTag.className.match(/destwidth=([0-9.\-]+)([, ]|$)/) ? RegExp.$1 : null;
this.selectedImageHeight = $('Form_EditorToolbarMediaForm_Height').value = imgTag.className.match(/destheight=([0-9.\-]+)([, ]|$)/) ? RegExp.$1 : null;
} catch(er) {
}
},
handleaction_insertimage: function() {
if(this.selectedImage) {
this.selectedImage.insert();
}
}
}
ImageThumbnail = Class.create();
ImageThumbnail.prototype = {
destroy: function() {
this.onclick = null;
},
onclick: function(e) {
$('Form_EditorToolbarMediaForm').selectImage(this);
return false;
},
insert: function() {
var formObj = $('Form_EditorToolbarMediaForm');
var altText = formObj.elements.AltText.value;
var titleText = formObj.elements.ImageTitle.value;
var cssClass = formObj.elements.CSSClass.value;
var baseURL = document.getElementsByTagName('base')[0].href;
var relativeHref = this.href.substr(baseURL.length);
var captionText = formObj.elements.CaptionText.value;
if(!tinyMCE.selectedInstance) tinyMCE.selectedInstance = tinyMCE.activeEditor;
if(tinyMCE.selectedInstance.contentWindow.focus) tinyMCE.selectedInstance.contentWindow.focus();
var data = {
'src' : relativeHref,
'alt' : altText,
'width' : $('Form_EditorToolbarMediaForm_Width').value,
'height' : $('Form_EditorToolbarMediaForm_Height').value,
'title' : titleText,
'class' : cssClass
};
this.ssInsertImage(tinyMCE.activeEditor, data, captionText);
jQuery(formObj).trigger('onafterinsert', data);
return false;
},
/**
* Insert an image with the given attributes
*/
ssInsertImage: function(ed, attributes, captionText) {
el = ed.selection.getNode();
var html;
if(captionText) {
html = '<div style="width: ' + attributes.width + 'px;" class="captionImage ' + attributes['class'] + '">';
html += '<img id="__mce_tmp" />';
html += '<p class="caption">' + captionText + '</p>';
html += '</div>';
} else {
html = '<img id="__mce_tmp" />';
}
if(el && el.nodeName == 'IMG') {
ed.dom.setAttribs(el, attributes);
} else {
ed.execCommand('mceInsertContent', false, html, {
skip_undo : 1
});
ed.dom.setAttribs('__mce_tmp', attributes);
ed.dom.setAttrib('__mce_tmp', 'id', '');
ed.undoManager.add();
}
}
}
var selectedimage = false;
function reselectImage(transport) {
if(selectedimage) {
links = $('Image').getElementsByTagName('a');
for(i =0; link = links[i]; i++) {
var quesmark = link.href.lastIndexOf('?');
image = link.href.substring(0, quesmark);
if(image == selectedimage) {
link.className = 'selectedImage';
$('Form_EditorToolbarMediaForm').selectedImage = link;
break;
}
}
}
$('Image').reapplyBehaviour();
this.addToTinyMCE = this.addToTinyMCE.bind(this);
}
FlashForm = Class.extend('ToolbarForm');
FlashForm.prototype = {
initialize: function() {
},
destroy: function() {
this.ToolbarForm = null;
this.onsubmit = null;
},
update_params: function(event) {
if(tinyMCE.imgElement) {
}
},
respondToNodeChange: function() {
if(tinyMCE.imgElement) {
} else {
}
},
selectFlash: function(flash) {
if(this.selectedFlash) {
this.selectedFlash.setAttribute("class", "");
}
this.selectedFlash = flash;
this.selectedFlash.setAttribute("class", "selectedFlash");
},
handleaction_insertflash: function() {
if(this.selectedFlash) {
this.selectedFlash.insert();
}
}
}
FlashThumbnail = Class.create();
FlashThumbnail.prototype = {
destroy: function() {
this.onclick = null;
},
onclick: function(e) {
$('Form_EditorToolbarFlashForm').selectFlash(this);
return false;
},
insert: function() {
var formObj = $('Form_EditorToolbarFlashForm');
var html = '';
var baseURL = document.getElementsByTagName('base')[0].href;
var relativeHref = this.href.substr(baseURL.length)
var width = formObj.elements.Width.value;
var height = formObj.elements.Height.value;
if(!tinyMCE.selectedInstance) tinyMCE.selectedInstance = tinyMCE.activeEditor;
if(tinyMCE.selectedInstance.contentWindow.focus) tinyMCE.selectedInstance.contentWindow.focus();
if (width == "") width = 100;
if (height == "") height = 100;
html = '';
html += '<object width="' + width +'" height="' + height +'" type="application/x-shockwave-flash" data="'+ relativeHref +'">';
html += '<param value="'+ relativeHref +'" name="movie">';
html += '</object>';
tinyMCE.selectedInstance.execCommand("mceInsertContent", false, html);
tinyMCE.selectedInstance.execCommand('mceRepaint');
// ed.execCommand('mceInsertContent', false, html, {skip_undo : 1});
jQuery(formObj).trigger('onafterinsert', {html: html, href: relativeHref, width: width, height: height});
return false;
}
}
MediaForm.applyTo('#Form_EditorToolbarMediaForm');
ImageThumbnail.applyTo('#Form_EditorToolbarMediaForm div.thumbnailstrip a');
SideFormAction.applyTo('#Form_EditorToolbarMediaForm .Actions input');
FlashForm.applyTo('#Form_EditorToolbarFlashForm');
FlashThumbnail.applyTo('#Form_EditorToolbarFlashForm div.thumbnailstrip a');
SideFormAction.applyTo('#Form_EditorToolbarFlashForm .Actions input');
/**
* These callback hook it into tinymce. They need to be referenced in the TinyMCE config.
*/
function sapphiremce_setupcontent(editor_id, body, doc) {
var allImages = body.getElementsByTagName('img');
var i,img;
for(i=0;img=allImages[i];i++) {
behaveAs(img, MCEImageResizer);
}
var allDLs = body.getElementsByTagName('img');
for(i=0;img=allDLs[i];i++) {
if(img.className.match(/(^|\b)specialImage($|\b)/)) {
behaveAs(img, MCEDLResizer);
}
}
}
function sapphiremce_cleanup(type, value) {
if(type == 'get_from_editor') {
// replace indented text with a <blockquote>
value = value.replace(/<p [^>]*margin-left[^>]*>([^\n|\n\015|\015\n]*)<\/p>/ig,"<blockquote><p>$1</p></blockquote>");
// replace VML pixel image references with image tags - experimental
value = value.replace(/<[a-z0-9]+:imagedata[^>]+src="?([^> "]+)"?[^>]*>/ig,"<img src=\"$1\">");
// Word comments
value = value.replace(new RegExp('<(!--)([^>]*)(--)>', 'g'), "");
// kill class=mso??? and on mouse* tags
value = value.replace(/([ \f\r\t\n\'\"])class=mso[a-z0-9]+[^ >]+/ig, "$1");
value = value.replace(/([ \f\r\t\n\'\"]class=")mso[a-z0-9]+[^ ">]+ /ig, "$1");
value = value.replace(/([ \f\r\t\n\'\"])class="mso[a-z0-9]+[^">]+"/ig, "$1");
value = value.replace(/([ \f\r\t\n\'\"])on[a-z]+=[^ >]+/ig, "$1");
value = value.replace(/ >/ig, ">");
// remove everything that's in a closing tag
value = value.replace(/<(\/[A-Za-z0-9]+)[ \f\r\t\n]+[^>]*>/ig,"<$1>");
}
if(type == 'get_from_editor_dom') {
var allImages =value.getElementsByTagName('img');
var i,img;
for(i=0;img=allImages[i];i++) {
img.onresizestart = null;
img.onresizeend = null;
img.removeAttribute('onresizestart');
img.removeAttribute('onresizeend');
}
var allDLs =value.getElementsByTagName('img');
for(i=0;img=allDLs[i];i++) {
if(img.className.match(/(^|\b)specialImage($|\b)/)) {
img.onresizestart = null;
img.onresizeend = null;
img.removeAttribute('onresizestart');
img.removeAttribute('onresizeend');
}
}
}
return value;
}

View File

@ -48,11 +48,13 @@ $gf_border_radius: 7px;
}
table.ss-gridfield.field {
display: table;
box-shadow: none;
padding: 0;
margin: 20px 0 0 0;
border-collapse: separate;
border-bottom: 0 none;
width: 100%;
thead {
color: darken($gf_colour_base, 50%);

View File

@ -0,0 +1,14 @@
<div class="ss-htmleditorfield-file $appCategory" data-id="$File.ID" data-url="$URL">
<div class="overview">
<span class="thumbnail">$Preview</span>
<span class="title">$Name</span>
<a href="#" class="action-delete"><% _t('HtmlEditorField.DeleteItem', 'delete') %></a>
</div>
<div class="details">
<fieldset>
<% control Fields %>
$FieldHolder
<% end_control %>
</fieldset>
</div>
</div>

View File

@ -76,19 +76,20 @@ class HtmlEditorFieldTest extends FunctionalTest {
);
}
public function testExtendMediaFormFields() {
if(class_exists('ThumbnailStripField')) {
$controller = new Controller();
public function testHtmlEditorFieldFileLocal() {
$file = new HtmlEditorField_File('http://domain.com/folder/my_image.jpg?foo=bar');
$this->assertEquals('http://domain.com/folder/my_image.jpg?foo=bar', $file->URL);
$this->assertEquals('my_image.jpg', $file->Name);
$this->assertEquals('jpg', $file->Extension);
// TODO Can't easily test remote file dimensions
}
$toolbar = new HtmlEditorField_Toolbar($controller, 'DummyToolbar');
$form = $toolbar->MediaForm();
$this->assertTrue(HtmlEditorFieldTest_DummyMediaFormFieldExtension::$update_called);
$this->assertEquals($form->Fields(), HtmlEditorFieldTest_DummyMediaFormFieldExtension::$fields);
} else {
$this->markTestSkipped('Test requires cms module (ThumbnailStripfield class)');
}
public function testHtmlEditorFieldFileRemote() {
$fileFixture = new File(array('Name' => 'my_local_image.jpg', 'Filename' => 'folder/my_local_image.jpg'));
$file = new HtmlEditorField_File('http://localdomain.com/folder/my_local_image.jpg', $fileFixture);
$this->assertEquals('http://localdomain.com/folder/my_local_image.jpg', $file->URL);
$this->assertEquals('my_local_image.jpg', $file->Name);
$this->assertEquals('jpg', $file->Extension);
}
}

View File

@ -1,10 +1,12 @@
File:
example_file:
Name: example.pdf
Filename: folder/subfolder/example.pdf
Image:
example_image:
Name: example.jpg
Filename: folder/subfolder/example.jpg
HtmlEditorFieldTest_Object:
home: