artabro/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module
2024-08-27 11:35:37 +02:00

1479 lines
46 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
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* 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).
* @property string $editFieldName Field name to use for linking to image editor (default=current Inputfield name)
*
* 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)
* @method array getImageEditButtons($pagefile, $id, $n, $buttonClass) 3.0.212+
* @method array getImageThumbnailActions($pagefile, $id, $n, $class) 3.0.212+
*
*
*/
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' => 127,
'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();
$config = $this->wire()->config;
$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');
$this->set('editFieldName', ''); // field name to use for image editor (default=name of this inputfield)
$options = $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 = $config->InputfieldImage;
$themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults;
$this->themeSettings = array_merge($this->themeSettings, $themeSettings);
}
/**
* Get setting or attribute
*
* @param string $key
* @return array|bool|mixed|string|null
*
*/
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;
/** @var JqueryCore $jqueryCore */
$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)) {
$moduleInfo = self::getModuleInfo();
$thisURL = $config->urls('InputfieldImage');
$jsExt = $config->debug ? "js" : "min.js";
$config->scripts->add($thisURL . "piexif.$jsExt");
$config->scripts->add($thisURL . "PWImageResizer.$jsExt?v={$config->version}-$moduleInfo[version]");
$maxSize = str_replace(',', '.', $this->maxSize);
$quality = str_replace(',', '.', (float) ($this->clientQuality / 100));
$this->wrapAttr('data-resize', "$this->maxWidth;$this->maxHeight;$maxSize;$quality");
}
$value = $this->val();
if(!$value instanceof Pageimages) $value = null;
if(!$renderValueMode && $value) {
$page = $this->getRootHasPage();
if($page->id && $this->wire()->user->hasPermission('page-edit-images', $page)) {
$jQueryUI = $modules->get('JqueryUI'); /** @var JqueryUI $jQueryUI */
$jQueryUI->use('modal');
} else {
$this->useImageEditor = 0;
}
}
if($value) $this->variations = $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) {
$out = '';
$n = 0;
$this->renderListReady($value);
if(!$this->uploadOnlyMode && WireArray::iterable($value)) {
foreach($value as $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) {
$sanitizer = $this->wire()->sanitizer;
$dropNew = $sanitizer->entities1($this->_('drop in new image file to replace'));
$focus = $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 = 'InputfieldImageList 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;
}
/**
* Wrap rendered item
*
* @param string $out
* @return string
*
*/
protected function renderItemWrap($out) {
$item = $this->currentItem;
$id = $item && !$this->renderValueMode ? " id='file_$item->hash'" : "";
return "<li$id class='ImageOuter $this->itemClass'>$out</li>";
}
/**
* Render upload
*
* @param Pagefiles|Pageimages $value
* @return string
*
*/
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'];
$chooseIcon = wireIconMarkup('folder-open-o', 'fw');
$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'>
$chooseIcon$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'];
$dropIcon = wireIconMarkup('cloud-upload');
$out .= "
<span class='AjaxUploadDropHere description'>
<span>
$dropIcon&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') . ' (width=0)');
}
$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);
}
/**
* @param Pagefile $pagefile
* @param int $n
* @return string
*
*/
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) {
$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->val()->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)";
}
$class = 'gridImage__overflow';
if($pagefile->ext() === 'svg') {
$class .= ' ' . ($pagefile->ratio < 1 ? 'portrait' : 'landscape');
}
$out = $this->getTooltip($pagefile) . "
<div class='$class'>
$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;
$thumbnailActions = array();
if($this->hasHook('getImageThumbnailActions()')) {
$thumbnailActions = $this->getImageThumbnailActions($pagefile, $id, $n, 'gridImage__btn');
}
$thumbnailActions = implode('', $thumbnailActions);
$out .= "
<div class='gridImage__hover'>
<div class='gridImage__inner'>
<label for='' class='gridImage__btn gridImage__trash'>
<input class='gridImage__deletebox' type='checkbox' name='delete_$id' value='1' title='$labels[delete]' />
<span class='fa fa-trash-o'></span>
</label>$thumbnailActions
<a class='gridImage__edit'>
<span>$labels[edit]</span>
</a>
</div>
</div>
";
$ext = $pagefile->ext();
$basename = $pagefile->basename(false);
$focus = $pagefile->focus();
if($pagefile !== $metaPagefile) {
// copy custom fields data from previous file
foreach($metaPagefile->filedata() as $key => $value) {
$pagefile->filedata($key, $value);
}
}
$inputfields = $this->getItemInputfields($pagefile);
if($inputfields) $additional .= $inputfields->render();
$tooltip = $sanitizer->entities1($this->_('Click to rename'));
$uploadName = $pagefile->uploadName();
if($uploadName != "$basename.$ext") {
$uploadName = $sanitizer->entities($this->_('Originally:') . ' ' . $uploadName);
} else {
$uploadName = '';
}
$out .= "
<div class='ImageData'>
<h2 class='InputfieldImageEdit__name pw-tooltip' title='$tooltip'><span title=\"$uploadName\" 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;
}
/**
* Get the image thumbnail icon actions/links/buttons
*
* These are icon-only actions/links displayed next to the trash icon when hovering over an image preview.
* They are also displayed as icons on the far right side of a image when in full list mode.
*
* Example:
* ~~~~~
* $wire->addHookAfter('InputfieldImage::getImageThumbnailActions', function(HookEvent $event) {
* $image = $event->arguments(0); // Pageimage
* $class = $event->arguments(3); // class to use on all returned actions
* $a = $event->return; // array
* $a['download'] = "<a class='$class' href='$pagefile->url' download><span class='fa fa-download'></span></a>";
* $event->return = $a;
* });
* ~~~~~
*
* #pw-hooker
*
* @param Pageimage $pagefile
* @param string $id Image id string
* @param int $n Image index number
* @param string $class Class that should appear on all returned actions/links/buttons
* @return array
* @since 3.0.212
*
*/
protected function ___getImageThumbnailActions($pagefile, $id, $n, $class) {
return array();
}
/**
* 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 '';
$buttonClass = $this->themeSettings['buttonClass'];
$buttons = $this->getImageEditButtons($pagefile, $id, $n, $buttonClass);
return implode('', $buttons);
}
/**
* Get array of buttons for image edit mode
*
* Hook after this to add or remove image edit buttons/actions.
*
* ~~~~~
* // Example of adding a download button
* $wire->addHookAfter('InputfieldImage::getImageEditButtons', function(HookEvent $event) {
* $image = $event->arguments(0); // Pageimage
* $class = $event->arguments(3);
* $buttons = $event->return; // array
* $icon = wireIconMarkup('download');
* $buttons['download'] = "<button class='$class'><a download href='$image->url'>$icon Download</a></button>";
* $event->return = $buttons;
* });
* ~~~~~
*
* #pw-hooker
*
* @param Pagefile|Pageimage $pagefile Image that buttons are for
* @param string $id Image/file id
* @param int $n Index of image/file (i.e 0=first)
* @param string $buttonClass Class attribute additions that should appear on all returned <button> elements
* @return array Array of <button> elements indexed by button name
* @since 3.0.212
*
*/
protected function ___getImageEditButtons($pagefile, $id, $n, $buttonClass) {
$buttons = array();
$pageID = $pagefile->pagefiles->page->id;
$variationCount = $pagefile->variations()->count();
$editUrl = $this->getEditUrl($pagefile, $pageID);
$variationUrl = $this->getVariationUrl($pagefile, $id);
$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;
// Crop
$icon = wireIconMarkup('crop');
$buttonText = str_replace('{out}', "$icon $labels[crop]", $this->themeSettings['buttonText']);
$buttons['crop'] = "<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']);
$buttons['focus'] = "<button type='button' class='InputfieldImageButtonFocus $buttonClass'>$buttonText</button>";
}
// Variations
$icon = wireIconMarkup('files-o');
$buttonText = "$icon $labels[variations] <span class='ui-priority-secondary'>($variationCount)</span>";
$buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']);
$buttons['variations'] = "<button type='button' data-href='$variationUrl' class='$modalButtonClass' data-buttons='button'>$buttonText</button>";
return $buttons;
}
/**
* 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;
$hooks = $this->wire()->hooks;
if($hooked === null) $hooked =
$hooks->isHooked('InputfieldImage::getFileActions()') ||
$hooks->isHooked('InputfieldFile::getFileActions()');
$actions = $hooked ? $this->getFileActions($pagefile) : $this->___getFileActions($pagefile);
if(empty($actions)) return '';
$selectClass = trim($this->themeSettings['selectClass'] . ' InputfieldFileActionSelect');
$sanitizer = $this->wire()->sanitizer;
$label = $sanitizer->entities1($this->_('Actions'));
$out =
"<select class='$selectClass' name='act_$id'>" .
"<option value=''>$label</option>";
foreach($actions as $name => $label) {
$label = $sanitizer->entities1($label);
$out .= "<option value='$name'>$label</option>";
}
$out .= "</select> ";
$label = $sanitizer->entities1($this->_('Action applied at save.'));
$out .= "<span class='InputfieldFileActionNote detail'>$label</span>";
return $out;
}
/**
* Get array of actions (displayed in select dropdown) available for given Pagefile
*
* #pw-hooker
*
* ~~~~~
* // Example of adding an “Get EXIF data” action
* $wire->addHookAfter('InputfieldImage::getFileActions', function(HookEvent $event) {
* $image = $event->arguments(0); // Pageimage
* if($image->ext == 'jpg' || $image->ext == 'jpeg') {
* $actions = $event->return; // array
* $actions['exif'] = 'Get EXIF data';
* $event->return = $actions;
* }
* });
*
* // Example of handling an “Get EXIF data” action
* $wire->addHookAfter('InputfieldImage::processUnknownFileAction', function(HookEvent $event) {
* $image = $event->arguments(0);
* $action = $event->arguments(1);
* if($action === 'exif') {
* $exif = exif_read_data($image->filename);
* $event->warning([ "EXIF data for $image->name" => $exif ], 'icon-photo nogroup');
* $event->return = true;
* }
* });
* ~~~~~
*
* @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 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
*
*/
protected function isEditableInRendering($pagefile) {
if($this->renderValueMode) {
$editable = false;
} else if($pagefile->ext == 'svg') {
$editable = true;
} else {
$editable = true;
}
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) {
$name = $this->editFieldName ? $this->editFieldName : $this->name;
return $this->wire()->config->urls->admin . "page/image/edit/" .
"?id=$pageID" .
"&file=$pageID,$pagefile->name" .
"&rte=0" .
"&field=$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'],
wireIconMarkup('check')
);
}
if($this->useTags && strlen($pagefile->tags)) {
$data[] = array(
$this->labels['tags'],
wireIconMarkup('check')
);
}
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) {
$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 $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 a select dropdown action was received that InputfieldImage does not recognize (for hooking purposes)
*
* This is what should be hooked to provide the processing for a custom action added from a hook.
* See the ___getFileActions() method documentation for full example including both hooks.
*
* #pw-hooker
*
* @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) {
return null;
}
}