mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
2c5d71dc29
commit
5220a46fd0
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -183,88 +183,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$('.cms-container').loadPanel(this.attr('href'), '', {selector: '.cms-edit-form'});
|
||||
|
@ -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
|
||||
* -------------------------------------------- */
|
||||
|
@ -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">
|
||||
|
@ -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 */ }
|
||||
|
@ -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('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>')
|
||||
)
|
||||
|
||||
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)
|
||||
// )
|
||||
)
|
||||
);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
@ -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(
|
||||
|
@ -39,7 +39,7 @@ class GridState extends HiddenField {
|
||||
|
||||
if ($value) $this->setValue($value);
|
||||
|
||||
parent::__construct($grid->Name() . '[GridState]');
|
||||
parent::__construct($grid->getName() . '[GridState]');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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);
|
||||
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);");
|
||||
|
||||
$("#FolderImages").each(function() {
|
||||
Behaviour.apply(this);
|
||||
});
|
||||
|
||||
if(params) {
|
||||
$("#FolderImages a[href*="+ params.Filename +"]").click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
402
javascript/tiny_mce_improvements.js
vendored
402
javascript/tiny_mce_improvements.js
vendored
@ -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;
|
||||
}
|
@ -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%);
|
||||
|
14
templates/Includes/HtmlEditorField_viewfile.ss
Normal file
14
templates/Includes/HtmlEditorField_viewfile.ss
Normal 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>
|
@ -76,19 +76,20 @@ class HtmlEditorFieldTest extends FunctionalTest {
|
||||
);
|
||||
}
|
||||
|
||||
public function testExtendMediaFormFields() {
|
||||
if(class_exists('ThumbnailStripField')) {
|
||||
$controller = new Controller();
|
||||
|
||||
$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 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
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user