From 7b1913b6ed1ce1f3b9ddc1f787c8e8a047016bc1 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Wed, 24 Jan 2018 11:48:50 -0500 Subject: [PATCH] Add support for image focus area / focus point / focal point (or whatever the best term is) to InputfieldImage per processwire/processwire-requests#150 --- wire/core/ImageSizerEngine.php | 61 ++- wire/core/ImageSizerEngineGD.php | 76 +++- wire/core/Pageimage.php | 277 ++++++++++-- .../InputfieldImage/InputfieldImage.css | 47 ++- .../InputfieldImage/InputfieldImage.js | 394 +++++++++++++++++- .../InputfieldImage/InputfieldImage.min.js | 2 +- .../InputfieldImage/InputfieldImage.module | 96 ++++- .../InputfieldImage/InputfieldImage.scss | 60 ++- 8 files changed, 901 insertions(+), 112 deletions(-) diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index 1bd721c0..9887d33a 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -109,9 +109,11 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable * * Possible values: northwest, north, northeast, west, center, east, southwest, south, southeast * or TRUE to crop to center, or FALSE to disable cropping. + * Or array where index 0 is % or px from left, and index 1 is % or px from top. Percent is assumed if + * values are number strings that end with %. Pixels are assumed of values are just integers. * Default is: TRUE * - * @var bool + * @var bool|array * */ protected $cropping = true; @@ -834,11 +836,13 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable if(isset($value[$v])) $$v = $value[$v]; } } - + foreach(array('x', 'y', 'w', 'h') as $k) { - $v = isset($$k) ? $$k : -1; - if(!is_int($v) || $v < 0) throw new WireException("Missing or wrong param $k for ImageSizer-cropExtra!"); - if(('w' == $k || 'h' == $k) && 0 == $v) throw new WireException("Wrong param $k for ImageSizer-cropExtra!"); + $v = (int) (isset($$k) ? $$k : -1); + if(!$v && $k == 'w' && $h > 0) $v = $this->getProportionalWidth((int) $h); + if(!$v && $k == 'h' && $w > 0) $v = $this->getProportionalHeight((int) $w); + if($v < 0) throw new WireException("Missing or wrong param $k=$v for ImageSizer-cropExtra! " . print_r($value, true)); + if(('w' == $k || 'h' == $k) && 0 == $v) throw new WireException("Wrong param $k=$v for ImageSizer-cropExtra! " . print_r($value, true)); } $this->cropExtra = array($x, $y, $w, $h); @@ -1384,7 +1388,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable protected function getCropDimensions(&$w1, &$h1, $gdWidth, $targetWidth, $gdHeight, $targetHeight) { if(is_string($this->cropping)) { - + // calculate from 8 named cropping points switch($this->cropping) { case 'nw': $w1 = 0; @@ -1418,20 +1422,45 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable } } else if(is_array($this->cropping)) { + // calculate from specific percent or pixels from left and top + // $this->cropping is an array with the following: + // index 0 represents % or pixels from left + // index 1 represents % or pixels from top // @interrobang + @u-nikos - if(strpos($this->cropping[0], '%') === false) $pointX = (int) $this->cropping[0]; - else $pointX = $gdWidth * ((int) $this->cropping[0] / 100); + if(strpos($this->cropping[0], '%') === false) { + $pointX = (int) $this->cropping[0]; + } else { + $pointX = $gdWidth * ((int) $this->cropping[0] / 100); + } - if(strpos($this->cropping[1], '%') === false) $pointY = (int) $this->cropping[1]; - else $pointY = $gdHeight * ((int) $this->cropping[1] / 100); + if(strpos($this->cropping[1], '%') === false) { + $pointY = (int) $this->cropping[1]; + } else { + $pointY = $gdHeight * ((int) $this->cropping[1] / 100); + } + + /* + if(isset($this->cropping[2]) && $this->cropping[2] > 1) { + // zoom percent (2-100) + $zoom = (int) $this->cropping[2]; + } + */ - if($pointX < $targetWidth / 2) $w1 = 0; - else if($pointX > ($gdWidth - $targetWidth / 2)) $w1 = $gdWidth - $targetWidth; - else $w1 = $pointX - $targetWidth / 2; + if($pointX < $targetWidth / 2) { + $w1 = 0; + } else if($pointX > ($gdWidth - $targetWidth / 2)) { + $w1 = $gdWidth - $targetWidth; + } else { + $w1 = $pointX - $targetWidth / 2; + } - if($pointY < $targetHeight / 2) $h1 = 0; - else if($pointY > ($gdHeight - $targetHeight / 2)) $h1 = $gdHeight - $targetHeight; - else $h1 = $pointY - $targetHeight / 2; + if($pointY < $targetHeight / 2) { + $h1 = 0; + } else if($pointY > ($gdHeight - $targetHeight / 2)) { + $h1 = $gdHeight - $targetHeight; + } else { + $h1 = $pointY - $targetHeight / 2; + } } } diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index 0ca4b4db..4325a918 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -102,11 +102,12 @@ class ImageSizerEngineGD extends ImageSizerEngine { protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) { $this->modified = false; + $isModified = false; if(isset($this->info['bits'])) $this->imageDepth = $this->info['bits']; $this->imageFormat = strtoupper(str_replace('image/', '', $this->info['mime'])); if(!in_array($this->imageFormat, $this->validSourceImageFormats())) { - throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images", basename($dstFilename)))); + throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images"), basename($dstFilename))); } $image = null; @@ -141,6 +142,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { if($this->rotate || $needRotation) { // @horst $degrees = $this->rotate ? $this->rotate : $orientations[0]; $image = $this->imRotate($image, $degrees); + $isModified = true; if(abs($degrees) == 90 || abs($degrees) == 270) { // we have to swap width & height now! $tmp = array($this->getWidth(), $this->getHeight()); @@ -155,7 +157,10 @@ class ImageSizerEngineGD extends ImageSizerEngine { } else if($orientations[1] > 0) { $vertical = $orientations[1] == 2; } - if(!is_null($vertical)) $image = $this->imFlip($image, $vertical); + if(!is_null($vertical)) { + $image = $this->imFlip($image, $vertical); + $isModified = true; + } } // if there is requested to crop _before_ resize, we do it here @horst @@ -182,6 +187,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { $this->prepareImageLayer($image, $imageTemp); imagecopy($image, $imageTemp, 0, 0, $x, $y, $w, $h); unset($x, $y, $w, $h); + $isModified = true; // now release the intermediate image and update settings imagedestroy($imageTemp); @@ -205,15 +211,12 @@ class ImageSizerEngineGD extends ImageSizerEngine { // current version is already the desired result, we only may have to compress JPEGs but leave GIF and PNG as is: - /* - * the following commented block of code prevents PNG/GIF cropping from working - if($this->imageType == \IMAGETYPE_PNG || $this->imageType == \IMAGETYPE_GIF) { + if(!$isModified && ($this->imageType == \IMAGETYPE_PNG || $this->imageType == \IMAGETYPE_GIF)) { $result = @copy($srcFilename, $dstFilename); if(isset($image) && is_resource($image)) @imagedestroy($image); // clean up if(isset($image)) $image = null; return $result; // early return ! } - */ // process JPEGs if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) { @@ -243,10 +246,40 @@ class ImageSizerEngineGD extends ImageSizerEngine { if(self::checkMemoryForImage(array($bgWidth, $bgHeight, 3)) === false) { throw new WireException(basename($srcFilename) . " - not enough memory to resize to the intermediate image"); } + + $sourceX = 0; + $sourceY = 0; + $sourceWidth = $this->image['width']; + $sourceHeight = $this->image['height']; + + /* + * @todo figure out how to make zoom setting adjust coordinates to imagecopyresampled() calls + $zoom = is_array($this->cropping) && isset($this->cropping[2]) ? $this->cropping[2] : 0; + if($zoom > 1) { + $zoom = $zoom * 0.01; + $sourceWidth -= $sourceWidth * $zoom; + $sourceHeight -= $sourceHeight * $zoom; + $sourceX = $this->image['width'] - ($sourceWidth / 2); + $sourceY = $this->image['height'] - ($sourceHeight / 2); + $bgX = 0; + $bgY = 0; + } + */ $thumb2 = imagecreatetruecolor($bgWidth, $bgHeight); $this->prepareImageLayer($thumb2, $image); - imagecopyresampled($thumb2, $image, 0, 0, 0, 0, $bgWidth, $bgHeight, $this->image['width'], $this->image['height']); + imagecopyresampled( + $thumb2, // destination image + $image, // source image + 0, // destination X + 0, // destination Y + $sourceX, // source X + $sourceY, // source Y + $bgWidth, // destination width + $bgHeight, // destination height + $sourceWidth, // source width + $sourceHeight // source height + ); if(self::checkMemoryForImage(array($finalWidth, $finalHeight, 3)) === false) { throw new WireException(basename($srcFilename) . " - not enough memory to crop to the final image"); @@ -254,7 +287,18 @@ class ImageSizerEngineGD extends ImageSizerEngine { $thumb = imagecreatetruecolor($finalWidth, $finalHeight); $this->prepareImageLayer($thumb, $image); - imagecopyresampled($thumb, $thumb2, 0, 0, $bgX, $bgY, $finalWidth, $finalHeight, $finalWidth, $finalHeight); + imagecopyresampled( + $thumb, // destination image + $thumb2, // source image + 0, // destination X + 0, // destination Y + $bgX, // source X + $bgY, // source Y + $finalWidth, // destination width + $finalHeight, // destination height + $finalWidth, // source width + $finalHeight // source height + ); imagedestroy($thumb2); } @@ -418,7 +462,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { $amount = intval($amount / 100 * $this->usmValue); // apply unsharp mask filter - return $this->UnsharpMask($im, $amount, $radius, $threshold); + return $this->unsharpMask($im, $amount, $radius, $threshold); } // if we do not use USM, we use our default sharpening method, @@ -562,12 +606,12 @@ class ImageSizerEngineGD extends ImageSizerEngine { for($x = 0; $x < $w - 1; $x++) { // each row for($y = 0; $y < $h; $y++) { // each pixel - $rgbOrig = ImageColorAt($img, $x, $y); + $rgbOrig = imagecolorat($img, $x, $y); $rOrig = (($rgbOrig >> 16) & 0xFF); $gOrig = (($rgbOrig >> 8) & 0xFF); $bOrig = ($rgbOrig & 0xFF); - $rgbBlur = ImageColorAt($imgBlur, $x, $y); + $rgbBlur = imagecolorat($imgBlur, $x, $y); $rBlur = (($rgbBlur >> 16) & 0xFF); $gBlur = (($rgbBlur >> 8) & 0xFF); @@ -586,20 +630,20 @@ class ImageSizerEngineGD extends ImageSizerEngine { : $bOrig; if(($rOrig != $rNew) || ($gOrig != $gNew) || ($bOrig != $bNew)) { - $pixCol = ImageColorAllocate($img, $rNew, $gNew, $bNew); - ImageSetPixel($img, $x, $y, $pixCol); + $pixCol = imagecolorallocate($img, $rNew, $gNew, $bNew); + imagesetpixel($img, $x, $y, $pixCol); } } } } else { for($x = 0; $x < $w; $x++) { // each row for($y = 0; $y < $h; $y++) { // each pixel - $rgbOrig = ImageColorAt($img, $x, $y); + $rgbOrig = imagecolorat($img, $x, $y); $rOrig = (($rgbOrig >> 16) & 0xFF); $gOrig = (($rgbOrig >> 8) & 0xFF); $bOrig = ($rgbOrig & 0xFF); - $rgbBlur = ImageColorAt($imgBlur, $x, $y); + $rgbBlur = imagecolorat($imgBlur, $x, $y); $rBlur = (($rgbBlur >> 16) & 0xFF); $gBlur = (($rgbBlur >> 8) & 0xFF); @@ -624,7 +668,7 @@ class ImageSizerEngineGD extends ImageSizerEngine { $bNew = 0; } $rgbNew = ($rNew << 16) + ($gNew << 8) + $bNew; - ImageSetPixel($img, $x, $y, $rgbNew); + imagesetpixel($img, $x, $y, $rgbNew); } } } diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 879c694b..06dc1878 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -24,18 +24,20 @@ * ~~~~~ * #pw-body * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * - * @property int $width Width of image, in pixels. - * @property int $height Height of image, in pixels. - * @property int $hidpiWidth HiDPI width of image, in pixels. #pw-internal - * @property int $hidpiHeight HiDPI heigh of image, in pixels. #pw-internal - * @property string $error Last image resizing error message, when applicable. #pw-group-resize-and-crop - * @property Pageimage $original Reference to original $image, if this is a resized version. #pw-group-variations - * @property string $url - * @property string $basename - * @property string $filename + * @property-read int $width Width of image, in pixels. + * @property-read int $height Height of image, in pixels. + * @property-read int $hidpiWidth HiDPI width of image, in pixels. #pw-internal + * @property-read int $hidpiHeight HiDPI heigh of image, in pixels. #pw-internal + * @property-read string $error Last image resizing error message, when applicable. #pw-group-resize-and-crop + * @property-read Pageimage $original Reference to original $image, if this is a resized version. #pw-group-variations + * @property-read string $url + * @property-read string $basename + * @property-read string $filename + * @property-read array $focus Focus array contains 'top' (float), 'left' (float), 'zoom' (int), and 'default' (bool) properties. + * @property-read bool $hasFocus Does this image have custom focus settings? (i.e. $focus['default'] == true) * * @method bool|array isVariation($basename, $allowSelf = false) * @method Pageimage crop($x, $y, $width, $height, $options = array()) @@ -177,6 +179,119 @@ class Pageimage extends Pagefile { } } + /** + * Get or set focus area for crops to use + * + * These settings are used by $this->size() calls that specify BOTH width AND height. Focus helps to + * ensure that the important subject of the photo is not cropped out when the requested size proportion + * differs from the original image proportion. For example, not chopping off someone’s head in a photo. + * + * Default behavior is to return an array containing "top" and "left" indexes, representing percentages + * from top and left. When arguments are specified, you are either setting the top/left percentages, or + * unsetting focus, or getting focus in different ways, described in arguments below. + * + * A zoom argument/property is also present here for future use, but not currently supported. + * + * #pw-group-other + * + * @param null|float|int|array|false $top Omit to get focus array, or specify one of the following: + * - GET: Omit all arguments to get focus array (default behavior). + * - GET: Specify boolean TRUE to return TRUE if focus data is present or FALSE if not. + * - GET: Specify integer 1 to make this method return pixel dimensions rather than percentages. + * - SET: Specify both $top and $left arguments to set (values assumed to be percentages). + * - SET: Specify array containing "top" and "left" indexes to set (percentages). + * - SET: Specify array where index 0 is top and index 1 is left (percentages). + * - SET: Specify string in the format "top left", i.e. "25 70" (percentages). + * - UNSET: Specify boolean false to remove any focus values. + * @param null|float|int $left Set left value (when $top value is float|int) + * - This argument is only used when setting focus and should be omitted otherwise. + * @param null|int $zoom Zoom percent (not currently supported) + * @return array|bool|Pageimage Returns one of the following: + * - When getting returns array containing top, left and default properties. + * - When TRUE was specified for the $top argument, it returns either TRUE (has focus) or FALSE (does not have). + * - When setting or unsetting returns $this. + * + */ + public function focus($top = null, $left = null, $zoom = null) { + + if(is_string($top) && strpos($top, ' ') && $left === null) { + // SET string like "25 70 0" (representing "top left zoom") + if(strpos($top, ' ') != strrpos($top, ' ')) { + // with zoom + list($top, $left, $zoom) = explode(' ', $top, 3); + } else { + // without zoom + list($top, $left) = explode(' ', $top, 2); + $zoom = 0; + } + } + + if($top === null || $top === true || ($top === 1 && $left === null)) { + // GET + $focus = $this->filedata('focus'); + if(!is_array($focus) || empty($focus)) { + // use default + if($top === true) return false; + $focus = array( + 'top' => 50, + 'left' => 50, + 'zoom' => 0, + 'default' => true, + 'str' => '50 50 0', + ); + } else { + // use custom + if($top === true) return true; + if(!isset($focus['zoom'])) $focus['zoom'] = 0; + $focus['default'] = false; + $focus['str'] = "$focus[top] $focus[left] $focus[zoom]"; + } + if($top === 1) { + // return pixel dimensions rather than percentages + $centerX = ($focus['left'] / 100) * $this->width(); // i.e. (50 / 100) * 500 = 250; + $centerY = ($focus['top'] / 100) * $this->height(); + $focus['left'] = $centerX; + $focus['top'] = $centerY; + } + return $focus; + + } else if($top === false) { + // UNSET + $this->filedata(false, 'focus'); + + } else if($top !== null && $left !== null) { + // SET + if(is_array($top)) { + if(isset($top['left'])) { + $left = $top['left']; + $top = $top['top']; + $zoom = isset($top['zoom']) ? $top['zoom'] : 0; + } else { + $top = $top[0]; + $left = $top[1]; + $zoom = isset($top[2]) ? $top[2] : 0; + } + } + + $top = (float) $top; + $left = (float) $left; + $zoom = (int) $zoom; + + if(((int) $top) == 50 && ((int) $left) == 50 && ($zoom < 2)) { + // if matches defaults, then no reason to store in filedata + $this->filedata(false, 'focus'); + } else { + $this->filedata('focus', array( + 'top' => round($top, 1), + 'left' => round($left, 1), + 'zoom' => $zoom + )); + } + } + + return $this; + } + /** * Get a property from this Pageimage * @@ -206,6 +321,12 @@ class Pageimage extends Pagefile { case 'error': $value = $this->error; break; + case 'focus': + $value = $this->focus(); + break; + case 'hasFocus': + $value = $this->focus(true); + break; default: $value = parent::get($key); } @@ -217,7 +338,8 @@ class Pageimage extends Pagefile { * * #pw-internal * - * @param bool $reset + * @param bool|string $reset Specify true to retrieve info fresh, or filename to check and return info for. + * When specifying a filename, the info is only returned (not populated with this object). * @return array * */ @@ -226,22 +348,30 @@ class Pageimage extends Pagefile { if($reset) $checkImage = true; else if($this->imageInfo['width']) $checkImage = false; else $checkImage = true; + + $imageInfo = $this->imageInfo; + $filename = is_string($reset) && file_exists($reset) ? $reset : ''; - if($checkImage) { + if($checkImage || $filename) { if($this->ext == 'svg') { - $info = $this->getImageInfoSVG(); - $this->imageInfo['width'] = $info['width']; - $this->imageInfo['height'] = $info['height']; + $info = $this->getImageInfoSVG($filename); + $imageInfo['width'] = $info['width']; + $imageInfo['height'] = $info['height']; } else { - $info = @getimagesize($this->filename); + if($filename) { + $info = @getimagesize($filename); + } else { + $info = @getimagesize($this->filename); + } if($info) { - $this->imageInfo['width'] = $info[0]; - $this->imageInfo['height'] = $info[1]; + $imageInfo['width'] = $info[0]; + $imageInfo['height'] = $info[1]; } } + if(!$filename) $this->imageInfo = $imageInfo; } - return $this->imageInfo; + return $imageInfo; } /** @@ -251,13 +381,15 @@ class Pageimage extends Pagefile { * * #pw-internal * + * @param string $filename Optional filename to check * @return array of width and height * */ - protected function getImageInfoSVG() { + protected function getImageInfoSVG($filename = '') { $width = 0; $height = 0; - $xml = @file_get_contents($this->filename); + if(!$filename) $filename = $this->filename; + $xml = @file_get_contents($filename); if($xml) { $a = @simplexml_load_string($xml)->attributes(); @@ -268,7 +400,7 @@ class Pageimage extends Pagefile { if((!$width || !$height) && (extension_loaded('imagick') || class_exists('\IMagick'))) { try { $imagick = new \Imagick(); - $imagick->readImage($this->filename); + $imagick->readImage($filename); $width = $imagick->getImageWidth(); $height = $imagick->getImageHeight(); } catch(\Exception $e) { @@ -329,7 +461,7 @@ class Pageimage extends Pagefile { * * - `quality` (int): Quality setting 1-100 (default=90, or as specified in /site/config.php). * - `upscaling` (bool): Allow image to be upscaled? (default=true). - * - `cropping` (string|bool): Cropping mode, see possible values in "cropping" section below (default=center). + * - `cropping` (string|bool|array): Cropping mode, see possible values in "cropping" section below (default=center). * - `suffix` (string|array): Suffix word to identify the new image, or use array of words for multiple (default=none). * - `forceNew` (bool): Force re-creation of the image even if it already exists? (default=false). * - `sharpening` (string): Sharpening mode: "none", "soft", "medium", or "strong" (default=soft). @@ -339,10 +471,16 @@ class Pageimage extends Pagefile { * - `hidpi` (bool): Use HiDPI/retina pixel doubling? (default=false). * - `hidpiQuality` (bool): Quality setting for HiDPI (default=40, typically lower than regular quality setting). * - `cleanFilename` (bool): Clean filename of historical resize information for shorter filenames? (default=false). + * - `nameWidth` (int): Width to use for filename (default is to use specified $width argument). + * - `nameHeight` (int): Height to use for filename (default is to use specified $height argument). + * - `focus` (bool): Should resizes that result in crop use focus area if available? (default=true). + * In order for focus to be applicable, resize must include both width and height. * * **Possible values for "cropping" option** * - * - `center` (string): to crop to center of image, default behavior. + * - `true` (bool): Auto detect and allow use of focus (default). + * - `false` (bool): Disallow cropping. + * - `center` (string): to crop to center of image. * - `x111y222` (string): to crop by pixels, 111px from left and 222px from top (replacing 111 and 222 with your values). * - `north` (string): Crop North (top), may also be just "n". * - `northwest` (string): Crop from Northwest (top left), may also be just "nw". @@ -353,7 +491,9 @@ class Pageimage extends Pagefile { * - `west` (string): Crop West (left), may also be just "w". * - `east` (string): Crop East (right), may alos be just "e". * - `blank` (string): Specify a blank string to disallow cropping during resize. - * + * - `array(111,222)` (array): Array of integers index 0 is left pixels and index 1 is top pixels. + * - `array('11%','22%')` (array): Array of '%' appended strings where index 0 is left percent and index 1 is top percent. + * * **Note about "quality" and "upscaling" options** * * ProcessWire doesn't keep separate copies of images with different "quality" or "upscaling" values. @@ -436,6 +576,9 @@ class Pageimage extends Pagefile { 'cleanFilename' => false, // clean filename of historial resize information 'rotate' => 0, 'flip' => '', + 'nameWidth' => null, // override width to use for filename, int when populated + 'nameHeight' => null, // override height to use for filename, int when populated + 'focus' => true, // allow single dimension resizes to use focus area? ); $this->error = ''; @@ -443,16 +586,25 @@ class Pageimage extends Pagefile { $configOptions = $this->wire('config')->imageSizerOptions; if(!is_array($configOptions)) $configOptions = array(); $options = array_merge($defaultOptions, $configOptions, $options); + if($options['cropping'] === 1) $options['cropping'] = true; $width = (int) $width; $height = (int) $height; - - if(is_string($options['cropping']) + + if($options['cropping'] === true && empty($options['cropExtra']) && $options['focus'] && $this->hasFocus) { + // crop to focus area + $focus = $this->focus(); + $focus['zoom'] = 0; // not yet supported + $options['cropping'] = array("$focus[left]%", "$focus[top]%", "$focus[zoom]"); + $crop = ''; // do not add suffix + + } else if(is_string($options['cropping']) && strpos($options['cropping'], 'x') === 0 && preg_match('/^x(\d+)[yx](\d+)/', $options['cropping'], $matches)) { $options['cropping'] = true; $options['cropExtra'] = array((int) $matches[1], (int) $matches[2], $width, $height); $crop = ''; + } else { $crop = ImageSizer::croppingValueStr($options['cropping']); } @@ -462,9 +614,15 @@ class Pageimage extends Pagefile { $options['suffix'] = empty($options['suffix']) ? array() : explode(' ', $options['suffix']); } - if($options['rotate'] && !in_array(abs((int) $options['rotate']), array(90, 180, 270))) $options['rotate'] = 0; - if($options['rotate']) $options['suffix'][] = ($options['rotate'] > 0 ? "rot" : "tor") . abs($options['rotate']); - if($options['flip']) $options['suffix'][] = strtolower(substr($options['flip'], 0, 1)) == 'v' ? 'flipv' : 'fliph'; + if($options['rotate'] && !in_array(abs((int) $options['rotate']), array(90, 180, 270))) { + $options['rotate'] = 0; + } + if($options['rotate']) { + $options['suffix'][] = ($options['rotate'] > 0 ? "rot" : "tor") . abs($options['rotate']); + } + if($options['flip']) { + $options['suffix'][] = strtolower(substr($options['flip'], 0, 1)) == 'v' ? 'flipv' : 'fliph'; + } $suffixStr = ''; if(!empty($options['suffix'])) { @@ -483,20 +641,28 @@ class Pageimage extends Pagefile { if($options['hidpiQuality']) $options['quality'] = $options['hidpiQuality']; } - //$basename = $this->pagefiles->cleanBasename($this->basename(), false, false, false); - // cleanBasename($basename, $originalize = false, $allowDots = true, $translate = false) $originalName = $this->basename(); - $basename = basename($originalName, "." . $this->ext()); // i.e. myfile + // determine basename without extension, i.e. myfile + $basename = basename($originalName, "." . $this->ext()); $originalSize = $debug ? @filesize($this->filename) : 0; + if($options['cleanFilename'] && strpos($basename, '.') !== false) { $basename = substr($basename, 0, strpos($basename, '.')); } - $basename .= '.' . $width . 'x' . $height . $crop . $suffixStr . "." . $this->ext(); // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg + + // filename uses requested width/height unless another specified via nameWidth or nameHeight options + $nameWidth = is_int($options['nameWidth']) ? $options['nameWidth'] : $width; + $nameHeight = is_int($options['nameHeight']) ? $options['nameHeight'] : $height; + + // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg + $basename .= '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr . "." . $this->ext(); $filenameFinal = $this->pagefiles->path() . $basename; $filenameUnvalidated = ''; $exists = file_exists($filenameFinal); + // create a new resize if it doesn't already exist or forceNew option is set if(!$exists || $options['forceNew']) { + // filenameUnvalidated is temporary filename used for resize $filenameUnvalidated = $this->pagefiles->page->filesManager()->getTempPath() . $basename; if($exists && $options['forceNew']) @unlink($filenameFinal); if(file_exists($filenameUnvalidated)) @unlink($filenameUnvalidated); @@ -913,6 +1079,7 @@ class Pageimage extends Pagefile { * - `1` (int): Rebuild all non-suffix variations, and those w/suffix specifed in $suffix argument. ($suffix is INCLUSION list) * - `2` (int): Rebuild all variations, except those with suffix specified in $suffix argument. ($suffix is EXCLUSION list) * - `3` (int): Rebuild only variations specified in the $suffix argument. ($suffix is ONLY-INCLUSION list) + * - `4` (int): Rebuild only non-proportional, non-crop variations (variations that specify both width and height) * * Mode 0 is the only truly safe mode, as in any other mode there are possibilities that the resulting * rebuild of the variation may not be exactly what was intended. The issues with other modes primarily @@ -940,7 +1107,7 @@ class Pageimage extends Pagefile { $options['forceNew'] = true; foreach($this->getVariations(array('info' => true)) as $info) { - + $o = $options; unset($o['cropping']); $skip = false; @@ -984,6 +1151,11 @@ class Pageimage extends Pagefile { } } + if($mode == 4 && ($info['width'] == 0 || $info['height'] == 0)) { + // skip images that don't specify both width and height + $skip = true; + } + if($skip) { $skipped[] = $name; continue; @@ -994,12 +1166,32 @@ class Pageimage extends Pagefile { $o['suffix'] = $info['suffix']; if(is_file($info['path'])) unlink($info['path']); + if(!$info['width'] && $info['actualWidth']) { + $info['width'] = $info['actualWidth']; + $options['nameWidth'] = 0; + } + if(!$info['height'] && $info['actualHeight']) { + $info['height'] = $info['actualHeight']; + $options['nameHeight'] = 0; + } + if($info['crop'] && preg_match('/^x(\d+)y(\d+)$/', $info['crop'], $matches)) { + // dimensional cropping info contained in filename $cropX = (int) $matches[1]; $cropY = (int) $matches[2]; - $variation = $this->crop($cropX, $cropY, $info['width'], $info['height'], $options); + $variation = $this->crop($cropX, $cropY, $info['width'], $info['height'], $options); + + } else if($info['crop']) { + // direct cropping info contained in filename + $options['cropping'] = $info['crop']; + $variation = $this->size($info['width'], $info['height'], $options); + + } else if($this->hasFocus) { + // crop to focus area, which the size() method will determine on its own + $variation = $this->size($info['width'], $info['height'], $options); + } else { - if($info['crop']) $options['cropping'] = $info['crop']; + // no crop, no focus, just resize $variation = $this->size($info['width'], $info['height'], $options); } @@ -1027,8 +1219,10 @@ class Pageimage extends Pagefile { * - `original` (string): Original basename * - `url` (string): URL to image * - `path` (string): Full path + filename to image - * - `width` (int): Specified width - * - `height` (int): Specified height + * - `width` (int): Specified width in filename + * - `height` (int): Specified height in filename + * - `actualWidth` (int): Actual width when checked manually + * - `actualHeight` (int): Acual height when checked manually * - `crop` (string): Cropping info string or blank if none * - `suffix` (array): Array of suffixes * @@ -1152,7 +1346,10 @@ class Pageimage extends Pagefile { } else { return false; } - + + $actualInfo = $this->getImageInfo($info['path']); + $info['actualWidth'] = $actualInfo['width']; + $info['actualHeight'] = $actualInfo['height']; $info['hidpiWidth'] = $this->hidpiWidth(0, $info['width']); $info['hidpiHeight'] = $this->hidpiWidth(0, $info['height']); diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css index 25a7e894..fc1743a8 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css @@ -61,7 +61,6 @@ position: absolute; top: 50%; left: 50%; - transition: transform ease .3s; -ms-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); } .gridImage.gridImagePlaceholder .gridImage__overflow { @@ -298,13 +297,22 @@ min-height: 1em; padding: 20px; } .InputfieldImageEdit__imagewrapper > div { - width: 100%; } + width: 100%; + position: relative; } .InputfieldImageEdit__imagewrapper .detail { - display: block; text-align: center; - opacity: 0; margin-top: 2px; } - .InputfieldImageEdit__imagewrapper:hover .detail { + .InputfieldImageEdit__imagewrapper .detail-upload { + display: block; + opacity: 0; } + .InputfieldImageEdit__imagewrapper .detail-focus { + display: none; } + .InputfieldImageEdit__imagewrapper:hover .detail-upload { + opacity: 0.7; } + .InputfieldImageEdit__imagewrapper > div.focusWrap .detail-upload { + display: none; } + .InputfieldImageEdit__imagewrapper > div.focusWrap .detail-focus { + display: block; opacity: 0.7; } .InputfieldImageEdit__replace img { opacity: 0.5; } @@ -351,6 +359,29 @@ .InputfieldImageEdit .InputfieldFileDescription + .InputfieldFileTags { margin-top: 0.5em; } +.gridImage__overflow .focusArea, +.InputfieldImageEdit .focusArea { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: #fff; + cursor: default; } + .gridImage__overflow .focusArea.focusActive, + .InputfieldImageEdit .focusArea.focusActive { + display: block; + overflow: hidden; } + .gridImage__overflow .focusArea .focusCircle, + .InputfieldImageEdit .focusArea .focusCircle { + cursor: move; + width: 40px; + height: 40px; + border: 3px solid #fff; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.3); } + .InputfieldImage .ImageData { display: none; } @@ -431,6 +462,8 @@ .InputfieldImageEditAll .gridImage__overflow > img { display: block; position: static !important; + top: 0; + left: 0; transition: none; -ms-transform: none; transform: none; @@ -438,6 +471,10 @@ max-width: 100% important; height: initial !important; cursor: move; } + .InputfieldImageEditAll .gridImage__overflow > .focusArea { + position: absolute; + top: 10px; + left: 10px; } .InputfieldImageEditAll .gridImage .ImageData { position: relative; float: left; diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js index 6757d664..9ba156df 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js @@ -30,6 +30,9 @@ function InputfieldImage($) { // grid items to retry for sizing by setGridSize() methods var retryGridItems = []; + // whether or not zoom-focus feature is available + var useZoomFocus = false; + /** * Whether or not AJAX drag/drop upload is allowed? * @@ -125,7 +128,7 @@ function InputfieldImage($) { }); $el.removeClass('InputfieldImageSorting'); }, - cancel: ".InputfieldImageEdit,input,textarea,button,select,option" + cancel: ".InputfieldImageEdit,.focusArea,input,textarea,button,select,option" }; $el.sortable(sortableOptions); @@ -280,6 +283,10 @@ function InputfieldImage($) { * */ function windowResize() { + $('.focusArea.focusActive').each(function() { + var $edit = $(this).closest('.InputfieldImageEdit, .gridImage'); + if($edit.length) stopFocus($edit); + }); updateGrid(); checkInputfieldWidth(); } @@ -318,12 +325,11 @@ function InputfieldImage($) { */ function setupEdit($el, $edit) { - if($el.closest('.InputfieldImageEditAll').length) return; + if($el.closest('.InputfieldImageEditAll').length) return; // edit all mode var $img = $edit.find(".InputfieldImageEdit__image"); var $thumb = $el.find("img"); - $img.attr({ src: $thumb.attr("data-original"), "data-original": $thumb.attr("data-original"), @@ -348,7 +354,6 @@ function InputfieldImage($) { .find("img") .add($img) .magnificPopup(options); - //.addClass('magnificInit'); // move all of the .ImageData elements to the edit panel $edit.find(".InputfieldImageEdit__edit") @@ -356,6 +361,247 @@ function InputfieldImage($) { .append($el.find(".ImageData").children().not(".InputfieldFileSort")); } + /** + * Setup image for a draggable focus area and optional zoom slider + * + * @param $edit Image editor container (.InputfieldImageEdit or .gridImage) + * + */ + function startFocus($edit) { + + var $img, $el, $thumb, $input, $focusArea, $focusCircle, $inputfield, + focusData = null, gridSize, mode, + $zoomSlider, $zoomBox, lastZoomPercent = 0, + lastZoomVisual = 0, startZoomVisual = -1; + + $inputfield = $edit.closest('.Inputfield'); + gridSize = getCookieData($inputfield, 'size'); + mode = getCookieData($inputfield, 'mode'); + + if($edit.hasClass('gridImage')) { + // list mode + $el = $edit; + $img = $edit.find('.gridImage__overflow').find('img'); + $thumb = $img; + } else { + // thumbnail click for editor mode + $el = $('#' + $edit.attr('data-for')); + $img = $edit.find('.InputfieldImageEdit__image'); + $thumb = $el.find('.gridImage__overflow').find('img'); + } + + // get the focus object, optionally for a specific focusStr + function getFocus(focusStr) { + if(typeof focusStr == "undefined") { + if(focusData !== null) return focusData; + var $input = $edit.find('.InputfieldImageFocus'); + var focusStr = $input.val(); + } + var a = focusStr.split(' '); + var data = { + 'top': (typeof a[0] == "undefined" ? 50.0 : parseFloat(a[0])), + 'left': (typeof a[1] == "undefined" ? 50.0 : parseFloat(a[1])), + 'zoom': (typeof a[2] == "undefined" ? 0 : parseInt(a[2])) + }; + focusData = data; + return data; + } + + // get focus string + function getFocusStr(focusObj) { + if(typeof focusObj == "undefined") focusObj = getFocus(); + return focusObj.top + ' ' + focusObj.left + ' ' + focusObj.zoom; + } + + // get single focus property: top left or zoom + function getFocusProperty(property) { + var focus = getFocus(); + return focus[property]; + } + + // set focus for top left and zoom + function setFocus(focusObj) { + focusData = focusObj; + var focusStr = focusObj.top + ' ' + focusObj.left + ' ' + focusObj.zoom; + $thumb.attr('data-focus', focusStr); // for consumption outside startFocus() + $input = $edit.find('.InputfieldImageFocus'); + if(focusStr != $input.val()) { + $input.val(focusStr).trigger('change'); + } + } + + // set just one focus property (top, left or zoom) + function setFocusProperty(property, value) { + var focus = getFocus(); + focus[property] = value; + setFocus(focus); + } + + // Set the position of the draggable focus item + function setFocusDragPosition() { + var focus = getFocus(); + var $overlay = $focusCircle.parent(); + var w = $overlay.width(); + var h = $overlay.height(); + var x = Math.round(((focus.left / 100) * w) - ($focusCircle.width() / 1.7)); + var y = Math.round(((focus.top / 100) * h) - ($focusCircle.height() / 2.3)); + if(x < 0) x = 0; + if(y < 0) y = 0; + $focusCircle.css({ + 'top': y + 'px', + 'left': x + 'px' + }); + } + + // setup focus area (div that contains all the focus stuff) + $focusArea = $img.siblings('.focusArea'); + if(!$focusArea.length) { + $focusArea = $('
').addClass('focusArea'); + $img.after($focusArea); + } + $focusArea.css({ + 'height': $img.height() + 'px', + 'width': $img.width() + 'px' + }).addClass('focusActive'); + + // set the draggable circle for focus + $focusCircle = $focusArea.find('.focusCircle'); + if(!$focusCircle.length) { + $focusCircle = $("
").addClass('focusCircle'); + $focusArea.append($focusCircle); + } + + // indicate active state for focusing, used by stopFocus() + $img.parent().addClass('focusWrap'); + + // set the initial position for the focus circle + setFocusDragPosition(); + + // function called whenever the slider is moved + var zoomSlide = function(zoomPercent) { + if(typeof zoomPercent == "undefined") zoomPercent = lastZoomPercent; + lastZoomPercent = zoomPercent; + var w = (100 - zoomPercent) + '%'; + $zoomBox.width(w); + var zoomBoxSize = $zoomBox.width(); + var focusCircleSize = $focusCircle.height(); + $zoomBox.height(zoomBoxSize) + + var zoom = zoomPercent; + var top = parseInt($focusCircle.css('top')); // top of drag item + top += Math.floor(focusCircleSize / 2); // plus half the height of drag item + top -= Math.ceil(zoomBoxSize / 2) - 3; // minus half the height of the zoom box (-3) + + var left = parseInt($focusCircle.css('left')); + left += Math.floor(focusCircleSize / 2); + left -= Math.ceil(zoomBoxSize / 2) - 3; + + if(top < 0) top = 0; + if(left < 0) left = 0; + // constrain to corners + if(top + zoomBoxSize > $focusArea.height()) top = $focusArea.height() - zoomBoxSize; + if(left + zoomBoxSize > $focusArea.width()) left = $focusArea.width() - zoomBoxSize; + + $zoomBox.css({ + top: top + 'px', + left: left + 'px' + }); + + setFocusProperty('zoom', zoomPercent); + + // determine when to visually start showing zoom (in grid mode) + var zoomVisual = zoomPercent; + if(zoomBoxSize > $img.height() || zoomBoxSize > $img.width()) { + zoomVisual = 0; + } else { + if(!lastZoomVisual) startZoomVisual = zoomVisual; + zoomVisual = (zoomVisual - startZoomVisual)+1; + } + if(mode == 'grid') setGridSizeItem($thumb.parent(), gridSize, false, zoomVisual); + lastZoomVisual = zoomVisual; + /* + console.log('lastZoomVisual=' + lastZoomVisual + ', startZoomVisual=' + startZoomVisual + + ', img.height=' + $img.height() + ', img.width=' + $img.width() + + ', zoomBoxSize=' + zoomBoxSize + ', zoomVisual=' + zoomVisual); + */ + }; // zoomSlide + + // function called when the focus item is dragged + var dragEvent = function(event, ui) { + var $this = $(this); + var w = $this.parent().width(); + var h = $this.parent().height(); + var t = ui.position.top > 0 ? ui.position.top + ($this.width() / 2) : 0; + var l = ui.position.left > 0 ? ui.position.left + ($this.height() / 2) : 0; + var oldFocus = getFocus(); + var newFocus = { + 'top': t > 0 ? ((t / h) * 100) : 0, + 'left': l > 0 ? ((l / w) * 100) : 0, + 'zoom': getFocusProperty('zoom') + }; + setFocus(newFocus); + if(useZoomFocus) { + zoomSlide(); + } else if(mode == 'grid') { + setGridSizeItem($thumb.parent(), gridSize, false); + } + }; // dragEvent + + // make draggable and attach events + $focusCircle.draggable({ + containment: 'parent', + drag: dragEvent, + stop: dragEvent + }); + + if(useZoomFocus) { + // setup the focus zoom slider + var zoom = getFocusProperty('zoom'); + $zoomSlider = $("
").addClass('focusZoomSlider').css({ + 'margin-top': '5px' + }); + $zoomBox = $("
").addClass('focusZoomBox').css({ + 'position': 'absolute', + 'background': 'rgba(0,0,0,0.5)', + 'box-shadow': '0 0 20px rgba(0,0,0,.9)' + }); + $focusArea.prepend($zoomBox); + $img.after($zoomSlider); + $thumb.attr('src', $img.attr('src')); + $zoomSlider.slider({ + min: 0, + max: 80, + value: zoom, + range: 'max', + slide: function(event, ui) { + zoomSlide(ui.value); + } + }); + zoomSlide(zoom); + } else { + $focusArea.css('background-color', 'rgba(0,0,0,0.5)'); + } + + } + + function stopFocus($edit) { + $focusCircle = $edit.find('.focusCircle'); + if($focusCircle.length) { + var $focusWrap = $focusCircle.closest('.focusWrap'); + $focusWrap.find('.focusZoomSlider').slider('destroy').remove(); + $focusWrap.find('.focusZoomBox').remove(); + $focusWrap.removeClass('focusWrap'); + $focusCircle.draggable('destroy'); + $focusCircle.parent().removeClass('focusActive'); + $focusCircle.remove(); + var $button = $edit.find('.InputfieldImageButtonFocus'); + if($button.length) { + $icon = $button.find('i'); + $icon.removeClass('focusIconActive').toggleClass($icon.attr('data-toggle')); + } + } + } + /** * Tear down the InputfieldImageEdit panel * @@ -363,6 +609,8 @@ function InputfieldImage($) { * */ function tearDownEdit($edit) { + stopFocus($edit); + $edit.off('click', '.InputfieldImageButtonFocus'); $inputArea = $edit.find(".InputfieldImageEdit__edit"); if($inputArea.children().not(".InputfieldFileSort").length) { var $items = $inputArea.children(); @@ -517,7 +765,24 @@ function InputfieldImage($) { }; $.magnificPopup.open(options); return true; - }); + + }).on('click', '.InputfieldImageButtonFocus', function() { + + var $button = $(this); + var $icon = $button.find('i'); + var $edit = $button.closest('.InputfieldImageEdit, .gridImage'); + var $focusCircle = $edit.find('.focusCircle'); + + if($focusCircle.length) { + // stops focus + stopFocus($edit); + } else { + // starts focus + startFocus($edit); + $icon.addClass('focusIconActive'); + $icon.toggleClass($icon.attr('data-toggle')); + } + }); $(document).on("click", function(e) { var $el = $(e.target); @@ -662,8 +927,14 @@ function InputfieldImage($) { pct = Math.floor(pct); $inputfield.find(".gridImage__overflow").each(function() { var dataPct = 100 - pct; - $(this).css('width', pct + '%'); - $(this).siblings('.ImageData').css('width', dataPct + '%'); + var $this = $(this); + $this.css('width', pct + '%'); + $this.siblings('.ImageData').css('width', dataPct + '%'); + $this.find('img').css({ + top: 0, + left: 0, + transform: 'none', + }); }); setCookieData($inputfield, 'listSize', pct); } @@ -718,9 +989,10 @@ function InputfieldImage($) { * @param $item * @param gridSize * @param ragged + * @param zoom * */ - function setGridSizeItem($item, gridSize, ragged) { + function setGridSizeItem($item, gridSize, ragged, zoom) { if($item.hasClass('gridImage__overflow')) { var $img = $item.children('img'); @@ -736,23 +1008,109 @@ function InputfieldImage($) { $item.width('auto').height('auto'); return; } + + if(typeof zoom == "undefined") zoom = 0; + var focus = {}; var w = $img.width(); var h = $img.height(); - if(!w) w = parseInt($img.attr('data-w')); - if(!h) h = parseInt($img.attr('data-h')); + var dataW = parseInt($img.attr('data-w')); + var dataH = parseInt($img.attr('data-h')); + if(!w) w = dataW; + if(!h) h = dataH; + + if(!ragged) { + var focusStr = $img.attr('data-focus'); + if(typeof focusStr == "undefined") focusStr = '50.0 50.0 0'; + var focusArray = focusStr.split(' '); + focus = { + top: parseFloat(focusArray[0]), + left: parseFloat(focusArray[1]), + zoom: parseInt(focusArray[2]) + }; + } if(ragged) { - $img.css('max-height', '100%').css('max-width', 'none'); + // show full thumbnail (not square) $img.attr('height', gridSize).removeAttr('width'); + $img.css({ + 'max-height': '100%', + 'max-width': 'none', + 'top': '50%', + 'left': '50%', + 'transform': 'translate3d(-50%, -50%, 0)' + }); + + } else if(zoom > 0 && useZoomFocus) { + // focus with zoom + if(w >= h) { + $img.attr('height', gridSize).removeAttr('width'); + var maxHeight = '100%'; + var maxWidth = 'none'; + } else { + var maxHeight = 'none'; + var maxWidth = '100%'; + $img.attr('width', gridSize).removeAttr('height'); + } + var top = focus.top; + var left = focus.left; + var scale = 1 + (zoom / 25); //(zoom * 0.037); + if(scale < 0) scale = 0; + if(left < 1.0) left = 0.001; + if(top < 1.0) top = 0.001; + if(left >= 55) { + left += (left * 0.15); + } else if(left <= 45) { + left -= (left * 0.15); + } + if(top > 50) { + top += (top * 0.1); + } else if(top < 50) { + top -= (top * 0.1); + } + if(left > 100) left = 100; + if(top > 100) top = 100; + $img.css({ + 'max-height': maxHeight, + 'max-width': maxWidth, + 'top': top + '%', + 'left': left + '%', + 'transform-origin': 'top left', + 'transform': 'scale(' + scale + ') translate3d(-' + (left) + '%, -' + (top) + '%, 0)' + }); + // console.log("top=" + top + ", left=" + left + ", scale=" + scale); + } else if(w >= h) { - $img.css('max-height', '100%').css('max-width', 'none'); + // image width greater than height $img.attr('height', gridSize).removeAttr('width'); + if(focus.left < 1) focus.left = 0.001; + $img.css({ + 'max-height': '100%', + 'max-width': 'none', + 'top': '50%', + 'left': focus.left + '%', + 'transform': 'translate3d(-' + focus.left + '%, -50%, 0)' + }); } else if(h > w) { - $img.css('max-height', 'none').css('max-width', '100%'); + // image height greater tahn width $img.attr('width', gridSize).removeAttr('height'); + if(focus.top < 1) focus.top = 0.001; + $img.css({ + 'max-height': 'none', + 'max-width': '100%', + 'top': focus.top + '%', + 'left': '50%', + 'transform': 'translate3d(-50%, -' + focus.top + '%, 0)' + }); } else { - $img.css('max-height', '100%').css('max-width', 'none'); + // perfectly square image + $img.css({ + 'max-height': '100%', + 'max-width': 'none', + 'top': '50%', + 'left': '50%', + 'transform': 'translate3d(-50%, -50%, 0)' + }); $img.removeAttr('width').attr('height', gridSize); } @@ -808,8 +1166,10 @@ function InputfieldImage($) { var $inputfield = $a.closest('.Inputfield'); var href = $a.attr('href'); var size; + var $aPrev = $a.parent().children('.' + activeClass); + var hrefPrev = $aPrev.attr('href'); - $a.parent().children('.' + activeClass).removeClass(activeClass); + $aPrev.removeClass(activeClass); $a.addClass(activeClass); if(href == 'list') { @@ -831,6 +1191,10 @@ function InputfieldImage($) { size = getCookieData($inputfield, 'size'); setGridSize($inputfield, size, false); setCookieData($inputfield, 'mode', 'grid'); + if(hrefPrev == 'left') setTimeout(function() { + // because width/height aren't immediately available for img, so run again in this case + setGridSize($inputfield, size, false); + }, 100); } //hrefPrev = href; //hrefPrev == href && href != 'left' && href != 'list' ? '' : href; diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js index 1e7f7e4e..bd352a31 100644 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js @@ -1 +1 @@ -function InputfieldImage(v){var k=null;var b={file:"",item:null,edit:null};var F={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var r=[];function s(){var M=window.File&&window.FileList&&window.FileReader;var L=v(".InputfieldAllowAjaxUpload").length>0;var N=v("#PageIDIndicator").length>0;return(M&&(N||L))}function y(N,L,M){L||(L=250);var O,P;return function(){var S=M||this;var R=+new Date(),Q=arguments;if(O&&R .gridImage",start:function(Q,P){var O=E(M.closest(".Inputfield"),"size");P.placeholder.append(v("
").css({display:"block",height:O+"px",width:O+"px"}));N=window.setTimeout(function(){G(M,null)},100);M.addClass("InputfieldImageSorting")},stop:function(Q,O){var P=v(this);if(N!==null){O.item.find(".InputfieldImageEdit__edit").click();clearTimeout(N)}P.children("li").each(function(S){var R=v(this).find(".InputfieldFileSort");if(R.val()!=S){R.val(S).change()}});M.removeClass("InputfieldImageSorting")},cancel:".InputfieldImageEdit,input,textarea,button,select,option"};M.sortable(L)}function p(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){var O=v(N.el).attr("data-original");if(typeof O=="undefined"||!O){O=v(N.el).attr("src")}N.src=O}};L.gallery={enabled:true};M.find("img").magnificPopup(L)}function t(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){N.src=v(N.el).attr("src")}};L.gallery={enabled:false};M.find("img").magnificPopup(L)}function C(L){return L.find(".InputfieldImageEdit--active")}function u(L){return v("#"+L.find(".InputfieldImageEdit__edit").attr("data-current"))}function D(N){var L=N.is(":checked");var M=N.parents(".gridImages").find(".gridImage__deletebox");if(L){M.prop("checked","checked").change()}else{M.removeAttr("checked").change()}}function J(M){if(typeof M=="undefined"){var L=v(".gridImages")}else{var L=M.find(".gridImages")}L.each(function(){var N=v(this),O=C(N);if(O.length){i(u(O),O)}})}function w(L){var O=[];var T=[];var Q=[];var N=0,S=0,M=0;var R;if(typeof L=="undefined"){R=v(".InputfieldImage.Inputfield")}else{R=L}R.removeClass("InputfieldImageNarrow InputfieldImageMedium InputfieldImageWide");R.each(function(){var V=v(this);var W=V.width();if(W<1){return}if(W<=500){O[N]=V;N++}else{if(W<=900){T[S]=V;S++}else{Q[M]=V;M++}}});for(var P=0;P=Q){O.css("max-height","100%").css("max-width","none");O.attr("height",M).removeAttr("width")}else{if(Q>L){O.css("max-height","none").css("max-width","100%");O.attr("width",M).removeAttr("height")}else{O.css("max-height","100%").css("max-width","none");O.removeAttr("width").attr("height",M)}}}var L=O.width();if(L){N.css({width:(P?L+"px":M+"px"),height:M+"px"})}else{var R=N.attr("data-tries");if(!R){R=0}if(typeof R=="undefined"){R=0}R=parseInt(R);if(R>3){N.css({width:M+"px",height:M+"px"})}else{r.push(N);N.attr("data-tries",R+1)}}}function A(M){if(M.find(".InputfieldImageListToggle").length){return}var P=v("").append("");var R=v("").append("");var L=v("").append("");var Q="InputfieldImageListToggle--active";var O="";var N=function(W){var V=v(this);var U=V.closest(".Inputfield");var S=V.attr("href");var T;V.parent().children("."+Q).removeClass(Q);V.addClass(Q);if(S=="list"){if(!U.hasClass("InputfieldImageEditAll")){U.find(".InputfieldImageEdit--active .InputfieldImageEdit__close").click();U.addClass("InputfieldImageEditAll")}T=E(U,"listSize");l(U,T);e(U,"mode","list")}else{if(S=="left"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,true);e(U,"mode","left");J()}else{if(S=="grid"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,false);e(U,"mode","grid")}}}B(U.find(".gridImages"));V.blur();return false};P.click(N);R.click(N);L.click(N);if(M.hasClass("InputfieldImage")){M.find(".InputfieldHeader").append(P).append(R).append(L);O=E(M,"mode")}else{v(".InputfieldImage .InputfieldHeader",M).append(P).append(R).append(L)}if(O=="list"){P.click()}else{if(O=="left"){R.click()}else{}}}function z(Q){var N=Q.children(".InputfieldHeader");if(N.children(".InputfieldImageSizeSlider").length){return}var P=Q.find(".gridImages");var M=P.attr("data-gridsize");var O=M/2;var L=M*2;var R=v('');N.append(R);R.slider({min:O,max:L,value:E(Q,"size"),range:"min",slide:function(U,W){var V=W.value;var X=15;var Y=Math.floor(M/X);var S=V-O;var T=Math.floor(X+(S/Y));if(Q.hasClass("InputfieldImageEditAll")){e(Q,"size",V);l(Q,T)}else{e(Q,"listSize",T);j(Q,V)}},start:function(S,T){if(Q.find(".InputfieldImageEdit:visible").length){Q.find(".InputfieldImageEdit__close").click()}},stop:function(S,T){J(Q)}})}function e(M,P,O){var N=E(M);var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"";if(!L.length||typeof O=="undefined"){return}if(N[L][P]==O){return}N[L][P]=O;v.cookie("InputfieldImage",N);c=N}function E(M,P){if(c&&typeof P=="undefined"){return c}var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"na";var O=c?c:v.cookie("InputfieldImage");var N=null;if(!O){var O={}}if(typeof O[L]=="undefined"){O[L]={}}if(typeof O[L].size=="undefined"){O[L].size=parseInt(M.find(".gridImages").attr("data-size"))}if(typeof O[L].listSize=="undefined"){O[L].listSize=23}if(typeof O[L].mode=="undefined"){O[L].mode=M.find(".gridImages").attr("data-gridMode")}if(c==null){c=O}if(typeof P=="undefined"){N=O}else{if(P===true){N=O[L]}else{if(typeof O[L][P]!="undefined"){N=O[L][P]}}}return N}function a(P){if(P.hasClass("InputfieldStateCollapsed")){return}var Q=parseInt(P.find(".InputfieldImageMaxFiles").val());var O=P.find(".gridImages");var N=E(P,"size");var R=E(P,"mode");var M=R=="left"?true:false;if(!N){N=O.attr("data-gridsize")}N=parseInt(N);if(P.hasClass("InputfieldImageEditAll")||R=="list"){var L=E(P,"listSize");l(P,L)}else{j(P,N,M)}if(!P.hasClass("InputfieldImageInit")){P.addClass("InputfieldImageInit");if(P.hasClass("InputfieldRenderValueMode")){return p(P)}else{if(Q==1){P.addClass("InputfieldImageMax1");t(P)}else{B(O)}}A(P);z(P)}w(P);P.on("change",".InputfieldFileActionSelect",function(){var S=v(this).next(".InputfieldFileActionNote");if(v(this).val().length){S.fadeIn()}else{S.hide()}})}function I(){v("body").addClass("ie-no-drop");v(".InputfieldImage.InputfieldFileMultiple").each(function(){var M=v(this),O=parseInt(M.find(".InputfieldFileMaxFiles").val()),L=M.find(".gridImages"),N=M.find(".InputfieldImageUpload");N.on("change","input[type=file]",function(){var S=v(this),Q=S.parent(".InputMask");if(S.val().length>1){Q.addClass("ui-state-disabled")}else{Q.removeClass("ui-state-disabled")}if(S.next("input.InputfieldFile").length>0){return}var P=L.children("li").length+N.find("input[type=file]").length+1;if(O>0&&P>=O){return}N.find(".InputMask").not(":last").each(function(){var T=v(this);if(T.find("input[type=file]").val()<1){T.remove()}});var R=Q.clone().removeClass("ui-state-disabled");R.children("input[type=file]").val("");R.insertAfter(Q)})})}function K(N){var M;if(N.length>0){M=N.find(".InputfieldImageUpload")}else{M=v(".InputfieldImageUpload")}M.each(function(Q){var R=v(this);var P=R.closest(".InputfieldContent");if(R.hasClass("InputfieldImageInitUpload")){return}O(P,Q);R.addClass("InputfieldImageInitUpload")});function O(Y,ak){var X=Y.parents("form");var P=Y.closest(".InputfieldRepeaterItem");var T=P.length?P.attr("data-editUrl"):X.attr("action");T+=(T.indexOf("?")>-1?"&":"?")+"InputfieldFileAjax=1";var ap=X.find("input._post_token");var W=ap.attr("name");var ab=ap.val();var aa=Y.find(".InputfieldImageErrors").first();var S=Y.find(".InputfieldImageUpload").data("fieldname");S=S.slice(0,-2);var ai=Y.closest(".Inputfield.InputfieldImage");var ao=Y.find(".InputfieldImageUpload").data("extensions").toLowerCase();var ah=Y.find(".InputfieldImageUpload").data("maxfilesize");var Z=Y.find("input[type=file]").get(0);var R=Y.find(".gridImages");var al=R.get(0);var ad=R.data("gridsize");var ae=null;var ac=parseInt(Y.find(".InputfieldImageMaxFiles").val());var an=n(ai);var aj=an.maxWidth>0||an.maxHeight>0||an.maxSize>0;am(Y);if(ac!=1){ag(R)}R.children().addClass("InputfieldFileItemExisting");ai.on("pwimageupload",function(aq,ar){af([ar.file],ar.xhr)});function V(ar,aq){if(typeof aq!=="undefined"){ar=""+aq+": "+ar}return"
  • "+ar+"
  • "}function Q(ar){var aq=new String(ar).substring(ar.lastIndexOf("/")+1);if(aq.lastIndexOf(".")!=-1){aq=aq.substring(0,aq.lastIndexOf("."))}return aq}function am(ar){if(ar.hasClass("InputfieldImageDropzoneInit")){return}var av=ar.get(0);var au=ar.closest(".Inputfield");function aq(){if(au.hasClass("pw-drag-in-file")){return}ar.addClass("ui-state-hover");au.addClass("pw-drag-in-file")}function at(){if(!au.hasClass("pw-drag-in-file")){return}ar.removeClass("ui-state-hover");au.removeClass("pw-drag-in-file")}av.addEventListener("dragleave",function(){at()},false);av.addEventListener("dragenter",function(aw){aw.preventDefault();aq()},false);av.addEventListener("dragover",function(aw){if(!ar.is("ui-state-hover")){aq()}aw.preventDefault();aw.stopPropagation();return false},false);av.addEventListener("drop",function(aw){af(aw.dataTransfer.files);at();aw.preventDefault();aw.stopPropagation();return false},false);ar.addClass("InputfieldImageDropzoneInit")}function ag(az){var aD=null;var aB=false;var ar=null;var aq=az.closest(".Inputfield");function aw(){aq.addClass("pw-drag-in-file")}function aC(){aq.removeClass("pw-drag-in-file")}function av(aF){var aJ=aF.offset();var aG=aF.width();var aE=aF.height();var aI=aJ.left+aG/2;var aH=aJ.top+aE/2;return{clientX:aI,clientY:aH}}function ay(){return az.find(".InputfieldImageEdit--active").length>0}function ax(aF){if(ay()){return}aF.preventDefault();aF.stopPropagation();aw();aB=false;if(aD==null){var aE=az.attr("data-size")+"px";var aG=v("
    ").addClass("gridImage__overflow");if(az.closest(".InputfieldImageEditAll").length){aG.css({width:"100%",height:aE})}else{aG.css({width:aE,height:aE})}aD=v("
  • ").addClass("ImageOuter gridImage gridImagePlaceholder").append(aG);az.append(aD)}var aH=av(aD);aD.simulate("mousedown",aH)}function aA(aE){if(ay()){return}aE.preventDefault();aE.stopPropagation();aw();aB=false;if(aD==null){return}var aF={clientX:aE.originalEvent.clientX,clientY:aE.originalEvent.clientY};aD.simulate("mousemove",aF)}function au(aE){if(ay()){return}aE.preventDefault();aE.stopPropagation();if(aD==null){return false}aB=true;if(ar){clearTimeout(ar)}ar=setTimeout(function(){if(!aB||aD==null){return}aD.remove();aD=null;aC()},1000)}function at(aE){if(ay()){return}aC();aB=false;var aF={clientX:aE.clientX,clientY:aE.clientY};aD.simulate("mouseup",aF);k=aD.next(".gridImage");aD.remove();aD=null}if(az.length&&!az.hasClass("gridImagesInitDropInPlace")){az.on("dragenter",ax);az.on("dragover",aA);az.on("dragleave",au);az.on("drop",at);az.addClass("gridImagesInitDropInPlace")}}function U(aP,aD,az){var aM=ProcessWire.config.InputfieldImage.labels;var ax=parseInt(aP.size/1024,10)+" kB";var aO='
    '+aM.dimensions+''+aM.na+"
    "+aM.filesize+""+ax+"
    "+aM.variations+"0
    ";var aR=v('
  • '),aJ=v(aO),ay=v('
    '),aq=v('
    '),aG=v("
    "),aI=v(""),aL=v(' '),aK=v('
    '),ar,aB,aQ,aE=URL.createObjectURL(aP),at=ai.find(".gridImages"),av=ac==1,aH=E(ai,"size"),aw=E(ai,"listSize"),au=ai.hasClass("InputfieldImageEditAll"),aA=v('');ay.append(aA);aG.find(".gridImage__inner").append(aL);aG.find(".gridImage__inner").append(aK.css("display","none"));aG.find(".gridImage__inner").append(aI);aq.append(v('

    '+aP.name+'

    '+ax+""));if(au){ay.css("width",aw+"%");aq.css("width",(100-aw)+"%")}else{ay.css({width:aH+"px",height:aH+"px"})}aR.append(aJ).append(ay).append(aG).append(aq);aA.attr({src:aE,"data-original":aE});img=new Image();img.addEventListener("load",function(){aJ.find(".dimensions").html(this.width+" × "+this.height);var aS=Math.min(this.width,this.height)/aH;aA.attr({width:this.width/aS,height:this.height/aS})},false);img.src=aE;if(typeof az!="undefined"){aB=az}else{aB=new XMLHttpRequest()}function aC(aS){if(typeof aS!="undefined"){if(!aS.lengthComputable){return}aI.attr("value",parseInt((aS.loaded/aS.total)*100))}v("body").addClass("pw-uploading");aK.css("display","block")}aB.upload.addEventListener("progress",aC,false);aB.addEventListener("load",function(){aB.getAllResponseHeaders();var aV=v.parseJSON(aB.responseText);if(typeof aV.ajaxResponse!="undefined"){aV=aV.ajaxResponse}var aT=aV.length>1;if(aV.error!==undefined){aV=[aV]}for(var aU=0;aU-1){aY=aY.substring(0,aY.indexOf("?"))}var aW=aY.substring(aY.lastIndexOf(".")+1).toLowerCase();aY=aY.substring(0,aY.lastIndexOf("."));if(aW==a2){a3.children("span").text(aY).removeAttr("contenteditable")}aX.find(".gridImage__edit").click()}b.file="";b.item=null;b.edit=null}if(ae){clearTimeout(ae)}k=null;ae=setTimeout(function(){if(ac!=1){B(at)}else{t(ai)}v("body").removeClass("pw-uploading");at.trigger("AjaxUploadDone")},500);ai.trigger("change").removeClass("InputfieldFileEmpty")},false);if(b.edit){b.edit.find(".InputfieldImageEdit__close").click()}else{if(ai.find(".InputfieldImageEdit:visible").length){ai.find(".InputfieldImageEdit__close").click()}}if(b.item){b.item.replaceWith(aR);b.item=aR}else{if(k&&k.length){k.before(aR)}else{at.append(aR)}}function aN(aS,aU){if(typeof az=="undefined"){aB.open("POST",T,true)}aB.setRequestHeader("X-FILENAME",encodeURIComponent(aS.name));aB.setRequestHeader("X-FIELDNAME",S);if(b.item){aB.setRequestHeader("X-REPLACENAME",b.file)}aB.setRequestHeader("Content-Type","application/octet-stream");aB.setRequestHeader("X-"+W,ab);aB.setRequestHeader("X-REQUESTED-WITH","XMLHttpRequest");if(typeof aU!="undefined"&&aU!=false){aB.send(aU)}else{aB.send(aS)}J();ai.trigger("change");var aT=ai.find(".InputfieldFileItem").length;if(aT==1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileMultiple").addClass("InputfieldFileSingle")}else{if(aT>1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileSingle").addClass("InputfieldFileMultiple")}}}aC();if(aj){var aF=new PWImageResizer(an);aK.addClass("pw-resizing");aF.resize(aP,function(aS){aK.removeClass("pw-resizing");aN(aP,aS)})}else{aN(aP)}}function af(aq,ay){var aw=function(aA){return parseInt(aA/1024,10)};if(typeof aq==="undefined"){al.innerHTML="No support for the File API in this web browser";return}for(var au=0,at=aq.length;auah&&ah>2000000){var ar=aw(aq[au].size),av=aw(ah);az="Filesize "+ar+" kb is too big. Maximum allowed is "+av+" kb";aa.append(V(az,aq[au].name))}else{if(typeof ay!="undefined"){U(aq[au],ax,ay)}else{U(aq[au],ax)}}}if(ac==1){break}}}Z.addEventListener("change",function(aq){af(this.files);aq.preventDefault();aq.stopPropagation();this.value=""},false)}function L(){var P=".InputfieldImageEdit__imagewrapper img";v(document).on("dragenter",P,function(){var S=v(this);if(S.closest(".InputfieldImageMax1").length){return}var T=S.attr("src");var Q=S.closest(".InputfieldImageEdit");var R=S.closest(".InputfieldImageEdit__imagewrapper");R.addClass("InputfieldImageEdit__replace");b.file=new String(T).substring(T.lastIndexOf("/")+1);b.item=v("#"+Q.attr("data-for"));b.edit=Q}).on("dragleave",P,function(){var R=v(this);if(R.closest(".InputfieldImageMax1").length){return}var Q=R.closest(".InputfieldImageEdit__imagewrapper");Q.removeClass("InputfieldImageEdit__replace");b.file="";b.item=null;b.edit=null})}L()}function n(M){var L={maxWidth:0,maxHeight:0,maxSize:0,quality:1,autoRotate:true,debug:ProcessWire.config.debug};var N=M.attr("data-resize");if(typeof N!="undefined"&&N.length){N=N.split(";");L.maxWidth=parseInt(N[0]);L.maxHeight=parseInt(N[1]);L.maxSize=parseFloat(N[2]);L.quality=parseFloat(N[3])}return L}function H(){v(".InputfieldImage.Inputfield").each(function(){a(v(this))});x();if(s()){K("")}else{I()}v(document).on("reloaded",".InputfieldImage",function(){var L=v(this);a(L);K(L)}).on("wiretabclick",function(N,M,L){M.find(".InputfieldImage").each(function(){a(v(this))})}).on("opened",".InputfieldImage",function(){a(v(this))})}H()}jQuery(document).ready(function(a){InputfieldImage(a)}); \ No newline at end of file +function InputfieldImage(x){var l=null;var b={file:"",item:null,edit:null};var I={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var s=[];var k=false;function u(){var P=window.File&&window.FileList&&window.FileReader;var O=x(".InputfieldAllowAjaxUpload").length>0;var Q=x("#PageIDIndicator").length>0;return(P&&(Q||O))}function A(Q,O,P){O||(O=250);var R,S;return function(){var V=P||this;var U=+new Date(),T=arguments;if(R&&U .gridImage",start:function(T,S){var R=H(P.closest(".Inputfield"),"size");S.placeholder.append(x("
    ").css({display:"block",height:R+"px",width:R+"px"}));Q=window.setTimeout(function(){J(P,null)},100);P.addClass("InputfieldImageSorting")},stop:function(T,R){var S=x(this);if(Q!==null){R.item.find(".InputfieldImageEdit__edit").click();clearTimeout(Q)}S.children("li").each(function(V){var U=x(this).find(".InputfieldFileSort");if(U.val()!=V){U.val(V).change()}});P.removeClass("InputfieldImageSorting")},cancel:".InputfieldImageEdit,.focusArea,input,textarea,button,select,option"};P.sortable(O)}function q(P){var O=x.extend(true,{},I);O.callbacks={elementParse:function(Q){var R=x(Q.el).attr("data-original");if(typeof R=="undefined"||!R){R=x(Q.el).attr("src")}Q.src=R}};O.gallery={enabled:true};P.find("img").magnificPopup(O)}function v(P){var O=x.extend(true,{},I);O.callbacks={elementParse:function(Q){Q.src=x(Q.el).attr("src")}};O.gallery={enabled:false};P.find("img").magnificPopup(O)}function F(O){return O.find(".InputfieldImageEdit--active")}function w(O){return x("#"+O.find(".InputfieldImageEdit__edit").attr("data-current"))}function G(Q){var O=Q.is(":checked");var P=Q.parents(".gridImages").find(".gridImage__deletebox");if(O){P.prop("checked","checked").change()}else{P.removeAttr("checked").change()}}function M(P){if(typeof P=="undefined"){var O=x(".gridImages")}else{var O=P.find(".gridImages")}O.each(function(){var Q=x(this),R=F(Q);if(R.length){i(w(R),R)}})}function y(O){var R=[];var W=[];var T=[];var Q=0,V=0,P=0;var U;if(typeof O=="undefined"){U=x(".InputfieldImage.Inputfield")}else{U=O}U.removeClass("InputfieldImageNarrow InputfieldImageMedium InputfieldImageWide");U.each(function(){var Y=x(this);var Z=Y.width();if(Z<1){return}if(Z<=500){R[Q]=Y;Q++}else{if(Z<=900){W[V]=Y;V++}else{T[P]=Y;P++}}});for(var S=0;S").addClass("focusArea");X.after(ai)}ai.css({height:X.height()+"px",width:X.width()+"px"}).addClass("focusActive");Z=ai.find(".focusCircle");if(!Z.length){Z=x("
    ").addClass("focusCircle");ai.append(Z)}X.parent().addClass("focusWrap");ae();var af=function(av){if(typeof av=="undefined"){av=Q}Q=av;var ao=(100-av)+"%";V.width(ao);var an=V.width();var aq=Z.height();V.height(an);var ap=av;var au=parseInt(Z.css("top"));au+=Math.floor(aq/2);au-=Math.ceil(an/2)-3;var at=parseInt(Z.css("left"));at+=Math.floor(aq/2);at-=Math.ceil(an/2)-3;if(au<0){au=0}if(at<0){at=0}if(au+an>ai.height()){au=ai.height()-an}if(at+an>ai.width()){at=ai.width()-an}V.css({top:au+"px",left:at+"px"});aa("zoom",av);var ar=av;if(an>X.height()||an>X.width()){ar=0}else{if(!W){aj=ar}ar=(ar-aj)+1}if(ad=="grid"){h(ab.parent(),ac,false,ar)}W=ar};var Y=function(an,au){var at=x(this);var av=at.parent().width();var ar=at.parent().height();var aw=au.position.top>0?au.position.top+(at.width()/2):0;var aq=au.position.left>0?au.position.left+(at.height()/2):0;var ao=R();var ap={top:aw>0?((aw/ar)*100):0,left:aq>0?((aq/av)*100):0,zoom:U("zoom")};P(ap);if(k){af()}else{if(ad=="grid"){h(ab.parent(),ac,false)}}};Z.draggable({containment:"parent",drag:Y,stop:Y});if(k){var O=U("zoom");al=x("
    ").addClass("focusZoomSlider").css({"margin-top":"5px"});V=x("
    ").addClass("focusZoomBox").css({position:"absolute",background:"rgba(0,0,0,0.5)","box-shadow":"0 0 20px rgba(0,0,0,.9)"});ai.prepend(V);X.after(al);ab.attr("src",X.attr("src"));al.slider({min:0,max:80,value:O,range:"max",slide:function(an,ao){af(ao.value)}});af(O)}else{ai.css("background-color","rgba(0,0,0,0.5)")}}function C(O){$focusCircle=O.find(".focusCircle");if($focusCircle.length){var P=$focusCircle.closest(".focusWrap");P.find(".focusZoomSlider").slider("destroy").remove();P.find(".focusZoomBox").remove();P.removeClass("focusWrap");$focusCircle.draggable("destroy");$focusCircle.parent().removeClass("focusActive");$focusCircle.remove();var Q=O.find(".InputfieldImageButtonFocus");if(Q.length){$icon=Q.find("i");$icon.removeClass("focusIconActive").toggleClass($icon.attr("data-toggle"))}}}function n(O){C(O);O.off("click",".InputfieldImageButtonFocus");$inputArea=O.find(".InputfieldImageEdit__edit");if($inputArea.children().not(".InputfieldFileSort").length){var P=$inputArea.children();x("#"+$inputArea.attr("data-current")).find(".ImageData").append(P)}}function J(Q,P){var O;if(Q){O=Q.find(".InputfieldImageEdit--active")}else{if(P){O=x(".InputfieldImageEdit--active").not(P.find(".InputfieldImageEdit--active"))}else{O=x(".InputfieldImageEdit--active")}}if(O.length){n(O);O.removeClass("InputfieldImageEdit--active").removeAttr("id");x("#"+O.attr("data-for")).removeClass("gridImageEditing")}x(".InputfieldImageEdit__replace").removeClass("InputfieldImageEdit__replace")}function i(R,P){if(!R||!R.length){return}var O=R.parent().children().not(".InputfieldImageEdit");var S=0;var T=false;var U=null;O.each(function(){if(U){return}var V=x(this);var W=V.offset().top;if(T&&W!=S){U=V}else{if(V.attr("id")==R.attr("id")){T=true}}S=W});if(U){P.insertBefore(U)}else{P.insertAfter(O.eq(O.length-1))}var Q=P.find(".InputfieldImageEdit__arrow");if(Q.length){Q.css("left",R.position().left+(R.outerWidth()/2)+"px")}}function z(){x(window).resize(A(g,200));x(document).on("click dblclick",".gridImage__trash",function(O){var P=x(this).find("input");P.prop("checked",f).change();if(O.type=="dblclick"){G(P);O.preventDefault();O.stopPropagation()}});x(document).on("change",".gridImage__deletebox",function(){p(x(this))});x(document).on("click",".gridImage__edit",function(R){var P=x(this).closest(".gridImage");if(!P.length){return}if(P.closest(".InputfieldImageEditAll").length){return false}var Q=P.closest(".gridImages");var O=Q.find(".InputfieldImageEdit");if(P.hasClass("gridImageEditing")){O.find(".InputfieldImageEdit__close").click()}else{i(P,O);n(O);d(P,O);O.addClass("InputfieldImageEdit--active").attr("data-for",P.attr("id"));Q.find(".gridImageEditing").removeClass("gridImageEditing");P.addClass("gridImageEditing")}}).on("click",".InputfieldImageEditAll img",function(Q){Q.stopPropagation();Q.preventDefault();x.magnificPopup.close();var O=x.extend(true,{},I);var P=x(this);O.items={src:P.attr("data-original"),title:P.attr("alt")};x.magnificPopup.open(O);return true}).on("click",".InputfieldImageButtonFocus",function(){var R=x(this);var O=R.find("i");var P=R.closest(".InputfieldImageEdit, .gridImage");var Q=P.find(".focusCircle");if(Q.length){C(P)}else{t(P);O.addClass("focusIconActive");O.toggleClass(O.attr("data-toggle"))}});x(document).on("click",function(P){var O=x(P.target);if(O.closest(".InputfieldImageEdit").length){J(null,O.parents(".gridImages"))}else{if(O.is("input, textarea")&&O.closest(".InputfieldImageEditAll").length){O.focus().one("blur",function(){O.closest(".gridImages").sortable("enable")});O.closest(".gridImages").sortable("disable")}else{if(O.closest(".gridImage__inner").length){J(null,O.parents(".gridImages"))}else{if(O.closest(".mfp-container").length){return}else{if(O.closest(".ui-dialog").length){return}else{if(O.is(".mfp-close")){return}else{if(O.is("a.remove")){return}else{J(null,null)}}}}}}}});x(document).on("click",".InputfieldImageEdit__close",function(O){J(x(this).parents(".gridImages"),null)});x(document).on("change",".InputfieldImage",function(){x(this).find(".InputfieldImageButtonCrop:not(.pw-modal-dblclick)").addClass("pw-modal-dblclick ui-state-disabled")}).on("click",".InputfieldImageButtonCrop.ui-state-disabled",function(Q){var P=x(this);var O=P.closest(".gridImages");if(!O.hasClass("gridImagesAlerted")){ProcessWire.alert(ProcessWire.config.InputfieldImage.labels.changes);O.addClass("gridImagesAlerted")}setTimeout(function(){P.removeClass("ui-state-active")},500);return false});x(".ImagesGrid").on("click","button.pw-modal",function(O){O.preventDefault()});r();y()}function r(){x(document).on("click",".InputfieldImageEdit__name",function(Q){var O=x(this).children("span");var R=O.closest(".gridImage, .InputfieldImageEdit").find(".InputfieldFileRename");var P=O.closest(".gridImages");P.sortable("disable");R.val(O.text());O.on("keypress",function(S){if(S.which==13){O.blur();return false}return true});O.attr("autocomplete","off").attr("autocorrect","off").attr("autocapitalize","off").attr("spellcheck","false");O.focus().on("blur",function(){var S=x(this).text();if(x.trim(S).length<1){O.text(R.val())}else{if(S!=R.val()){R.val(S).change();P.closest(".Inputfield").trigger("change")}}O.off("keypress");P.sortable("enable")})})}function m(O,P){P=Math.floor(P);O.find(".gridImage__overflow").each(function(){var Q=100-P;var R=x(this);R.css("width",P+"%");R.siblings(".ImageData").css("width",Q+"%");R.find("img").css({top:0,left:0,transform:"none"})});e(O,"listSize",P)}function j(S,O,Q){if(!O){return}var P=O+"px";var R=S.find(".gridImages");if(typeof Q=="undefined"||Q==null){Q=R.attr("data-ragged")?true:false}if(Q){R.attr("data-ragged",1)}else{R.removeAttr("data-ragged")}R.find(".gridImage__overflow").each(function(){h(x(this),O,Q)});R.find(".gridImage__edit, .gridImage__resize").css("line-height",P);R.attr("data-size",O);e(S,"size",O);if(s.length){setTimeout(function(){while(s.length){var T=s.pop();h(T,O,Q)}},150)}}function h(P,T,S,O){if(P.hasClass("gridImage__overflow")){var R=P.children("img")}else{if(P.is("img")){var R=P;P=R.closest(".gridImage__overflow")}else{return}}if(!T){R.removeAttr("width").removeAttr("height");P.width("auto").height("auto");return}if(typeof O=="undefined"){O=0}var Y={};var U=R.width();var ac=R.height();var V=parseInt(R.attr("data-w"));var ad=parseInt(R.attr("data-h"));if(!U){U=V}if(!ac){ac=ad}if(!S){var Z=R.attr("data-focus");if(typeof Z=="undefined"){Z="50.0 50.0 0"}var ab=Z.split(" ");Y={top:parseFloat(ab[0]),left:parseFloat(ab[1]),zoom:parseInt(ab[2])}}if(S){R.attr("height",T).removeAttr("width");R.css({"max-height":"100%","max-width":"none",top:"50%",left:"50%",transform:"translate3d(-50%, -50%, 0)"})}else{if(O>0&&k){if(U>=ac){R.attr("height",T).removeAttr("width");var aa="100%";var ae="none"}else{var aa="none";var ae="100%";R.attr("width",T).removeAttr("height")}var X=Y.top;var Q=Y.left;var af=1+(O/25);if(af<0){af=0}if(Q<1){Q=0.001}if(X<1){X=0.001}if(Q>=55){Q+=(Q*0.15)}else{if(Q<=45){Q-=(Q*0.15)}}if(X>50){X+=(X*0.1)}else{if(X<50){X-=(X*0.1)}}if(Q>100){Q=100}if(X>100){X=100}R.css({"max-height":aa,"max-width":ae,top:X+"%",left:Q+"%","transform-origin":"top left",transform:"scale("+af+") translate3d(-"+(Q)+"%, -"+(X)+"%, 0)"})}else{if(U>=ac){R.attr("height",T).removeAttr("width");if(Y.left<1){Y.left=0.001}R.css({"max-height":"100%","max-width":"none",top:"50%",left:Y.left+"%",transform:"translate3d(-"+Y.left+"%, -50%, 0)"})}else{if(ac>U){R.attr("width",T).removeAttr("height");if(Y.top<1){Y.top=0.001}R.css({"max-height":"none","max-width":"100%",top:Y.top+"%",left:"50%",transform:"translate3d(-50%, -"+Y.top+"%, 0)"})}else{R.css({"max-height":"100%","max-width":"none",top:"50%",left:"50%",transform:"translate3d(-50%, -50%, 0)"});R.removeAttr("width").attr("height",T)}}}}var U=R.width();if(U){P.css({width:(S?U+"px":T+"px"),height:T+"px"})}else{var W=P.attr("data-tries");if(!W){W=0}if(typeof W=="undefined"){W=0}W=parseInt(W);if(W>3){P.css({width:T+"px",height:T+"px"})}else{s.push(P);P.attr("data-tries",W+1)}}}function D(P){if(P.find(".InputfieldImageListToggle").length){return}var S=x("").append("");var U=x("").append("");var O=x("").append("");var T="InputfieldImageListToggle--active";var R="";var Q=function(aa){var Z=x(this);var X=Z.closest(".Inputfield");var V=Z.attr("href");var W;var ab=Z.parent().children("."+T);var Y=ab.attr("href");ab.removeClass(T);Z.addClass(T);if(V=="list"){if(!X.hasClass("InputfieldImageEditAll")){X.find(".InputfieldImageEdit--active .InputfieldImageEdit__close").click();X.addClass("InputfieldImageEditAll")}W=H(X,"listSize");m(X,W);e(X,"mode","list")}else{if(V=="left"){X.removeClass("InputfieldImageEditAll");W=H(X,"size");j(X,W,true);e(X,"mode","left");M()}else{if(V=="grid"){X.removeClass("InputfieldImageEditAll");W=H(X,"size");j(X,W,false);e(X,"mode","grid");if(Y=="left"){setTimeout(function(){j(X,W,false)},100)}}}}E(X.find(".gridImages"));Z.blur();return false};S.click(Q);U.click(Q);O.click(Q);if(P.hasClass("InputfieldImage")){P.find(".InputfieldHeader").append(S).append(U).append(O);R=H(P,"mode")}else{x(".InputfieldImage .InputfieldHeader",P).append(S).append(U).append(O)}if(R=="list"){S.click()}else{if(R=="left"){U.click()}else{}}}function B(T){var Q=T.children(".InputfieldHeader");if(Q.children(".InputfieldImageSizeSlider").length){return}var S=T.find(".gridImages");var P=S.attr("data-gridsize");var R=P/2;var O=P*2;var U=x('');Q.append(U);U.slider({min:R,max:O,value:H(T,"size"),range:"min",slide:function(X,Z){var Y=Z.value;var aa=15;var ab=Math.floor(P/aa);var V=Y-R;var W=Math.floor(aa+(V/ab));if(T.hasClass("InputfieldImageEditAll")){e(T,"size",Y);m(T,W)}else{e(T,"listSize",W);j(T,Y)}},start:function(V,W){if(T.find(".InputfieldImageEdit:visible").length){T.find(".InputfieldImageEdit__close").click()}},stop:function(V,W){M(T)}})}function e(P,S,R){var Q=H(P);var T=P.attr("id");var O=T?T.replace("wrap_Inputfield_",""):"";if(!O.length||typeof R=="undefined"){return}if(Q[O][S]==R){return}Q[O][S]=R;x.cookie("InputfieldImage",Q);c=Q}function H(P,S){if(c&&typeof S=="undefined"){return c}var T=P.attr("id");var O=T?T.replace("wrap_Inputfield_",""):"na";var R=c?c:x.cookie("InputfieldImage");var Q=null;if(!R){var R={}}if(typeof R[O]=="undefined"){R[O]={}}if(typeof R[O].size=="undefined"){R[O].size=parseInt(P.find(".gridImages").attr("data-size"))}if(typeof R[O].listSize=="undefined"){R[O].listSize=23}if(typeof R[O].mode=="undefined"){R[O].mode=P.find(".gridImages").attr("data-gridMode")}if(c==null){c=R}if(typeof S=="undefined"){Q=R}else{if(S===true){Q=R[O]}else{if(typeof R[O][S]!="undefined"){Q=R[O][S]}}}return Q}function a(S){if(S.hasClass("InputfieldStateCollapsed")){return}var T=parseInt(S.find(".InputfieldImageMaxFiles").val());var R=S.find(".gridImages");var Q=H(S,"size");var U=H(S,"mode");var P=U=="left"?true:false;if(!Q){Q=R.attr("data-gridsize")}Q=parseInt(Q);if(S.hasClass("InputfieldImageEditAll")||U=="list"){var O=H(S,"listSize");m(S,O)}else{j(S,Q,P)}if(!S.hasClass("InputfieldImageInit")){S.addClass("InputfieldImageInit");if(S.hasClass("InputfieldRenderValueMode")){return q(S)}else{if(T==1){S.addClass("InputfieldImageMax1");v(S)}else{E(R)}}D(S);B(S)}y(S);S.on("change",".InputfieldFileActionSelect",function(){var V=x(this).next(".InputfieldFileActionNote");if(x(this).val().length){V.fadeIn()}else{V.hide()}})}function L(){x("body").addClass("ie-no-drop");x(".InputfieldImage.InputfieldFileMultiple").each(function(){var P=x(this),R=parseInt(P.find(".InputfieldFileMaxFiles").val()),O=P.find(".gridImages"),Q=P.find(".InputfieldImageUpload");Q.on("change","input[type=file]",function(){var V=x(this),T=V.parent(".InputMask");if(V.val().length>1){T.addClass("ui-state-disabled")}else{T.removeClass("ui-state-disabled")}if(V.next("input.InputfieldFile").length>0){return}var S=O.children("li").length+Q.find("input[type=file]").length+1;if(R>0&&S>=R){return}Q.find(".InputMask").not(":last").each(function(){var W=x(this);if(W.find("input[type=file]").val()<1){W.remove()}});var U=T.clone().removeClass("ui-state-disabled");U.children("input[type=file]").val("");U.insertAfter(T)})})}function N(Q){var P;if(Q.length>0){P=Q.find(".InputfieldImageUpload")}else{P=x(".InputfieldImageUpload")}P.each(function(T){var U=x(this);var S=U.closest(".InputfieldContent");if(U.hasClass("InputfieldImageInitUpload")){return}R(S,T);U.addClass("InputfieldImageInitUpload")});function R(ab,an){var aa=ab.parents("form");var S=ab.closest(".InputfieldRepeaterItem");var W=S.length?S.attr("data-editUrl"):aa.attr("action");W+=(W.indexOf("?")>-1?"&":"?")+"InputfieldFileAjax=1";var at=aa.find("input._post_token");var Z=at.attr("name");var ae=at.val();var ad=ab.find(".InputfieldImageErrors").first();var V=ab.find(".InputfieldImageUpload").data("fieldname");V=V.slice(0,-2);var al=ab.closest(".Inputfield.InputfieldImage");var ar=ab.find(".InputfieldImageUpload").data("extensions").toLowerCase();var ak=ab.find(".InputfieldImageUpload").data("maxfilesize");var ac=ab.find("input[type=file]").get(0);var U=ab.find(".gridImages");var ao=U.get(0);var ag=U.data("gridsize");var ah=null;var af=parseInt(ab.find(".InputfieldImageMaxFiles").val());var aq=o(al);var am=aq.maxWidth>0||aq.maxHeight>0||aq.maxSize>0;ap(ab);if(af!=1){aj(U)}U.children().addClass("InputfieldFileItemExisting");al.on("pwimageupload",function(au,av){ai([av.file],av.xhr)});function Y(av,au){if(typeof au!=="undefined"){av=""+au+": "+av}return"
  • "+av+"
  • "}function T(av){var au=new String(av).substring(av.lastIndexOf("/")+1);if(au.lastIndexOf(".")!=-1){au=au.substring(0,au.lastIndexOf("."))}return au}function ap(av){if(av.hasClass("InputfieldImageDropzoneInit")){return}var ay=av.get(0);var ax=av.closest(".Inputfield");function au(){if(ax.hasClass("pw-drag-in-file")){return}av.addClass("ui-state-hover");ax.addClass("pw-drag-in-file")}function aw(){if(!ax.hasClass("pw-drag-in-file")){return}av.removeClass("ui-state-hover");ax.removeClass("pw-drag-in-file")}ay.addEventListener("dragleave",function(){aw()},false);ay.addEventListener("dragenter",function(az){az.preventDefault();au()},false);ay.addEventListener("dragover",function(az){if(!av.is("ui-state-hover")){au()}az.preventDefault();az.stopPropagation();return false},false);ay.addEventListener("drop",function(az){ai(az.dataTransfer.files);aw();az.preventDefault();az.stopPropagation();return false},false);av.addClass("InputfieldImageDropzoneInit")}function aj(aC){var aG=null;var aE=false;var av=null;var au=aC.closest(".Inputfield");function az(){au.addClass("pw-drag-in-file")}function aF(){au.removeClass("pw-drag-in-file")}function ay(aI){var aM=aI.offset();var aJ=aI.width();var aH=aI.height();var aL=aM.left+aJ/2;var aK=aM.top+aH/2;return{clientX:aL,clientY:aK}}function aB(){return aC.find(".InputfieldImageEdit--active").length>0}function aA(aI){if(aB()){return}aI.preventDefault();aI.stopPropagation();az();aE=false;if(aG==null){var aH=aC.attr("data-size")+"px";var aJ=x("
    ").addClass("gridImage__overflow");if(aC.closest(".InputfieldImageEditAll").length){aJ.css({width:"100%",height:aH})}else{aJ.css({width:aH,height:aH})}aG=x("
  • ").addClass("ImageOuter gridImage gridImagePlaceholder").append(aJ);aC.append(aG)}var aK=ay(aG);aG.simulate("mousedown",aK)}function aD(aH){if(aB()){return}aH.preventDefault();aH.stopPropagation();az();aE=false;if(aG==null){return}var aI={clientX:aH.originalEvent.clientX,clientY:aH.originalEvent.clientY};aG.simulate("mousemove",aI)}function ax(aH){if(aB()){return}aH.preventDefault();aH.stopPropagation();if(aG==null){return false}aE=true;if(av){clearTimeout(av)}av=setTimeout(function(){if(!aE||aG==null){return}aG.remove();aG=null;aF()},1000)}function aw(aH){if(aB()){return}aF();aE=false;var aI={clientX:aH.clientX,clientY:aH.clientY};aG.simulate("mouseup",aI);l=aG.next(".gridImage");aG.remove();aG=null}if(aC.length&&!aC.hasClass("gridImagesInitDropInPlace")){aC.on("dragenter",aA);aC.on("dragover",aD);aC.on("dragleave",ax);aC.on("drop",aw);aC.addClass("gridImagesInitDropInPlace")}}function X(aS,aG,aC){var aP=ProcessWire.config.InputfieldImage.labels;var aA=parseInt(aS.size/1024,10)+" kB";var aR='
    '+aP.dimensions+''+aP.na+"
    "+aP.filesize+""+aA+"
    "+aP.variations+"0
    ";var aU=x('
  • '),aM=x(aR),aB=x('
    '),au=x('
    '),aJ=x("
    "),aL=x(""),aO=x(' '),aN=x('
    '),av,aE,aT,aH=URL.createObjectURL(aS),aw=al.find(".gridImages"),ay=af==1,aK=H(al,"size"),az=H(al,"listSize"),ax=al.hasClass("InputfieldImageEditAll"),aD=x('');aB.append(aD);aJ.find(".gridImage__inner").append(aO);aJ.find(".gridImage__inner").append(aN.css("display","none"));aJ.find(".gridImage__inner").append(aL);au.append(x('

    '+aS.name+'

    '+aA+""));if(ax){aB.css("width",az+"%");au.css("width",(100-az)+"%")}else{aB.css({width:aK+"px",height:aK+"px"})}aU.append(aM).append(aB).append(aJ).append(au);aD.attr({src:aH,"data-original":aH});img=new Image();img.addEventListener("load",function(){aM.find(".dimensions").html(this.width+" × "+this.height);var aV=Math.min(this.width,this.height)/aK;aD.attr({width:this.width/aV,height:this.height/aV})},false);img.src=aH;if(typeof aC!="undefined"){aE=aC}else{aE=new XMLHttpRequest()}function aF(aV){if(typeof aV!="undefined"){if(!aV.lengthComputable){return}aL.attr("value",parseInt((aV.loaded/aV.total)*100))}x("body").addClass("pw-uploading");aN.css("display","block")}aE.upload.addEventListener("progress",aF,false);aE.addEventListener("load",function(){aE.getAllResponseHeaders();var aY=x.parseJSON(aE.responseText);if(typeof aY.ajaxResponse!="undefined"){aY=aY.ajaxResponse}var aW=aY.length>1;if(aY.error!==undefined){aY=[aY]}for(var aX=0;aX-1){a1=a1.substring(0,a1.indexOf("?"))}var aZ=a1.substring(a1.lastIndexOf(".")+1).toLowerCase();a1=a1.substring(0,a1.lastIndexOf("."));if(aZ==a5){a6.children("span").text(a1).removeAttr("contenteditable")}a0.find(".gridImage__edit").click()}b.file="";b.item=null;b.edit=null}if(ah){clearTimeout(ah)}l=null;ah=setTimeout(function(){if(af!=1){E(aw)}else{v(al)}x("body").removeClass("pw-uploading");aw.trigger("AjaxUploadDone")},500);al.trigger("change").removeClass("InputfieldFileEmpty")},false);if(b.edit){b.edit.find(".InputfieldImageEdit__close").click()}else{if(al.find(".InputfieldImageEdit:visible").length){al.find(".InputfieldImageEdit__close").click()}}if(b.item){b.item.replaceWith(aU);b.item=aU}else{if(l&&l.length){l.before(aU)}else{aw.append(aU)}}function aQ(aV,aX){if(typeof aC=="undefined"){aE.open("POST",W,true)}aE.setRequestHeader("X-FILENAME",encodeURIComponent(aV.name));aE.setRequestHeader("X-FIELDNAME",V);if(b.item){aE.setRequestHeader("X-REPLACENAME",b.file)}aE.setRequestHeader("Content-Type","application/octet-stream");aE.setRequestHeader("X-"+Z,ae);aE.setRequestHeader("X-REQUESTED-WITH","XMLHttpRequest");if(typeof aX!="undefined"&&aX!=false){aE.send(aX)}else{aE.send(aV)}M();al.trigger("change");var aW=al.find(".InputfieldFileItem").length;if(aW==1){al.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileMultiple").addClass("InputfieldFileSingle")}else{if(aW>1){al.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileSingle").addClass("InputfieldFileMultiple")}}}aF();if(am){var aI=new PWImageResizer(aq);aN.addClass("pw-resizing");aI.resize(aS,function(aV){aN.removeClass("pw-resizing");aQ(aS,aV)})}else{aQ(aS)}}function ai(au,aB){var az=function(aD){return parseInt(aD/1024,10)};if(typeof au==="undefined"){ao.innerHTML="No support for the File API in this web browser";return}for(var ax=0,aw=au.length;axak&&ak>2000000){var av=az(au[ax].size),ay=az(ak);aC="Filesize "+av+" kb is too big. Maximum allowed is "+ay+" kb";ad.append(Y(aC,au[ax].name))}else{if(typeof aB!="undefined"){X(au[ax],aA,aB)}else{X(au[ax],aA)}}}if(af==1){break}}}ac.addEventListener("change",function(au){ai(this.files);au.preventDefault();au.stopPropagation();this.value=""},false)}function O(){var S=".InputfieldImageEdit__imagewrapper img";x(document).on("dragenter",S,function(){var V=x(this);if(V.closest(".InputfieldImageMax1").length){return}var W=V.attr("src");var T=V.closest(".InputfieldImageEdit");var U=V.closest(".InputfieldImageEdit__imagewrapper");U.addClass("InputfieldImageEdit__replace");b.file=new String(W).substring(W.lastIndexOf("/")+1);b.item=x("#"+T.attr("data-for"));b.edit=T}).on("dragleave",S,function(){var U=x(this);if(U.closest(".InputfieldImageMax1").length){return}var T=U.closest(".InputfieldImageEdit__imagewrapper");T.removeClass("InputfieldImageEdit__replace");b.file="";b.item=null;b.edit=null})}O()}function o(P){var O={maxWidth:0,maxHeight:0,maxSize:0,quality:1,autoRotate:true,debug:ProcessWire.config.debug};var Q=P.attr("data-resize");if(typeof Q!="undefined"&&Q.length){Q=Q.split(";");O.maxWidth=parseInt(Q[0]);O.maxHeight=parseInt(Q[1]);O.maxSize=parseFloat(Q[2]);O.quality=parseFloat(Q[3])}return O}function K(){x(".InputfieldImage.Inputfield").each(function(){a(x(this))});z();if(u()){N("")}else{L()}x(document).on("reloaded",".InputfieldImage",function(){var O=x(this);a(O);N(O)}).on("wiretabclick",function(Q,P,O){P.find(".InputfieldImage").each(function(){a(x(this))})}).on("opened",".InputfieldImage",function(){a(x(this))})}K()}jQuery(document).ready(function(a){InputfieldImage(a)}); \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module index 5bd2fa4e..28147c90 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module @@ -52,7 +52,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu return array( 'title' => __('Images', __FILE__), // Module Title 'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary - 'version' => 121, + 'version' => 122, 'permanent' => true, ); } @@ -125,6 +125,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu $this->labels = array_merge($this->labels, array( 'crop' => $this->_('Crop'), + 'focus' => $this->_('Focus'), 'variations' => $this->_('Variations'), 'dimensions' => $this->_('Dimensions'), 'filesize' => $this->_('Filesize'), @@ -144,6 +145,11 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu $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 @@ -186,11 +192,14 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu } if(!$renderValueMode && $this->value instanceof Pageimages) { - $process = $this->wire('process'); - if($process instanceof WirePageEditor) { - $page = $process->getPage(); - } else { - $page = new NullPage(); + $page = $this->hasPage; + if(!$page || !$page->id) { + $process = $this->wire('process'); + if($process instanceof WirePageEditor) { + $page = $process->getPage(); + } else { + $page = new NullPage(); + } } if($page->id && $this->wire('user')->hasPermission('page-edit-images', $page)) { $modules->get('JqueryUI')->use('modal'); @@ -250,6 +259,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu 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 .= "
    @@ -258,7 +268,8 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
    - $dropNew + $dropNew + $focus
    @@ -474,6 +485,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu $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 @@ -542,6 +554,10 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu $attr['data-w'] = $_thumbWidth; $attr['data-h'] = $_thumbHeight; $attr["data-original"] = $img->URL; + + $focus = $img->focus(); + $attr['data-focus'] = $focus['str']; + $markup = " $value) $markup .= "$key=\"$value\" "; $markup .= " />"; @@ -559,6 +575,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu 'error' => $error, 'title' => $title, ); + return $a; } @@ -639,6 +656,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu // @todo move the following to separate method shared by the renderSingle $ext = $pagefile->ext(); $basename = $pagefile->basename(false); + $focus = $pagefile->focus(); $out .= "
    @@ -651,6 +669,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu +
    "; } @@ -744,16 +763,28 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu $variations = $this->getPagefileVariations($pagefile); $variationCount = count($variations); $editUrl = $this->getEditUrl($pagefile, $pageID); - $variationUrl = $this->getVariationUrl($pagefile, $id); - $buttonClass = $this->themeSettings['buttonClass'] . " $this->modalClass pw-modal"; + $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 = ''; - $buttonText = str_replace('{out}', " $labels[crop]", $this->themeSettings['buttonText']); - $out = ""; - $buttonText = " $labels[variations] ($variationCount)"; + + // Crop + $buttonText = str_replace('{out}', " $labels[crop]", $this->themeSettings['buttonText']); + $out .= ""; + + // Focus + $iconA = $pagefile->hasFocus ? 'fa-check-circle-o' : 'fa-circle-o'; + $iconB = $pagefile->hasFocus ? 'fa-check-circle' : 'fa-dot-circle-o'; + $buttonText = str_replace('{out}', " $labels[focus]", $this->themeSettings['buttonText']); + $out .= ""; + + // Variations + $buttonText = " $labels[variations] ($variationCount)"; $buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']); - $out .= ""; + $out .= ""; return $out; } @@ -1262,6 +1293,45 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu 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 * diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss index 3876bdc1..e7ac2de8 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss @@ -102,7 +102,7 @@ $itemPadding: 0.4em; position: absolute; top: 50%; left: 50%; - transition: transform ease .3s; + //transition: transform ease .3s; -ms-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); } @@ -466,19 +466,32 @@ $itemPadding: 0.4em; & > div { // Fix for Firefox not respecting aspect ratio on images width: 100%; + position: relative; } - .detail { - display: block; text-align: center; - opacity: 0; margin-top: 2px; } - &:hover .detail { + .detail-upload { + display: block; + opacity: 0; + } + .detail-focus { + display: none; + } + &:hover .detail-upload { opacity: 0.7; } + & > div.focusWrap .detail-upload { + display: none; + } + & > div.focusWrap .detail-focus { + display: block; + opacity: 0.7; + } + } - + &__replace img { // dragging new img on top of old one opacity: 0.5; @@ -546,6 +559,33 @@ $itemPadding: 0.4em; } +.gridImage__overflow .focusArea, +.InputfieldImageEdit .focusArea { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: #fff; + cursor: default; + + &.focusActive { + display: block; + overflow: hidden; + } + + .focusCircle { + cursor: move; + width: 40px; + height: 40px; + border: 3px solid #fff; + border-radius: 50%; + background-color: rgba(255,255,255,0.3); + } +} + + .InputfieldImage .ImageData { // element in a .gridImage item that contains the details and editable content display: none; @@ -670,6 +710,8 @@ $itemPadding: 0.4em; & > img { display: block; position: static !important; + top: 0; + left: 0; transition: none; -ms-transform: none; transform: none; @@ -678,6 +720,12 @@ $itemPadding: 0.4em; height: initial !important; cursor: move; } + + & > .focusArea { + position: absolute; + top: 10px; // to match padding of __overflow + left: 10px; + } } .ImageData {