tags, which are then converted with JavaScript.
*
* @package forms
* @subpackage fields-formattedinput
*/
class HtmlEditorField extends TextareaField {
/**
* @config
* @var Boolean Use TinyMCE's GZIP compressor
*/
private static $use_gzip = true;
/**
* @config
* @var Integer Default insertion width for Images and Media
*/
private static $insert_width = 600;
/**
* @config
* @var bool Should we check the valid_elements (& extended_valid_elements) rules from HtmlEditorConfig server side?
*/
private static $sanitise_server_side = false;
/**
* @config
* @var array
*/
private static $casting = array(
'Value' => 'HTMLText',
);
protected $rows = 30;
/**
* Includes the JavaScript neccesary for this field to work using the {@link Requirements} system.
*/
public static function include_js() {
require_once 'tinymce/tiny_mce_gzip.php';
$configObj = HtmlEditorConfig::get_active();
if(Config::inst()->get('HtmlEditorField', 'use_gzip')) {
$internalPlugins = array();
foreach($configObj->getPlugins() as $plugin => $path) if(!$path) $internalPlugins[] = $plugin;
$tag = TinyMCE_Compressor::renderTag(array(
'url' => THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php',
'plugins' => implode(',', $internalPlugins),
'themes' => 'advanced',
'languages' => $configObj->getOption('language')
), true);
preg_match('/src="([^"]*)"/', $tag, $matches);
Requirements::javascript(html_entity_decode($matches[1]));
} else {
Requirements::javascript(MCE_ROOT . 'tiny_mce_src.js');
}
Requirements::customScript($configObj->generateJS(), 'htmlEditorConfig');
}
/**
* @see TextareaField::__construct()
*/
public function __construct($name, $title = null, $value = '') {
parent::__construct($name, $title, $value);
self::include_js();
}
/**
* @return string
*/
public function Field($properties = array()) {
// mark up broken links
$value = Injector::inst()->create('HTMLValue', $this->value);
if($links = $value->getElementsByTagName('a')) foreach($links as $link) {
$matches = array();
if(preg_match('/\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
if(!DataObject::get_by_id('SiteTree', $matches[1])) {
$class = $link->getAttribute('class');
$link->setAttribute('class', ($class ? "$class ss-broken" : 'ss-broken'));
}
}
if(preg_match('/\[file_link(?:\s*|%20|,)?id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
if(!DataObject::get_by_id('File', $matches[1])) {
$class = $link->getAttribute('class');
$link->setAttribute('class', ($class ? "$class ss-broken" : 'ss-broken'));
}
}
}
$properties['Value'] = htmlentities($value->getContent(), ENT_COMPAT, 'UTF-8');
return parent::Field($properties);
}
public function getAttributes() {
return array_merge(
parent::getAttributes(),
array(
'tinymce' => 'true',
'style' => 'width: 97%; height: ' . ($this->rows * 16) . 'px', // prevents horizontal scrollbars
'value' => null,
)
);
}
public function saveInto(DataObjectInterface $record) {
if($record->hasField($this->name) && $record->escapeTypeForField($this->name) != 'xml') {
throw new Exception (
'HtmlEditorField->saveInto(): This field should save into a HTMLText or HTMLVarchar field.'
);
}
$htmlValue = Injector::inst()->create('HTMLValue', $this->value);
// Sanitise if requested
if($this->config()->sanitise_server_side) {
$santiser = Injector::inst()->create('HtmlEditorSanitiser', HtmlEditorConfig::get_active());
$santiser->sanitise($htmlValue);
}
// Resample images and add default attributes
if($images = $htmlValue->getElementsByTagName('img')) foreach($images as $img) {
// strip any ?r=n data from the src attribute
$img->setAttribute('src', preg_replace('/([^\?]*)\?r=[0-9]+$/i', '$1', $img->getAttribute('src')));
// Resample the images if the width & height have changed.
if($image = File::find(urldecode(Director::makeRelative($img->getAttribute('src'))))){
$width = $img->getAttribute('width');
$height = $img->getAttribute('height');
if($width && $height && ($width != $image->getWidth() || $height != $image->getHeight())) {
//Make sure that the resized image actually returns an image:
$resized=$image->ResizedImage($width, $height);
if($resized) $img->setAttribute('src', $resized->getRelativePath());
}
}
// Add default empty title & alt attributes.
if(!$img->getAttribute('alt')) $img->setAttribute('alt', '');
if(!$img->getAttribute('title')) $img->setAttribute('title', '');
// Use this extension point to manipulate images inserted using TinyMCE, e.g. add a CSS class, change default title
// $image is the image, $img is the DOM model
$this->extend('processImage', $image, $img);
}
// optionally manipulate the HTML after a TinyMCE edit and prior to a save
$this->extend('processHTML', $htmlValue);
// Store into record
$record->{$this->name} = $htmlValue->getContent();
}
/**
* @return HtmlEditorField_Readonly
*/
public function performReadonlyTransformation() {
$field = $this->castedCopy('HtmlEditorField_Readonly');
$field->dontEscape = true;
return $field;
}
public function performDisabledTransformation() {
return $this->performReadonlyTransformation();
}
}
/**
* Readonly version of an {@link HTMLEditorField}.
* @package forms
* @subpackage fields-formattedinput
*/
class HtmlEditorField_Readonly extends ReadonlyField {
public function Field($properties = array()) {
$valforInput = $this->value ? Convert::raw2att($this->value) : "";
return "id() . "\">"
. ( $this->value && $this->value != '' ? $this->value : '(not set)' )
. "name."\" value=\"".$valforInput."\" />";
}
public function Type() {
return 'htmleditorfield readonly';
}
}
/**
* 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
*/
class HtmlEditorField_Toolbar extends RequestHandler {
private static $allowed_actions = array(
'LinkForm',
'MediaForm',
'viewfile',
'getanchors'
);
/**
* @var string
*/
protected $templateViewFile = 'HtmlEditorField_viewfile';
protected $controller, $name;
public function __construct($controller, $name) {
parent::__construct();
Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/jquery/jquery.js");
Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
Requirements::javascript(FRAMEWORK_DIR ."/javascript/HtmlEditorField.js");
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
$this->controller = $controller;
$this->name = $name;
}
public function forTemplate() {
return sprintf(
'
',
Controller::join_links($this->controller->Link(), $this->name, 'LinkForm', 'forTemplate'),
Controller::join_links($this->controller->Link(), $this->name, 'MediaForm', 'forTemplate')
);
}
/**
* Searches the SiteTree for display in the dropdown
*
* @return callback
*/
public function siteTreeSearchCallback($sourceObject, $labelField, $search) {
return DataObject::get($sourceObject, "\"MenuTitle\" LIKE '%$search%' OR \"Title\" LIKE '%$search%'");
}
/**
* Return a {@link Form} instance allowing a user to
* add links in the TinyMCE content editor.
*
* @return Form
*/
public function LinkForm() {
$siteTree = new TreeDropdownField('internal', _t('HtmlEditorField.PAGE', "Page"),
'SiteTree', 'ID', 'MenuTitle', true);
// mimic the SiteTree::getMenuTitle(), which is bypassed when the search is performed
$siteTree->setSearchFunction(array($this, 'siteTreeSearchCallback'));
$numericLabelTmpl = '%d'
. '%s';
$form = new Form(
$this->controller,
"{$this->name}/LinkForm",
new FieldList(
$headerWrap = new CompositeField(
new LiteralField(
'Heading',
sprintf('
%s
',
_t('HtmlEditorField.LINK', 'Insert Link'))
)
),
$contentComposite = new CompositeField(
new OptionsetField(
'LinkType',
sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.LINKTO', 'Link to')),
array(
'internal' => _t('HtmlEditorField.LINKINTERNAL', 'Page on the site'),
'external' => _t('HtmlEditorField.LINKEXTERNAL', 'Another website'),
'anchor' => _t('HtmlEditorField.LINKANCHOR', 'Anchor on this page'),
'email' => _t('HtmlEditorField.LINKEMAIL', 'Email address'),
'file' => _t('HtmlEditorField.LINKFILE', 'Download a file'),
),
'internal'
),
new LiteralField('Step2',
'
'
),
$siteTree,
new TextField('external', _t('HtmlEditorField.URL', 'URL'), 'http://'),
new EmailField('email', _t('HtmlEditorField.EMAIL', 'Email address')),
new TreeDropdownField('file', _t('HtmlEditorField.FILE', 'File'), 'File', 'ID', 'Title', true),
new TextField('Anchor', _t('HtmlEditorField.ANCHORVALUE', 'Anchor')),
new TextField('Description', _t('HtmlEditorField.LINKDESCR', 'Link description')),
new CheckboxField('TargetBlank',
_t('HtmlEditorField.LINKOPENNEWWIN', 'Open link in a new window?')),
new HiddenField('Locale', null, $this->controller->Locale)
)
),
new FieldList(
ResetFormAction::create('remove', _t('HtmlEditorField.BUTTONREMOVELINK', 'Remove link'))
->addExtraClass('ss-ui-action-destructive')
->setUseButtonTag(true)
,
FormAction::create('insert', _t('HtmlEditorField.BUTTONINSERTLINK', 'Insert link'))
->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept')
->setUseButtonTag(true)
)
);
$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
$contentComposite->addExtraClass('ss-insert-link content');
$form->unsetValidator();
$form->loadDataFrom($this);
$form->addExtraClass('htmleditorfield-form htmleditorfield-linkform cms-dialog-content');
$this->extend('updateLinkForm', $form);
return $form;
}
/**
* Get the folder ID to filter files by for the "from cms" tab
*
* @return int
*/
protected function getAttachParentID() {
$parentID = $this->controller->getRequest()->requestVar('ParentID');
$this->extend('updateAttachParentID', $parentID);
return $parentID;
}
/**
* Return a {@link Form} instance allowing a user to
* add images and flash objects to the TinyMCE content editor.
*
* @return Form
*/
public function MediaForm() {
// TODO Handle through GridState within field - currently this state set too late to be useful here (during
// request handling)
$parentID = $this->getAttachParentID();
$fileFieldConfig = GridFieldConfig::create()->addComponents(
new GridFieldFilterHeader(),
new GridFieldSortableHeader(),
new GridFieldDataColumns(),
new GridFieldPaginator(5),
// TODO Shouldn't allow delete here, its too confusing with a "remove from editor view" action.
// Remove once we can fit the search button in the last actual title column
new GridFieldDeleteAction(),
new GridFieldDetailForm()
);
$fileField = new GridField('Files', false, null, $fileFieldConfig);
$fileField->setList($this->getFiles($parentID));
$fileField->setAttribute('data-selectable', true);
$fileField->setAttribute('data-multiselect', true);
$columns = $fileField->getConfig()->getComponentByType('GridFieldDataColumns');
$columns->setDisplayFields(array(
'CMSThumbnail' => false,
'Name' => _t('File.Name'),
));
$numericLabelTmpl = '%d'
. '%s';
$fromCMS = new CompositeField(
new LiteralField('headerSelect',
'
'.sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.FindInFolder', 'Find in Folder')).'
'),
$select = TreeDropdownField::create('ParentID', "", 'Folder')
->addExtraClass('noborder')
->setValue($parentID),
$fileField
);
$fromCMS->addExtraClass('content ss-uploadfield');
$select->addExtraClass('content-select');
$fromWeb = new CompositeField(
new LiteralField('headerURL',
'
'),
$editComposite = new CompositeField(
new LiteralField('contentEdit', '')
)
);
$allFields->addExtraClass('ss-insert-media');
$headings = new CompositeField(
new LiteralField(
'Heading',
sprintf('