praiadeseselle/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module
2022-03-08 15:55:41 +01:00

1348 lines
42 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* Class InputfieldImage
*
* Inputfield for FieldtypeImage fields
*
*
* Accessible Properties
*
* @property string $extensions Space separated list of allowed image extensions (default="JPG JPEG GIF PNG")
* @property array $okExtensions Array of manually whitelisted extensions, for instance [ 'SVG' ] must be manually whitelisted if allowed. (default=[])
* @property int|string $maxWidth Max width for uploaded images, larger will be sized down (default='')
* @property int|string $maxHeight Max height for uploaded images, larger will be sized down (default='')
* @property float $maxSize Maximum number of megapixels for client-side resize, i.e. 1.7 is ~1600x1000, alt. to maxWidth/maxHeight (default=0).
* @property bool|int $maxReject Reject images that exceed max allowed size? (default=false)
* @property int|string $minWidth Min width for uploaded images, smaller will be refused (default='')
* @property int|string $minHeight Min height for uploaded images, smaller will be refused (default='')
* @property bool|int $dimensionsByAspectRatio Switch min-/maxWidth and min-/maxHeight restriction for portrait images
* @property string $itemClass Space separated CSS classes for items rendered by this Inputfield. Generally you should append rather than replace.
* @property int|bool $useImageEditor Whether or not the modal image editor is allowed for this field (default=true)
* @property int $adminThumbScale for backwards compatibility only
* @property int|bool $resizeServer Resize to max width/height at server? 1=Server-only, 0=Use client-side resize when possible (default=0).
* @property int $clientQuality Quality setting to use for client-side resize. 60=60%, 90=90%, etc. (default=90).
*
* The following properties default values are pulled from $config->adminThumbOptions and can be overridden
* by setting directly to an instance of this Inputfield:
*
* @property int $gridSize squared size of the admin thumbnails (default=130)
* @property string $gridMode Default grid mode in admin, one of "grid", "left" or "list" (default="grid")
* @property string $focusMode May be 'on', 'off', or 'zoom'
* @property array $imageSizerOptions Options to pass along to the ImageSizer class. See /wire/config.php $imageSizerOptions for details.
*
*
* Hookable Methods
*
* @method string render()
* @method string renderItem(Pageimage $pagefile, $id, $n)
* @method string renderList(Pageimages $value)
* @method string renderUpload(Pageimages $value)
* @method string renderSingleItem(Pageimage $pagefile, $id, $n)
* @method string renderButtons(Pageimage $pagefile, $id, $n)
* @method string renderAdditionalFields(Pageimage $pagefile, $id, $n)
* @method array buildTooltipData(Pageimage $pagefile)
* @method array getFileActions(Pagefile $pagefile)
* @method bool|null processUnknownFileAction(Pageimage $pagefile, $action, $label)
*
*
*/
class InputfieldImage extends InputfieldFile implements InputfieldItemList, InputfieldHasSortableValue {
public static function getModuleInfo() {
return array(
'title' => __('Images', __FILE__), // Module Title
'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
'version' => 124,
'permanent' => true,
);
}
/**
* Default square grid item size
*
*/
const defaultGridSize = 130;
/**
* Force render value mode for dev/debug purposes
*
*/
const debugRenderValue = false;
/**
* Cached list of all image variations
*
* @var array
*
*/
protected $variations = array();
/**
* Class used for modal editor windows
*
* @var string
*
*/
protected $modalClass = 'pw-modal-large';
public function init() {
parent::init();
$this->set('extensions', 'JPG JPEG GIF PNG');
$this->set('maxWidth', '');
$this->set('maxHeight', '');
$this->set('maxSize', 0.0);
$this->set('maxReject', 0);
$this->set('minWidth', '');
$this->set('minHeight', '');
$this->set('resizeServer', 0); // 0=allow client resize, 1=resize at server only
$this->set('clientQuality', 90);
$this->set('dimensionsByAspectRatio', 0);
$this->set('itemClass', 'gridImage ui-widget');
$options = $this->wire('config')->adminThumbOptions;
if(!is_array($options)) $options = array();
$gridSize = empty($options['gridSize']) ? self::defaultGridSize : (int) $options['gridSize'];
if($gridSize < 100) $gridSize = self::defaultGridSize; // establish min of 100
if($gridSize >= (self::defaultGridSize * 2)) $gridSize = self::defaultGridSize; // establish max of 259
$this->set('gridSize', $gridSize);
$this->set('gridMode', 'grid'); // one of "grid", "left" or "list"
$this->set('focusMode', 'on'); // One of "on", "zoom" or "off"
// adminThumbScale is no longer in use (here in case descending module using it)
$this->set('adminThumbScale', empty($options['scale']) ? 1.0 : (float) $options['scale']);
if(empty($options['imageSizerOptions'])) {
// properties specified in $options rather than $options['imageSizerOptions'], so we copy them
$options['imageSizerOptions'] = array();
foreach($options as $key => $value) {
if($key == 'height' || $key == 'width' || $key == 'scale' || $key == 'gridSize') continue;
$options['imageSizerOptions'][$key] = $value;
}
}
$this->set('imageSizerOptions', empty($options['imageSizerOptions']) ? array() : $options['imageSizerOptions']);
$this->set('useImageEditor', 1);
$this->labels = array_merge($this->labels, array(
'crop' => $this->_('Crop'),
'focus' => $this->_('Focus'),
'variations' => $this->_('Variations'),
'dimensions' => $this->_('Dimensions'),
'filesize' => $this->_('Filesize'),
'edit' => $this->_('Edit'),
'drag-drop-in' => $this->_('drag and drop in new images above'),
'na' => $this->_('N/A'), // for JS
'changes' => $this->_('This images field may have unsaved changes that could be lost after this action. Please save before cropping, or double-click the button proceed anyway.'),
));
$themeDefaults = array(
// 'error' => "<span class='ui-state-error-text'>{out}</span>", // provided by InputfieldFile
'buttonClass' => "ui-button ui-corner-all ui-state-default",
'buttonText' => "<span class='ui-button-text'>{out}</span>",
'selectClass' => '',
);
$themeSettings = $this->wire('config')->InputfieldImage;
$themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults;
$this->themeSettings = array_merge($this->themeSettings, $themeSettings);
}
public function get($key) {
if($key == 'themeSettings') return $this->themeSettings;
return parent::get($key);
}
/**
* Called right before Inputfield render
*
* @param Inputfield $parent Parent Inputfield
* @param bool $renderValueMode Whether or not we are in renderValue mode
* @return bool
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
if(self::debugRenderValue) {
// force render value mode for dev/debugging purposes
$renderValueMode = true;
$this->renderValueMode = true;
$this->addClass('InputfieldRenderValueMode', 'wrapClass');
}
$config = $this->wire('config');
$modules = $this->wire('modules');
$jqueryCore = $modules->get('JqueryCore');
$jqueryCore->use('simulate');
$jqueryCore->use('cookie');
$modules->loadModuleFileAssets('InputfieldFile');
$modules->getInstall("JqueryMagnific");
if(!$renderValueMode && $this->focusMode == 'zoom') {
$this->addClass('InputfieldImageFocusZoom', 'wrapClass');
}
$settings = $config->get('InputfieldImage');
if(!is_array($settings)) $settings = array();
if(empty($settings['ready'])) {
$settings['labels'] = $this->labels;
$settings['ready'] = true;
$config->js('InputfieldImage', $settings);
}
// client side image resize
if(!$this->resizeServer && ($this->maxWidth || $this->maxHeight || $this->maxSize)) {
$thisURL = $config->urls->InputfieldImage;
$jsExt = $config->debug ? "js" : "min.js";
$config->scripts->add($thisURL . "exif.$jsExt");
$config->scripts->add($thisURL . "PWImageResizer.$jsExt");
$maxSize = str_replace(',', '.', $this->maxSize);
$quality = str_replace(',', '.', (float) ($this->clientQuality / 100));
$this->wrapAttr('data-resize', "$this->maxWidth;$this->maxHeight;$maxSize;$quality");
}
if(!$renderValueMode && $this->value instanceof Pageimages) {
$page = $this->getRootHasPage();
if($page->id && $this->wire('user')->hasPermission('page-edit-images', $page)) {
$modules->get('JqueryUI')->use('modal');
} else {
$this->useImageEditor = 0;
}
}
if($this->value instanceof Pageimages) $this->variations = $this->value->getAllVariations();
return parent::renderReady($parent, $renderValueMode);
}
/**
* Render Inputfield
*
* @return string
*
*/
public function ___render() {
if($this->isAjax) clearstatcache();
$out = parent::___render();
return $out;
}
/**
* Render list of images
*
* @param Pageimages|array $value
* @return string
* @throws WireException
*
*/
protected function ___renderList($value) {
//if(!$value) return '';
$out = '';
$n = 0;
$this->renderListReady($value);
if(!$this->uploadOnlyMode && WireArray::iterable($value)) {
foreach($value as $k => $pagefile) {
$id = $this->pagefileId($pagefile);
$this->currentItem = $pagefile;
$out .= $this->renderItemWrap($this->renderItem($pagefile, $id, $n++));
/*
if($this->maxFiles != 1) {
$out .= $this->renderItemWrap($this->renderItem($pagefile, $id, $n++));
} else {
$out .= $this->renderSingleItem($pagefile, $id, $n++);
}
*/
}
if(!$this->renderValueMode) {
$dropNew = $this->wire('sanitizer')->entities1($this->_('drop in new image file to replace'));
$focus = $this->wire('sanitizer')->entities1($this->_('drag circle to center of focus'));
$out .= "
<div class='InputfieldImageEdit'>
<div class='InputfieldImageEdit__inner'>
<div class='InputfieldImageEdit__arrow'></div>
<div class='InputfieldImageEdit__close'><span class='fa fa-times'></span></div>
<div class='InputfieldImageEdit__imagewrapper'>
<div>
<img class='InputfieldImageEdit__image' src='' alt=''>
<small class='detail detail-upload'>$dropNew</small>
<small class='detail detail-focus'>$focus</small>
</div>
</div>
<div class='InputfieldImageEdit__edit'></div>
</div>
</div>
";
}
}
$class = 'gridImages ui-helper-clearfix';
if($this->uploadOnlyMode) $class .= " InputfieldImageUploadOnly";
if($this->overwrite && !$this->renderValueMode) $class .= " InputfieldFileOverwrite";
$out = "<ul class='$class' data-gridSize='$this->gridSize' data-gridMode='$this->gridMode'>$out</ul>";
$out = "<ul class='InputfieldImageErrors'></ul>$out";
return $out;
}
protected function renderItemWrap($out) {
$item = $this->currentItem;
$id = $item && !$this->renderValueMode ? " id='file_$item->hash'" : "";
return "<li$id class='ImageOuter {$this->itemClass}'>$out</li>";
}
protected function ___renderUpload($value) {
if($this->noUpload || $this->renderValueMode) return '';
// enables user to choose more than one file
if($this->maxFiles != 1) $this->setAttribute('multiple', 'multiple');
$attrs = $this->getAttributes();
unset($attrs['value']);
if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
$attrStr = $this->getAttributesString($attrs);
$extensions = $this->getAllowedExtensions();
$formatExtensions = $this->formatExtensions($extensions);
$chooseLabel = $this->labels['choose-file'];
$out =
"<div " .
"data-maxfilesize='{$this->maxFilesize}' " .
"data-extensions='{$extensions}' " .
"data-fieldname='$attrs[name]' " .
"class='InputfieldImageUpload'" .
">";
$out .= "
<div class='InputMask ui-button ui-state-default'>
<span class='ui-button-text'>
<i class='fa fa-fw fa-folder-open-o'></i>$chooseLabel
</span>
<input $attrStr>
</div>
<span class='InputfieldImageValidExtensions detail'>$formatExtensions</span>
<input type='hidden' class='InputfieldImageMaxFiles' value='{$this->maxFiles}' />
";
if(!$this->noAjax) {
$dropLabel = $this->uploadOnlyMode ? $this->labels['drag-drop'] : $this->labels['drag-drop-in'];
// $refreshLabel = $this->('legacy thumbnails will be re-created on save');
$out .= "
<span class='AjaxUploadDropHere description'>
<span>
<i class='fa fa-cloud-upload'></i>&nbsp;$dropLabel
</span>
</span>";
}
if($this->get('_hasLegacyThumbs')) {
$label = $this->_('There are older/low quality thumbnail preview images above check this box to re-create them.');
$out .= "
<p class='InputfieldImageRefresh detail'>
<label>
<input type='checkbox' name='_refresh_thumbnails_$this->name' value='1' />
$label
</label>
</p>
";
}
$out .= "</div>";
return $out;
}
/**
* Resize images to max width/height if specified in field config and image is larger than max
*
* #pw-hooker
*
* @param Pagefile $pagefile
* @throws WireException
*
*/
protected function ___fileAdded(Pagefile $pagefile) {
/** @var Pageimage $pagefile */
if($pagefile->ext() === 'svg') {
parent::___fileAdded($pagefile);
return;
}
$pagefile2 = null;
if(!$pagefile->width) {
$pagefile->unlink();
throw new WireException($this->_('Invalid image'));
}
$minWidth = $this->minWidth;
$minHeight = $this->minHeight;
if($this->dimensionsByAspectRatio && $pagefile->width < $pagefile->height){
$minWidth = $this->minHeight;
$minHeight = $this->minWidth;
}
if(
($minWidth && $pagefile->width < $minWidth) ||
($minHeight && $pagefile->height < $minHeight)
) {
$actualDimensions = $pagefile->width . 'x' . $pagefile->height;
$requiredDimensions = $minWidth . 'x' . $minHeight;
throw new WireException(
sprintf($this->_('Image of %s does not meet minimum size requirements'), $actualDimensions) . " ($requiredDimensions)"
);
}
$maxWidth = $this->maxWidth;
$maxHeight = $this->maxHeight;
if($this->dimensionsByAspectRatio && $pagefile->width < $pagefile->height){
$maxWidth = $this->maxHeight;
$maxHeight = $this->maxWidth;
}
if(
($maxWidth && $pagefile->width > $maxWidth) ||
($maxHeight && $pagefile->height > $maxHeight)
) {
if($this->maxReject) {
$actualDimensions = $pagefile->width . '×' . $pagefile->height;
$requiredDimensions = $maxWidth . '×' . $maxHeight;
throw new WireException(
sprintf($this->_('Image of %s exceeds maximum allowed size'), $actualDimensions) . " ($requiredDimensions)"
);
}
$pagefile2 = $pagefile->size($maxWidth, $maxHeight, array('cropping' => false));
if($pagefile->filename != $pagefile2->filename) {
$this->wire('files')->unlink($pagefile->filename);
$this->wire('files')->rename($pagefile2->filename, $pagefile->filename);
}
$pagefile->getImageInfo(true); // force it to reload its dimensions
}
if($pagefile2) {
$this->message($this->_("Image resized to fit maximum allowed dimensions") . " ({$maxWidth}x{$maxHeight}");
}
parent::___fileAdded($pagefile);
}
protected function fileAddedGetMarkup(Pagefile $pagefile, $n) {
/** @var Pageimage $pagefile */
/*
$markup = $this->maxFiles == 1
? $this->renderSingleItem($pagefile, $this->pagefileId($pagefile), $n)
: $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n));
*/
$markup = $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n));
return $markup;
}
/**
* Get thumbnail image info
*
* @param Pageimage $img Image to get thumb for
* @param bool $useSizeAttributes Whether width and or height size attributes should be included in the <img> tag
* @param bool $remove Specify true to remove legacy thumbnail file
*
* @return array of(
* 'thumb' => Pageimage object,
* 'attr' => associative array of image attributes
* 'markup' => string of markup for <img>,
* 'amarkup' => same as above but wrapped in <a> tag
* 'error' => error message if applicable
* 'title' => potential title attribute for <a> tag with image info
* );
*
*/
public function getAdminThumb(Pageimage $img, $useSizeAttributes = true, $remove = false) {
$thumb = $img;
$error = '';
$attr = array();
$_thumbHeight = $thumb->height;
$thumbHeight = $_thumbHeight;
$_thumbWidth = $thumb->width;
$thumbWidth = $_thumbWidth;
$useResize = ($img->ext == 'svg' && $thumbHeight == '100%')
|| ($this->gridSize && $thumbHeight > $this->gridSize)
|| ($this->gridSize && $thumbWidth > $this->gridSize);
if($useResize) {
$imageSizerOptions = $this->imageSizerOptions;
$imageSizerOptions['upscaling'] = true;
$imageSizerOptions['focus'] = false; // disable focus since we show focus from JS/CSS in admin thumbs
$adminThumbOptions = $this->wire('config')->adminThumbOptions;
$gridSize2x = $this->gridSize * 2;
// if($adminThumbOptions['scale'] === 1.0) $gridSize2x = $this->gridSize; // force non-HiDPI
// check if there is an existing thumbnail using pre-gridSize legacy settings
$h = (int) $adminThumbOptions['height'];
$w = (int) $adminThumbOptions['width'];
$f = $img->pagefiles->path() . basename($img->basename(), '.' . $img->ext()) . ".{$w}x{$h}." . $img->ext();
$exists = is_file($f);
if($exists && $remove) {
$this->wire('files')->unlink($f);
$exists = false;
}
if($exists) {
// use existing legacy thumbnail (upscaled in browser to gridSize)
$thumb = $thumb->size($w, $h, $imageSizerOptions);
if($thumbWidth > $thumbHeight) {
$thumbHeight = $this->gridSize;
$thumbWidth = 0;
} else if($thumbWidth < $thumbHeight) {
$thumbWidth = $this->gridSize;
$thumbHeight = 0;
} else {
$thumbWidth = $this->gridSize;
$thumbHeight = $this->gridSize;
}
$this->set('_hasLegacyThumbs', true);
} else {
// use new thumbnail size, 260px (scaled to 130px in output)
if($thumbWidth >= $thumbHeight) {
if($thumbHeight > $gridSize2x) {
$thumb = $thumb->height($gridSize2x, $imageSizerOptions);
$thumbHeight = $this->gridSize;
$thumbWidth = 0;
}
} else if($thumbWidth > $gridSize2x) {
$thumb = $thumb->width($gridSize2x, $imageSizerOptions);
$thumbWidth = $this->gridSize;
$thumbHeight = 0;
}
}
if($thumb->error) $error = $thumb->error;
}
if($useSizeAttributes) {
if($thumb->get('_requireHeight')) {
// _requireHeight set by InputfieldPageEditImageSelect
if(!$thumbHeight || $thumbHeight > $this->gridSize) $thumbHeight = $this->gridSize;
$attr['height'] = $thumbHeight;
} else if($thumbHeight && $thumbWidth) {
$attr['width'] = $thumbWidth;
$attr['height'] = $thumbHeight;
} else if($thumbHeight) {
if(!$thumbHeight) $thumbHeight = $this->gridSize;
$attr['height'] = $thumbHeight;
} else if($thumbWidth) {
$attr['width'] = $thumbWidth;
}
}
$attr['src'] = $thumb->URL;
$attr['alt'] = $this->wire('sanitizer')->entities1($img->description);
$attr['data-w'] = $_thumbWidth;
$attr['data-h'] = $_thumbHeight;
$attr["data-original"] = $img->URL;
$focus = $img->focus();
$attr['data-focus'] = $focus['str'];
$markup = "<img ";
foreach($attr as $key => $value) $markup .= "$key=\"$value\" ";
$markup .= " />";
$title = $img->basename() . " ({$img->width}x{$img->height}) $img->filesizeStr";
if($attr['alt']) $title .= ": $attr[alt]";
$amarkup = "<a href='$img->url' title='$title'>$markup</a>";
$a = array(
'thumb' => $thumb,
'attr' => $attr,
'markup' => $markup,
'amarkup' => $amarkup,
'error' => $error,
'title' => $title,
);
return $a;
}
/**
* Get Pagefile to pull description and tags from
*
* @param Pagefile $pagefile
* @return Pageimage|Pagefile
*
*/
protected function getMetaPagefile(Pagefile $pagefile) {
if(!$this->isAjax || !isset($_SERVER['HTTP_X_REPLACENAME'])) return $pagefile;
$metaFilename = $_SERVER['HTTP_X_REPLACENAME'];
if(strpos($metaFilename, '?')) list($metaFilename,) = explode('?', $metaFilename);
$metaFilename = $this->wire('sanitizer')->name($metaFilename);
$metaPagefile = $this->attr('value')->get($metaFilename);
if(!$metaPagefile instanceof Pagefile) $metaPagefile = $pagefile;
return $metaPagefile;
}
/**
* Render a Pageimage item
*
* @param Pagefile|Pageimage $pagefile
* @param string $id
* @param int $n
*
* @return string
*
*/
protected function ___renderItem($pagefile, $id, $n) {
$sanitizer = $this->wire('sanitizer');
$thumb = $this->getAdminThumb($pagefile, false);
$fileStats = str_replace(' ', '&nbsp;', $pagefile->filesizeStr) . ", {$pagefile->width}&times;{$pagefile->height} ";
foreach($pagefile->extras() as $name => $extra) {
if($extra->exists()) $fileStats .= " &bull; $extra->filesizeStr $name ($extra->savingsPct)";
}
// $gridSize = $this->gridSize;
// <div class='gridImage__overflow' style='width: {$gridSize}px; height: {$gridSize}px'>
$out = $this->getTooltip($pagefile) . "
<div class='gridImage__overflow'>
$thumb[markup]
</div>
";
if(!$this->isEditableInRendering($pagefile)) return $out;
if($this->uploadOnlyMode) {
$out .= "
<div class='ImageData'>
<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />
</div>
";
} else {
$buttons = $pagefile->ext() == 'svg' ? '' : $this->renderButtons($pagefile, $id, $n);
$metaPagefile = $this->getMetaPagefile($pagefile);
$description = $this->renderItemDescriptionField($metaPagefile, $id, $n);
$additional = $this->renderAdditionalFields($metaPagefile, $id, $n);
$actions = $this->renderFileActionSelect($metaPagefile, $id);
$error = '';
if($thumb['error']) {
$error = str_replace('{out}', $sanitizer->entities($thumb['error']), $this->themeSettings['error']);
}
$labels = $this->labels;
$out .= "
<div class='gridImage__hover'>
<div class='gridImage__inner'>
<label for='' class='gridImage__trash'>
<input class='gridImage__deletebox' type='checkbox' name='delete_$id' value='1' title='$labels[delete]' />
<span class='fa fa-trash-o'></span>
</label>
<a class='gridImage__edit'>
<span>$labels[edit]</span>
</a>
</div>
</div>
";
$ext = $pagefile->ext();
$basename = $pagefile->basename(false);
$focus = $pagefile->focus();
$inputfields = $this->getItemInputfields($pagefile);
if($inputfields) $additional .= $inputfields->render();
$out .= "
<div class='ImageData'>
<h2 class='InputfieldImageEdit__name'><span contenteditable='true'>$basename</span>.$ext</h2>
<span class='InputfieldImageEdit__info'>$fileStats</span>
<div class='InputfieldImageEdit__errors'>$error</div>
<div class='InputfieldImageEdit__buttons'><small>$buttons</small> $actions</div>
<div class='InputfieldImageEdit__core'>$description</div>
<div class='InputfieldImageEdit__additional'>$additional</div>
<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />
<input class='InputfieldFileReplace' type='hidden' name='replace_$id' />
<input class='InputfieldFileRename' type='hidden' name='rename_$id' />
<input class='InputfieldImageFocus' type='hidden' name='focus_$id' value='$focus[str]' />
</div>
";
}
return $out;
}
/**
* Render a Pageimage item
*
* @deprecated No longer used by core. Left for a little while longer in case any extending module uses it.
* @param Pagefile|Pageimage $pagefile
* @param string $id
* @param int $n
* @return string
*
*/
protected function ___renderSingleItem($pagefile, $id, $n) {
$editable = $this->isEditableInRendering($pagefile);
$fileStats = str_replace(' ', '&nbsp;', $pagefile->filesizeStr) . ", {$pagefile->width}&times;{$pagefile->height} ";
$description = $this->wire('sanitizer')->entities($pagefile->description);
$deleteLabel = $this->labels['delete'];
if($editable) {
$buttons = $this->renderButtons($pagefile, $id, $n);
$metaPagefile = $this->getMetaPagefile($pagefile);
$descriptionField = $this->renderItemDescriptionField($metaPagefile, $id, $n);
$additional = $this->renderAdditionalFields($metaPagefile, $id, $n);
$editableOut = "
<div class='InputfieldImageEdit__buttons'>$buttons</div>
<div class='InputfieldImageEdit__core'>$descriptionField</div>
<div class='InputfieldImageEdit__additional'>$additional</div>
<input class='InputfieldFileSort' type='hidden' name='sort_$id' value='$n' />
<input class='InputfieldFileReplace' type='hidden' name='replace_$id' />
<input class='InputfieldFileRename' type='hidden' name='rename_$id' />
";
} else {
$editableOut = '';
//$editableOut = "<p>" . $this->_("Not editable.") . "</p>";
}
$trashOut = '';
if($editable && !$this->renderValueMode) $trashOut = "
<div class='InputfieldImageEdit__trash-single'>
<label for='' class='gridImage__trash gridImage__trash--single'>
<input class='gridImage__deletebox' type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />
<span class='fa fa-trash-o'></span>
</label>
</div>
";
$out = "
<div class='ImageOuter InputfieldImageEdit InputfieldImageEditSingle' id='file_$pagefile->hash'>
<div class='InputfieldImageEdit__inner'>
$trashOut
<div class='InputfieldImageEdit__imagewrapper'>
<div>
<img class='InputfieldImageEdit__image' src='$pagefile->URL' alt='$description'>
</div>
</div>
<div class='InputfieldImageEdit__edit'>
<h2 class='InputfieldImageEdit__name'>$pagefile->name</h2>
<span class='InputfieldImageEdit__info'>$fileStats</span>
$editableOut
</div>
</div>
</div>
";
return $out;
}
/**
* Render buttons for image edit mode
*
* #pw-hooker
*
* @param Pagefile|Pageimage $pagefile
* @param string $id
* @param int $n
* @return string
*
*/
protected function ___renderButtons($pagefile, $id, $n) {
if(!$this->useImageEditor) return '';
if($n) {} // ignore, $n is for hooks
$pageID = $pagefile->pagefiles->page->id;
$variationCount = $pagefile->variations()->count();
// if($pagefile->webp()->exists()) $variationCount++;
$editUrl = $this->getEditUrl($pagefile, $pageID);
$variationUrl = $this->getVariationUrl($pagefile, $id);
$buttonClass = $this->themeSettings['buttonClass'];
$modalButtonClass = trim("$buttonClass $this->modalClass pw-modal");
$modalAttrs = "data-buttons='#non_rte_dialog_buttons button' data-autoclose='1' data-close='#non_rte_cancel'";
$labels = $this->labels;
$out = '';
// Crop
$buttonText = str_replace('{out}', "<i class='fa fa-crop'></i> $labels[crop]", $this->themeSettings['buttonText']);
$out .= "<button type='button' data-href='$editUrl' class='InputfieldImageButtonCrop $modalButtonClass' $modalAttrs>$buttonText</button>";
// Focus
if($this->focusMode && $this->focusMode != 'off') {
$iconA = $pagefile->hasFocus ? 'fa-check-circle-o' : 'fa-circle-o';
$iconB = $pagefile->hasFocus ? 'fa-check-circle' : 'fa-dot-circle-o';
$buttonText = str_replace('{out}', "<i class='fa $iconA' data-toggle='$iconA $iconB'></i> $labels[focus]", $this->themeSettings['buttonText']);
$out .= "<button type='button' class='InputfieldImageButtonFocus $buttonClass'>$buttonText</button>";
}
// Variations
$buttonText = "<i class='fa fa-files-o'></i> $labels[variations] <span class='ui-priority-secondary'>($variationCount)</span>";
$buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']);
$out .= "<button type='button' data-href='$variationUrl' class='$modalButtonClass' data-buttons='button'>$buttonText</button>";
return $out;
}
/**
* Render an image action select for given Pageimage
*
* @param Pagefile $pagefile
* @param string $id
* @return string
*
*/
protected function renderFileActionSelect(Pagefile $pagefile, $id) {
if(!$this->useImageEditor) return '';
static $hooked = null;
if($hooked === null) $hooked =
$this->wire('hooks')->isHooked('InputfieldImage::getFileActions()') ||
$this->wire('hooks')->isHooked('InputfieldFile::getFileActions()');
$actions = $hooked ? $this->getFileActions($pagefile) : $this->___getFileActions($pagefile);
if(empty($actions)) return '';
$selectClass = trim($this->themeSettings['selectClass'] . ' InputfieldFileActionSelect');
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
$out =
"<select class='$selectClass' name='act_$id'>" .
"<option value=''>" . $this->_('Actions') . "</option>";
foreach($actions as $name => $label) {
$out .= "<option value='$name'>" . $sanitizer->entities1($label) . "</option>";
}
$out .= "</select> ";
$out .= "<span class='InputfieldFileActionNote detail'>" . $this->_('Action applied at save.') . "</span>";
return $out;
}
/**
* Get array of actions available for given Pagefile
*
* @param Pagefile|Pageimage $pagefile
* @return array Associative array of ('action_name' => 'Action Label')
*
*/
public function ___getFileActions(Pagefile $pagefile) {
static $labels = null;
static $hasIMagick = null;
if($hasIMagick === null) {
$hasIMagick = $this->wire('modules')->isInstalled('ImageSizerEngineIMagick');
}
if($labels === null) $labels = array(
'flip' => $this->_('Flip'),
'rotate' => $this->_('Rotate'),
'dup' => $this->_('Duplicate'),
'rmv' => $this->_('Remove variations'),
'rbv' => $this->_('Rebuild variations'),
'rmf' => $this->_('Remove focus'),
'vertical' => $this->_('vert'),
'horizontal' => $this->_('horiz'),
'both' => $this->_('both'),
'cop' => $this->_('Copy'),
'pas' => $this->_('Paste'),
'x50' => $this->_('Reduce 50%'),
'bw' => $this->_('B&W'), // Black and White
'sep' => $this->_('Sepia'),
);
$actions = array(
'dup' => $labels['dup'],
);
if($this->maxFiles && count($pagefile->pagefiles) >= $this->maxFiles) {
unset($actions['dup']);
}
if($pagefile->ext() != 'svg') {
// $actions['rmv'] = $labels['rmv'];
// $actions['rbv'] = $labels['rbv'];
$actions['fv'] = "$labels[flip] $labels[vertical]";
$actions['fh'] = "$labels[flip] $labels[horizontal]";
$actions['fb'] = "$labels[flip] $labels[both]";
foreach(array(90, 180, 270, -90, -180, -270) as $degrees) {
$actions["r$degrees"] = "$labels[rotate] {$degrees}°";
}
if($hasIMagick) {
$actions['x50'] = $labels['x50'];
}
$actions['bw'] = $labels['bw'];
$actions['sep'] = $labels['sep'];
if($pagefile->hasFocus) {
$actions['rmf'] = $labels['rmf'];
}
}
return $actions;
}
/**
* Render non-editable value
*
* @return string
*
public function ___renderValue() {
$value = $this->value;
if(!$value instanceof Pageimages) return '';
$out = '';
foreach($value as $img) {
$info = $this->getAdminThumb($img);
$out .= $info['amarkup'];
}
return $out;
}
*/
/**
* Render any additional fields (for hooks)
*
* #pw-hooker
*
* @param Pageimage|Pagefile $pagefile
* @param string $id
* @param int $n
*
*/
protected function ___renderAdditionalFields($pagefile, $id, $n) { }
/*
protected function ___renderClipboard() {
$clipboard = $this->wire('session')->getFor('Pagefiles', 'clipboard');
if(!is_array($clipboard)) return '';
foreach($clipboard as $key) {
list($type, $pageID, $fieldName, $file) = explode(':', $key);
$page = $this->wire('pages')->get((int) $pageID);
$field = $this->wire('fields')->get($fieldName);
}
}
*/
/**
* Template method: allow items to be collapsed? Override default from InputfieldFile
*
* @return bool
*
*/
protected function allowCollapsedItems() {
return false;
}
/**
* Configure field
*
* @return InputfieldWrapper
*
*/
public function ___getConfigInputfields() {
$inputfields = parent::___getConfigInputfields();
require_once(__DIR__ . '/config.php');
$configuration = new InputfieldImageConfiguration();
$this->wire($configuration);
$configuration->getConfigInputfields($this, $inputfields);
return $inputfields;
}
/**
* Is the given image editable during rendering?
*
* @param Pagefile|Pageimage $pagefile
* @return bool|int
*
*/
protected function isEditableInRendering($pagefile) {
//$editable = (int) $this->useImageEditor;
//if($editable) {
if($this->renderValueMode) {
$editable = false;
} else if($pagefile->ext == 'svg') {
$editable = true;
} else {
$editable = true;
}
// if(strpos($this->name, '_repeater') && preg_match('/_repeater\d+$/', $this->name)) {
// $editable = false;
// }
return $editable;
}
/**
* Get URL for viewing image variations
*
* @param Pageimage $pagefile
* @param string $id
* @return string
*
*/
protected function getVariationUrl($pagefile, $id) {
return $this->wire('config')->urls->admin . "page/image/variations/" .
"?id={$pagefile->page->id}" .
"&file=$pagefile->name" .
"&modal=1" .
"&varcnt=varcnt_$id";
}
/**
* Get variations for the given Pagefile
*
* @param Pagefile|Pageimage $pagefile
* @return array
*
*/
protected function getPagefileVariations(Pagefile $pagefile) {
return isset($this->variations[$pagefile->name]) ? $this->variations[$pagefile->name] : array();
}
/**
* Get the image editor URL
*
* @param Pagefile|Pageimage $pagefile
* @param int $pageID
* @return string
*
*/
protected function getEditUrl(Pagefile $pagefile, $pageID) {
return $this->wire('config')->urls->admin . "page/image/edit/" .
"?id=$pageID" .
"&file=$pageID,$pagefile->name" .
"&rte=0" .
"&field=$this->name";
}
/**
* Render the description field input
*
* @param Pagefile|Pageimage $pagefile
* @param string $id
* @param int $n
* @return string
*
*/
protected function renderItemDescriptionField(Pagefile $pagefile, $id, $n) {
return parent::renderItemDescriptionField($pagefile, $id, $n); // TODO: Change the autogenerated stub
}
/**
* Get the hover tooltip that appears above thumbnails
*
* @param Pageimage $pagefile
* @return string
*
*/
protected function getTooltip($pagefile) {
$data = $this->buildTooltipData($pagefile);
$rows = "";
foreach($data as $row) {
$rows .= "<tr><th>$row[0]</th><td>$row[1]</td></tr>";
}
$tooltip = "<div class='gridImage__tooltip'><table>$rows</table></div>";
return $tooltip;
}
/**
* Get the root "hasPage" being edited
*
* @return NullPage|Page
* @since 3.0.168
*
*/
protected function getRootHasPage() {
$page = $this->hasPage;
if(!$page || !$page->id) {
$process = $this->wire()->process;
$page = $process instanceof WirePageEditor ? $process->getPage() : new NullPage();
}
if(wireClassExists('RepeaterPage')) { /** @var RepeaterPage $page */
while(wireInstanceOf($page, 'RepeaterPage')) $page = $page->getForPage();
}
return $page;
}
/**
* Build data for the tooltip that appears above the thumbnails
*
* #pw-hooker
*
* @param Pagefile|Pageimage $pagefile
* @return array
*
*/
protected function ___buildTooltipData($pagefile) {
$data = array(
array(
$this->labels['dimensions'],
"{$pagefile->width}x{$pagefile->height}"
),
array(
$this->labels['filesize'],
str_replace(' ', '&nbsp;', $pagefile->filesizeStr)
),
array(
$this->labels['variations'],
count($this->getPagefileVariations($pagefile))
)
);
if(strlen($pagefile->description)) {
$data[] = array(
$this->labels['description'],
"<span class='fa fa-check'></span>"
);
}
if($this->useTags && strlen($pagefile->tags)) {
$data[] = array(
$this->labels['tags'],
"<span class='fa fa-check'></span>"
);
}
return $data;
}
/**
* Return whether or not admin thumbs should be scaled
*
* @return bool
* @deprecated
*
*/
protected function getAdminThumbScale() {
return $this->adminThumbScale > 0 && ((float) $this->adminThumbScale) != 1.0;
}
/**
* Process input
*
* @param WireInputData $input
* @return $this
*
*/
public function ___processInput(WireInputData $input) {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
$page = $this->getRootHasPage();
if($page && $page->id) {
if(!$this->wire()->user->hasPermission('page-edit-images', $page)) $this->useImageEditor = 0;
}
parent::___processInput($input);
if((int) $this->wire('input')->post("_refresh_thumbnails_$this->name")) {
foreach($this->value as $img) {
$this->getAdminThumb($img, false, true);
}
$this->message($this->_('Recreated all legacy thumbnails') . " - $this->name");
}
if(!$this->isAjax && !$this->wire('config')->ajax) {
// process actions, but only on non-ajax save requests
foreach($this->value as $k => $pagefile) {
$id = $this->pagefileId($pagefile);
$action = $input->{"act_$id"};
if(empty($action)) continue;
$action = $sanitizer->pageName($action);
$actions = $this->getFileActions($pagefile);
if(!isset($actions[$action])) continue; // action not available for this file
$success = $this->processFileAction($pagefile, $action, $actions[$action]);
if($success === null) {
// action was not handled
}
}
}
return $this;
}
/**
* Process input for a given Pageimage
*
* @param WireInputData $input
* @param Pagefile|Pageimage $pagefile
* @param int $n
* @return bool
*
*/
protected function ___processInputFile(WireInputData $input, Pagefile $pagefile, $n) {
$changed = false;
$id = $this->name . '_' . $pagefile->hash;
$key = "focus_$id";
$val = $input->$key;
if($val !== null) {
if(!strlen($val)) $val = '50 50 0';
$focus = $pagefile->focus();
if($focus['str'] !== $val) {
$pagefile->focus($val);
$changed = true;
$focus = $pagefile->focus();
$rebuild = $pagefile->rebuildVariations();
// @todo rebuild variations only for images that specify both width and height
$this->message(
"Updated focus for $pagefile to: top=$focus[top]%, left=$focus[left]%, zoom=$focus[zoom] " .
"and rebuilt " . count($rebuild['rebuilt']) . " variations",
Notice::debug
);
}
}
if(parent::___processInputFile($input, $pagefile, $n)) $changed = true;
return $changed;
}
/**
* Process an action on a Pagefile/Pageimage
*
* @param Pageimage $pagefile Image file to process
* @param string $action Action to execute
* @param string $label Label that was provided to describe action
* @return bool|null Returns true on success, false on fail, or null if action was not handled or recognized
*
*/
protected function processFileAction(Pageimage $pagefile, $action, $label) {
if(!$this->useImageEditor) return null;
$success = null;
$showSuccess = true;
$rebuildVariations = false;
if($action == 'dup') {
// duplicate image file
$_pagefile = $pagefile->pagefiles->clone($pagefile);
$success = $_pagefile ? true : false;
if($success) {
$this->wire('session')->message(
sprintf($this->_('Duplicated file %1$s => %2$s'), $pagefile->basename(), $_pagefile->basename())
);
$showSuccess = false;
}
} else if($action == 'cop') {
// copy to another page and/or field
/*
$key = 'cop:' . $pagefile->page->id . ':' . $pagefile->field->name . ':' . $pagefile->basename();
$clipboard = $this->wire('session')->getFor('Pagefiles', 'clipboard');
if(!is_array($clipboard)) $clipboard = array();
if(!in_array($key, $clipboard)) $clipboard[] = $key;
$this->wire('session')->setFor('Pagefiles', 'clipboard', $clipboard);
*/
} else if($action == 'rbv') {
// rebuild variations
} else if($action == 'rmv') {
// remove variations
} else if($action == 'rmf') {
// remove focus
$pagefile->focus(false);
$success = true;
} else {
/** @var ImageSizer $sizer Image sizer actions */
$sizer = $this->wire(new ImageSizer($pagefile->filename()));
$rebuildVariations = true;
if($action == 'fv') {
$success = $sizer->flipVertical();
} else if($action == 'fh') {
$success = $sizer->flipHorizontal();
} else if($action == 'fb') {
$success = $sizer->flipBoth();
} else if($action == 'bw') {
$success = $sizer->convertToGreyscale();
} else if($action == 'sep') {
$success = $sizer->convertToSepia();
} else if($action == 'x50') {
/** @var ImageSizerEngineIMagick $engine */
$engine = $sizer->getEngine();
if(method_exists($engine, 'reduceByHalf')) {
$success = $engine->reduceByHalf($pagefile->filename());
$rebuildVariations = false;
}
} else if(strpos($action, 'r') === 0 && preg_match('/^r(-?\d+)$/', $action, $matches)) {
$deg = (int) $matches[1];
$success = $sizer->rotate($deg);
}
}
if($success && $rebuildVariations) $pagefile->rebuildVariations();
if($success === null) {
// for hooks
$success = $this->processUnknownFileAction($pagefile, $action, $label);
}
if($success) {
$pagefile->trackChange("action-$action");
$this->trackChange('value');
}
if($success && $showSuccess) {
$this->message(sprintf($this->_('Executed action “%1$s” on file %2$s'), $label, $pagefile->basename));
} else if($success === false) {
$this->error(sprintf($this->_('Failed action “%1$s” on file %2$s'), $label, $pagefile->basename));
} else if($success === null) {
$this->error(sprintf($this->_('No handler found for action “%1$s” on file %2$s'), $label, $pagefile->basename));
}
return $success;
}
/**
* Called when an action was received that InputfieldImage does not recognize (for hooking purposes)
*
* @param Pageimage $pagefile Image file to process
* @param string $action Action to execute
* @param string $label Label that was provided to describe action
* @return bool|null Returns true on success, false on fail, or null if action was not handled or recognized
*
*/
protected function ___processUnknownFileAction(Pageimage $pagefile, $action, $label) {
if($pagefile && $action && $label) {}
return null;
}
}