diff --git a/Build/Gruntfile.js b/Build/Gruntfile.js index 7b830f6ba61115c0a305a1062b4f03d5bc142575..e6337c08c5ef53813b28152f7d79accc60b6d3d9 100644 --- a/Build/Gruntfile.js +++ b/Build/Gruntfile.js @@ -49,6 +49,8 @@ module.exports = function(grunt) { '<%= paths.core %>Public/JavaScript/Contrib/jquery.dataTables.js': '/datatables/media/js/jquery.dataTables.min.js', '<%= paths.core %>Public/JavaScript/Contrib/require.js': '/requirejs/require.js', '<%= paths.core %>Public/JavaScript/Contrib/moment.js': '/moment/moment.js', + '<%= paths.core %>Public/JavaScript/Contrib/cropper.min.js': '/cropper/dist/cropper.min.js', + '<%= paths.core %>Public/JavaScript/Contrib/imagesloaded.pkgd.min.js': '/imagesloaded/imagesloaded.pkgd.min.js', '<%= paths.core %>Public/JavaScript/Contrib/bootstrap-datetimepicker.js': '/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js', '<%= paths.core %>Public/JavaScript/Contrib/autosize.js': '/autosize/dest/autosize.min.js', '<%= paths.core %>Public/JavaScript/Contrib/placeholders.jquery.min.js': '/Placeholders.js/dist/placeholders.jquery.min.js', diff --git a/Build/bower.json b/Build/bower.json index 4cccb5123bd11eabba1783f6c5b8ba4afc18e18b..b1a966c135723f02376d2ea8c769c8918cad329d 100644 --- a/Build/bower.json +++ b/Build/bower.json @@ -35,6 +35,8 @@ "nprogress": "0.1.6", "datatables": "1.10.5", "autosize": "2.0.0", + "cropper": "0.9.1", + "imagesloaded": "3.1.8", "Placeholders.js": "4.0.1" } } diff --git a/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php b/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php new file mode 100644 index 0000000000000000000000000000000000000000..1d2455dc6f0f78ecb6da09bebc9380d86c808d5c --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php @@ -0,0 +1,242 @@ +<?php +namespace TYPO3\CMS\Backend\Form\Element; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\FormEngine; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; +use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; +use TYPO3\CMS\Extbase\Utility\ArrayUtility; +use TYPO3\CMS\Extbase\Utility\DebuggerUtility; + +/** + * Generation of crop image TCEform elements + */ +class ImageManipulationElement extends AbstractFormElement { + + /** + * Default element configuration + * + * @var array + */ + protected $defaultConfig = array( + 'file_field' => 'uid_local', + 'enableZoom' => FALSE, + 'allowedExtensions' => NULL, // default: $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] + 'ratios' => array( + '1.7777777777777777' => 'LLL:EXT:lang/locallang_wizards.xlf:imwizard.ratio.16_9', + '1.3333333333333333' => 'LLL:EXT:lang/locallang_wizards.xlf:imwizard.ratio.4_3', + '1' => 'LLL:EXT:lang/locallang_wizards.xlf:imwizard.ratio.1_1', + 'NaN' => 'LLL:EXT:lang/locallang_wizards.xlf:imwizard.ratio.free', + ) + ); + + /** + * Handler for unknown types. + * + * @param string $table The table name of the record + * @param string $field The field name which this element is supposed to edit + * @param array $row The record data array where the value(s) for the field can be found + * @param array $additionalInformation An array with additional configuration options. + * @return string The HTML code for the TCEform field + */ + public function render($table, $field, $row, &$additionalInformation) { + // If ratios are set do not add default options + if (isset($additionalInformation['fieldConf']['config']['ratios'])) { + unset($this->defaultConfig['ratios']); + } + $config = ArrayUtility::arrayMergeRecursiveOverrule($this->defaultConfig, $additionalInformation['fieldConf']['config']); + + // By default we allow all image extensions that can be handled by the GFX functionality + if ($config['allowedExtensions'] === NULL) { + $config['allowedExtensions'] = $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']; + } + + if ($this->isGlobalReadonly() || $config['readOnly']) { + $formEngineDummy = new FormEngine(); + $noneElement = GeneralUtility::makeInstance(NoneElement::class, $formEngineDummy); + $elementConfiguration = array( + 'fieldConf' => array( + 'config' => $config, + ), + 'itemFormElValue' => $additionalInformation['itemFormElValue'], + ); + return $noneElement->render('', '', '', $elementConfiguration); + } + + $file = $this->getFile($row, $config['file_field']); + if (!$file) { + return ''; + } + + $content = ''; + $preview = ''; + if (GeneralUtility::inList(mb_strtolower($config['allowedExtensions']), mb_strtolower($file->getExtension()))) { + + // Get preview + $preview = $this->getPreview($file, $additionalInformation['itemFormElValue']); + + // Check if ratio labels hold translation strings + $languageService = $this->getLanguageService(); + foreach ((array)$config['ratios'] as $ratio => $label) { + $config['ratios'][$ratio] = $languageService->sL($label, TRUE); + } + + $formFieldId = str_replace('.', '', uniqid('formengine-image-manipulation-', TRUE)); + $wizardData = array( + 'file' => $file->getUid(), + 'zoom' => $config['enableZoom'] ? '1' : '0', + 'ratios' => json_encode($config['ratios']), + ); + $wizardData['token'] = GeneralUtility::hmac(implode('|', $wizardData), 'ImageManipulationWizard'); + + $buttonAttributes = array( + 'data-url' => BackendUtility::getAjaxUrl('ImageManipulationWizard::getHtmlForImageManipulationWizard', $wizardData), + 'data-severity' => 'notice', + 'data-image-name' => $file->getNameWithoutExtension(), + 'data-image-uid' => $file->getUid(), + 'data-file-field' => $config['file_field'], + 'data-field' => $formFieldId, + ); + + $button = '<button class="btn btn-default t3js-image-manipulation-trigger"'; + foreach ($buttonAttributes as $key => $value) { + $button .= ' ' . $key . '="' . htmlspecialchars($value) . '"'; + } + $button .= '><span class="t3-icon fa fa-crop"></span>'; + $button .= $languageService->sL('LLL:EXT:lang/locallang_wizards.xlf:imwizard.open-editor', TRUE); + $button .= '</button>'; + + $inputField = '<input type="hidden" ' + . 'id="' . $formFieldId . '" ' + . 'name="' . $additionalInformation['itemFormElName'] . '" ' + . 'value="' . htmlspecialchars($additionalInformation['itemFormElValue']) . '" />'; + + $content .= $inputField . $button; + + $content .= $this->getImageManipulationInfoTable($additionalInformation['itemFormElValue']); + + /** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */ + $pageRenderer = $GLOBALS['SOBE']->doc->getPageRenderer(); + $pageRenderer->loadRequireJsModule( + 'TYPO3/CMS/Backend/ImageManipulation', + 'function(ImageManipulation){ImageManipulation.initializeTrigger()}' // Initialize after load + ); + } + + $content .= '<p class="text-muted"><em>' . $languageService->sL('LLL:EXT:lang/locallang_wizards.xlf:imwizard.supported-types-message', TRUE) . '<br />'; + $content .= mb_strtoupper(implode(', ', GeneralUtility::trimExplode(',', $config['allowedExtensions']))); + $content .= '</em></p>'; + + $item = '<div class="media">'; + $item .= $preview; + $item .= '<div class="media-body">' . $content . '</div>'; + $item .= '</div>'; + + return $item; + } + + /** + * Get file object + * + * @param array $row + * @param string $fieldName + * @return NULL|\TYPO3\CMS\Core\Resource\File + */ + protected function getFile(array $row, $fieldName) { + $file = NULL; + $fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : NULL; + + if (strpos($fileUid, 'sys_file_') === 0) { + $fileUid = substr($fileUid, 9); + } + if (MathUtility::canBeInterpretedAsInteger($fileUid)) { + try { + $file = ResourceFactory::getInstance()->getFileObject($fileUid); + } catch (FileDoesNotExistException $e) { + } + } + return $file; + } + + /** + * Get preview image if cropping is set + * + * @param File $file + * @param string $crop + * @return string + */ + public function getPreview(File $file, $crop) { + $preview = ''; + if ($crop) { + $imageSetup = array('width' => '150m', 'height' => '200m', 'crop' => $crop); + $processedImage = $file->process(\TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $imageSetup); + // Only use a thumbnail if the processing process was successful by checking if image width is set + if ($processedImage->getProperty('width')) { + $imageUrl = $processedImage->getPublicUrl(TRUE); + $preview = '<img src="' . $imageUrl . '" ' . + 'class="media-object thumbnail" ' . + 'width="' . $processedImage->getProperty('width') . '" ' . + 'height="' . $processedImage->getProperty('height') . '" >'; + } + } + return '<div class="media-left t3js-image-manipulation-preview' . ($preview ? '' : ' hide'). '">' . $preview . '</div>'; + } + + /** + * Get image manipulation info table + * + * @param string $rawImageManipulationValue + * @return string + */ + protected function getImageManipulationInfoTable($rawImageManipulationValue) { + $content = ''; + $imageManipulation = NULL; + $x = $y = $width = $height = 0; + + // Determine cropping values + if ($rawImageManipulationValue) { + $imageManipulation = json_decode($rawImageManipulationValue); + if (is_object($imageManipulation)) { + $x = (int)$imageManipulation->x; + $y = (int)$imageManipulation->y; + $width = (int)$imageManipulation->width; + $height = (int)$imageManipulation->height; + } else { + $imageManipulation = NULL; + } + } + $languageService = $this->getLanguageService(); + + $content .= '<div class="table-fit-block table-spacer-wrap">'; + $content .= '<table class="table table-no-borders t3js-image-manipulation-info'. ($imageManipulation === NULL ? ' hide' : '') . '">'; + $content .= '<tr><td>' . $languageService->sL('LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-x', TRUE) . '</td>'; + $content .= '<td class="t3js-image-manipulation-info-crop-x">' . $x . 'px</td></tr>'; + $content .= '<tr><td>' . $languageService->sL('LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-y', TRUE) . '</td>'; + $content .= '<td class="t3js-image-manipulation-info-crop-y">' . $y . 'px</td></tr>'; + $content .= '<tr><td>' . $languageService->sL('LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-width', TRUE) . '</td>'; + $content .= '<td class="t3js-image-manipulation-info-crop-width">' . $width . 'px</td></tr>'; + $content .= '<tr><td>' . $languageService->sL('LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-height', TRUE) . '</td>'; + $content .= '<td class="t3js-image-manipulation-info-crop-height">' . $height . 'px</td></tr>'; + $content .= '</table>'; + $content .= '</div>'; + + return $content; + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormEngine.php b/typo3/sysext/backend/Classes/Form/FormEngine.php index 68ccf5828a6b21a709148725cdbbd66e5b5cc641..892f1294cccc1ec3b7832147e1c9ca319764aed5 100644 --- a/typo3/sysext/backend/Classes/Form/FormEngine.php +++ b/typo3/sysext/backend/Classes/Form/FormEngine.php @@ -1039,6 +1039,7 @@ class FormEngine { 'none' => 'NoneElement', 'user' => 'UserElement', 'flex' => 'FlexElement', + 'image_manipulation' => 'ImageManipulationElement', 'unknown' => 'UnknownElement', ); if (!isset($typeClassNameMapping[$type])) { diff --git a/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php b/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php new file mode 100644 index 0000000000000000000000000000000000000000..3847fd7d1f8469c314ecd079f854a20078c33290 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php @@ -0,0 +1,109 @@ +<?php +namespace TYPO3\CMS\Backend\Form\Wizard; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\HttpUtility; +use TYPO3\CMS\Core\Utility\MathUtility; +use TYPO3\CMS\Fluid\View\StandaloneView; + +/** + * Wizard for rendering image manipulation view + */ +class ImageManipulationWizard { + + /** + * @var string + */ + protected $templatePath = 'EXT:backend/Resources/Private/Templates/'; + + /** + * Returns the html for the AJAX API + * + * @param array $params + * @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxRequestHandler + * @return void + */ + public function getHtmlForImageManipulationWizard($params, $ajaxRequestHandler) { + if (!$this->checkHmacToken()) { + HttpUtility::setResponseCodeAndExit(HttpUtility::HTTP_STATUS_403); + } + + $fileUid = GeneralUtility::_GET('file'); + $image = NULL; + if (MathUtility::canBeInterpretedAsInteger($fileUid)) { + try { + $image = ResourceFactory::getInstance()->getFileObject($fileUid); + } catch (FileDoesNotExistException $e) {} + } + + $view = $this->getFluidTemplateObject($this->templatePath . 'Wizards/ImageManipulationWizard.html'); + $view->assign('image', $image); + $view->assign('zoom', (bool)GeneralUtility::_GET('zoom')); + $view->assign('ratios', $this->getRatiosArray()); + $content = $view->render(); + + $ajaxRequestHandler->addContent('content', $content); + $ajaxRequestHandler->setContentFormat('html'); + } + + /** + * Check if hmac token is correct + * + * @return bool + */ + protected function checkHmacToken() { + $parameters = array(); + if (GeneralUtility::_GET('file')) { + $parameters['file'] = GeneralUtility::_GET('file'); + } + $parameters['zoom'] = GeneralUtility::_GET('zoom') ? '1' : '0'; + $parameters['ratios'] = GeneralUtility::_GET('ratios') ?: ''; + + $token = GeneralUtility::hmac(implode('|', $parameters), 'ImageManipulationWizard'); + return $token === GeneralUtility::_GET('token'); + } + + /** + * Get available ratios + * + * @return array + */ + protected function getRatiosArray() { + $ratios = json_decode(GeneralUtility::_GET('ratios')); + // Json transforms a array with sting keys to a array, + // we need to transform this to an array for the fluid ForViewHelper + if (is_object($ratios)) { + $ratios = get_object_vars($ratios); + } + return $ratios; + } + + /** + * Returns a new standalone view, shorthand function + * + * @param string $templatePathAndFileName optional the path to set the template path and filename + * @return StandaloneView + */ + protected function getFluidTemplateObject($templatePathAndFileName = NULL) { + $view = GeneralUtility::makeInstance(StandaloneView::class); + if ($templatePathAndFileName) { + $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($templatePathAndFileName)); + } + return $view; + } +} diff --git a/typo3/sysext/backend/Resources/Private/Templates/Wizards/ImageManipulationWizard.html b/typo3/sysext/backend/Resources/Private/Templates/Wizards/ImageManipulationWizard.html new file mode 100644 index 0000000000000000000000000000000000000000..2d3d92f218edfd926f75717941172b1b59a58465 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/Wizards/ImageManipulationWizard.html @@ -0,0 +1,102 @@ + +<f:if condition="{image.properties.width}"> + <f:then> + <div class="modal-panel"> + + <div class="modal-panel-sidebar modal-panel-sidebar-right"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title"><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.image-manipulation" /></h4> + </div> + <div class="modal-body"> + <form class="form"> + <div class="form-group"> + <label><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.image-title" />:</label> + <p>{f:if(condition:image.properties.title, then:image.properties.title, else:image.name)}</p> + </div> + <div class="form-group"> + <label><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.original-dimensions" />:</label> + <p>{image.properties.width} × {image.properties.height}</p> + </div> + + <f:if condition="{ratios}"> + <div class="form-group"> + <label for="ratio"><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.aspect-ratio" /></label> + <div class="btn-group btn-group-justified t3js-ratio-buttons" data-toggle="buttons"> + <f:for each="{ratios}" as="ratio" key="key" iteration="iteration"> + <label class="btn btn-default" data-method="setAspectRatio" data-option="{key}" title="{f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.set-aspect-ratio')}"><input class="sr-only" id="aspestRatio{iteration.cycle}" name="aspestRatio" value="{key}" type="radio"> <span>{ratio}</span></label> + </f:for> + </div> + </div> + </f:if> + + <f:if condition="{zoom}"> + <div class="form-group t3js-setting-zoom"> + <label for="zoom"><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.zoom" /></label> + <div class="btn-group"> + <button class="btn btn-default" data-method="zoom" data-option="0.1" title="{f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.zoom-in')}"><i class="fa fa-search-plus"></i></button> + <button class="btn btn-default" data-method="zoom" data-option="-0.1" title="{f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.zoom-out')}"><i class="fa fa-search-minus"></i></button> + </div> + </div> + </f:if> + + <div class="form-group"> + <label><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.selection" /></label> + <div class="table-fit-block"> + <table class="table table-no-borders table-transparent t3js-image-manipulation-info"> + <tr> + <td> + <f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-x"/> + </td> + <td class="t3js-image-manipulation-info-crop-x"></td> + </tr> + <tr> + <td> + <f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-y"/> + </td> + <td class="t3js-image-manipulation-info-crop-y"></td> + </tr> + <tr> + <td> + <f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-width"/> + </td> + <td class="t3js-image-manipulation-info-crop-width"></td> + </tr> + <tr> + <td> + <f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.crop-height"/> + </td> + <td class="t3js-image-manipulation-info-crop-height"></td> + </tr> + </table> + </div> + <div class="form-group"> + <button class="btn btn-default" data-method="reset" title="{f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.reset')}"><i class="fa fa-refresh"></i> {f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.reset')}</button> + </div> + </div> + + </form> + </div> + <div class="modal-footer"> + <button class="btn btn-default" data-method="dismiss" title="{f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.cancel')}"><i class="fa fa-remove"></i> {f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.cancel')}</button> + <button class="btn btn-default" data-method="save" title="{f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.accept')}"><i class="fa fa-check"></i> {f:translate(key:'LLL:EXT:lang/locallang_wizards.xlf:imwizard.accept')}</button> + </div> + </div> + + <div class="modal-panel-body"> + <div class="t3js-cropper-image-container"> + <img src="{f:uri.image(image:image, maxWidth:'1600', maxHeight: '1024')}" + naturalWidth="{image.properties.width}" naturalHeight="{image.properties.height}" /> + </div> + </div> + + </div> + + </f:then> + <f:else> + <div class="alert alert-danger"> + <h4 class="alert-title"><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.no-image-found" /></h4> + <p class="alert-message"><f:translate key="LLL:EXT:lang/locallang_wizards.xlf:imwizard.no-image-found-message" /></p> + </div> + </f:else> +</f:if> \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js b/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js new file mode 100644 index 0000000000000000000000000000000000000000..15392fdcf4c04f98b29c82c4a5ae9e6b517d5956 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js @@ -0,0 +1,271 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * contains all logic for the image crop GUI + */ +define('TYPO3/CMS/Backend/ImageManipulation', ['jquery', 'TYPO3/CMS/Backend/Modal'], function ($) { + + var ImageManipulation = { + margin: 20, + currentModal: null, + cropperSelector: '.t3js-cropper-image-container > img', + $trigger: null + }; + + + /** + * Initialize triggers + */ + ImageManipulation.initializeTrigger = function() { + var $triggers = $('.t3js-image-manipulation-trigger'); + // Remove existing bind function + $triggers.off('click', ImageManipulation.buttonClick); + // Bind new function + $triggers.on('click', ImageManipulation.buttonClick); + }; + + /** + * Functions that should be bind to the trigger button + * + * @param e click event + */ + ImageManipulation.buttonClick = function(e) { + e.preventDefault(); + // Prevent double trigger + if (ImageManipulation.$trigger !== $(this)) { + ImageManipulation.$trigger = $(this); + ImageManipulation.show(); + } + }; + + /** + * Open modal with image to crop + */ + ImageManipulation.show = function() { + ImageManipulation.currentModal = top.TYPO3.Modal.loadUrl( + ImageManipulation.$trigger.data('image-name'), + TYPO3.Severity.notice, + [], + ImageManipulation.$trigger.data('url'), + ImageManipulation.initializeCropperModal, + '.modal-content' + ); + ImageManipulation.currentModal.addClass('modal-dark'); + }; + + /** + * Initialize the cropper modal + */ + ImageManipulation.initializeCropperModal = function() { + top.require(['cropper', 'imagesloaded'], function(cropperJs, imagesLoaded) { + var $image = ImageManipulation.getCropper(); + + // wait until image is loaded + imagesLoaded($image, function() { + var $modal = ImageManipulation.currentModal.find('.modal-dialog'); + var $modalContent = $modal.find('.modal-content'); + var $modalPanelSidebar = $modal.find('.modal-panel-sidebar'); + var $modalPanelBody = $modal.find('.modal-panel-body'); + // Let modal auto-fill width + $modal.css({width:'auto', marginLeft: ImageManipulation.margin, marginRight: ImageManipulation.margin}) + .addClass('modal-image-manipulation modal-resize'); + + $modalContent.addClass('cropper-bg'); + + // Determine available height + var height = top.TYPO3.jQuery(window).height() + - (ImageManipulation.margin * 2); + $image.css({maxHeight: height}); + + // Wait a few microseconds before calculating available width (DOM isn't always updated direct) + setTimeout(function() { + $modalPanelBody.css({width: $modalContent.innerWidth() - $modalPanelSidebar.outerWidth() - (ImageManipulation.margin * 2)}); + + setTimeout(function() { + // Shrink modal when possible (the set left/right margin + width auto above makes it fill 100%) + var minWidth = Math.max(500, $image.outerWidth() + $modalPanelSidebar.outerWidth() + (ImageManipulation.margin * 2)); + var width = $modal.width() > minWidth ? minWidth : $modal.width(); + $modal.width(width); + $modalPanelBody.width(width - $modalPanelSidebar.outerWidth() - (ImageManipulation.margin * 2)); + + var modalBodyMinHeight = $modalContent.height() - + ($modalPanelSidebar.find('.modal-header').outerHeight() + $modalPanelSidebar.find('.modal-body-footer').outerHeight()); + $modalPanelSidebar.find('.modal-body').css('min-height', modalBodyMinHeight); + + // Center modal horizontal + $modal.css({marginLeft: 'auto', marginRight: 'auto'}); + + // Center modal vertical + top.TYPO3.Modal.center(); + + // Wait a few microseconds to let the modal resize + setTimeout(ImageManipulation.initializeCropper, 100); + }, 100); + + }, 100); + }); + + }); + }; + + /** + * Initialize cropper + */ + ImageManipulation.initializeCropper = function() { + var $image = ImageManipulation.getCropper(), cropData; + + // Give img-container same dimensions as the image + ImageManipulation.currentModal.find('.t3js-cropper-image-container'). + css({width: $image.width(), height: $image.height()}); + + var $trigger = ImageManipulation.$trigger; + var jsonString = $trigger.parent().find('#' + $trigger.data('field')).val(); + if (jsonString.length) { + cropData = $.parseJSON(jsonString); + } + + // Calculate current resize factor + var factor = $image.width() / $image.prop('naturalWidth'); + var $infoX = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-x'); + var $infoY = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-y'); + var $infoWidth = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-width'); + var $infoHeight = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-height'); + + $image.cropper({ + autoCropArea: 0.5, + strict: false, + zoomable: ImageManipulation.currentModal.find('.t3js-setting-zoom').length > 0, + built: function() { + if (cropData) { + var cropBox = {}; + cropBox.left = cropData.x * factor; + cropBox.top = cropData.y * factor; + cropBox.width = cropData.width * factor; + cropBox.height = cropData.height * factor; + $image.cropper('setCropBoxData', cropBox); + } + }, + crop: function (data) { + $infoX.text(Math.round(data.x) + 'px'); + $infoY.text(Math.round(data.y) + 'px'); + $infoWidth.text(Math.round(data.height) + 'px'); + $infoHeight.text(Math.round(data.width) + 'px'); + } + }); + + // Destroy cropper when modal is closed + ImageManipulation.currentModal.on('hidden.bs.modal', function() { + $image.cropper('destroy'); + }); + + ImageManipulation.initializeCroppingActions(); + }; + + /** + * Get image to be cropped + * + * @returns jQuery object + */ + ImageManipulation.getCropper = function() { + return ImageManipulation.currentModal.find(ImageManipulation.cropperSelector); + }; + + /** + * Bind buttons from cropper tool panel + */ + ImageManipulation.initializeCroppingActions = function() { + ImageManipulation.currentModal.find('[data-method]').click(function(e) { + e.preventDefault(); + var method = $(this).data('method'); + var options = $(this).data('option') || {}; + if (typeof ImageManipulation[method] === 'function') { + ImageManipulation[method](options); + } + }); + }; + + /** + * Change the aspect ratio of the crop box + * + * @param aspectRatio Number + */ + ImageManipulation.setAspectRatio = function(aspectRatio) { + var $cropper = ImageManipulation.getCropper(); + $cropper.cropper('setAspectRatio', aspectRatio); + }; + + /** + * Set zoom ratio + * + * Zoom in: requires a positive number (ratio > 0) + * Zoom out: requires a negative number (ratio < 0) + * + * @param ratio Number + */ + ImageManipulation.zoom = function(ratio) { + var $cropper = ImageManipulation.getCropper(); + $cropper.cropper('zoom', ratio); + }; + + /** + * Save crop values in form and close modal + */ + ImageManipulation.save = function() { + var $image = ImageManipulation.getCropper(); + var $trigger = ImageManipulation.$trigger; + var formFieldId = $trigger.data('field'); + var $formField = $trigger.parent().find('#' + formFieldId); + var $formGroup = $formField.closest('.form-group'); + var cropData = $image.cropper('getData'); + var newValue = ''; + $formGroup.addClass('has-change'); + if (cropData.width > 0 && cropData.height > 0) { + newValue = JSON.stringify(cropData); + $formGroup.find('.t3js-image-manipulation-info').removeClass('hide'); + $formGroup.find('.t3js-image-manipulation-info-crop-x').text(Math.round(cropData.x) + 'px'); + $formGroup.find('.t3js-image-manipulation-info-crop-y').text(Math.round(cropData.y) + 'px'); + $formGroup.find('.t3js-image-manipulation-info-crop-width').text(Math.round(cropData.width) + 'px'); + $formGroup.find('.t3js-image-manipulation-info-crop-height').text(Math.round(cropData.height) + 'px'); + + } else { + $formGroup.find('.t3js-image-manipulation-info').addClass('hide'); + $formGroup.find('.t3js-image-manipulation-preview').addClass('hide'); + } + $formField.val(newValue); + ImageManipulation.dismiss(); + }; + + /** + * Reset crop selection + */ + ImageManipulation.reset = function() { + var $image = ImageManipulation.getCropper(); + $image.cropper('clear'); + }; + + /** + * Close the current open modal + */ + ImageManipulation.dismiss = function() { + if (ImageManipulation.currentModal) { + ImageManipulation.currentModal.modal('hide').remove(); + ImageManipulation.currentModal = null; + } + }; + + return function() { + TYPO3.ImageManipulation = ImageManipulation; + return ImageManipulation; + }(); +}); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Modal.js b/typo3/sysext/backend/Resources/Public/JavaScript/Modal.js index 2ba2f2a95742f645e815bca5879f6a9695b51b51..cfdbae8060e33fca750f19c2394aaaadce123900 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Modal.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Modal.js @@ -111,6 +111,29 @@ define('TYPO3/CMS/Backend/Modal', ['jquery', 'TYPO3/CMS/Backend/Notification', ' return $modal; }; + /** + * load URL with AJAX, append the content to the modal-body + * and trigger the callback + * + * @param {string} title + * @param {int} severity + * @param {array} buttons + * @param {string} url + * @param {string} target + * @param {function} callback + */ + Modal.loadUrl = function(title, severity, buttons, url, callback, target) { + $.get(url, function(response) { + Modal.currentModal.find(target ? target : '.modal-body').empty().append(response); + if (callback) { + callback(); + } + Modal.currentModal.trigger('modal-loaded'); + }, 'html'); + return Modal.show(title, '<p class="loadmessage"><i class="fa fa-spinner fa-spin fa-5x "></i></p>', severity, buttons); + }; + + /** * Shows a dialog * Events: @@ -233,6 +256,7 @@ define('TYPO3/CMS/Backend/Modal', ['jquery', 'TYPO3/CMS/Backend/Notification', ' $(document).on('click', '.t3js-modal-trigger', function(evt) { evt.preventDefault(); var $element = $(this); + var url = $element.data('url') || null; var title = $element.data('title') || 'Alert'; var content = $element.data('content') || 'Are you sure?'; var severity = (typeof top.TYPO3.Severity[$element.data('severity')] !== 'undefined') ? top.TYPO3.Severity[$element.data('severity')] : top.TYPO3.Severity.info; @@ -252,7 +276,13 @@ define('TYPO3/CMS/Backend/Modal', ['jquery', 'TYPO3/CMS/Backend/Notification', ' } } ]; - Modal.confirm(title, content, severity, buttons); + if (url !== null) { + var separator = (url.indexOf('?') > -1) ? '&' : '?'; + var params = $.param({data: $element.data()}); + Modal.loadUrl(title, severity, buttons, url + separator + params); + } else { + Modal.show(title, content, severity, buttons); + } }); }; diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index b3b4f36ed2374f1b30b5d3401ca5e078a464c28f..a305d2c2fa9e0c4b6fcb4074437edc8eb4154321 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -1598,7 +1598,7 @@ class DataHandler { $res = $this->checkValue_text($res, $value, $tcaFieldConf, $PP, $field); break; case 'passthrough': - + case 'image_manipulation': case 'user': $res['value'] = $value; break; @@ -1612,7 +1612,6 @@ class DataHandler { $res = $this->checkValue_radio($res, $value, $tcaFieldConf, $PP); break; case 'group': - case 'select': $res = $this->checkValue_group_select($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field); break; diff --git a/typo3/sysext/core/Classes/Page/PageRenderer.php b/typo3/sysext/core/Classes/Page/PageRenderer.php index 692b5bbf35c1be179a3988254ec6f4276bea0672..865c0b3bde66a3c6152bc748dab631b19e2d6ddd 100644 --- a/typo3/sysext/core/Classes/Page/PageRenderer.php +++ b/typo3/sysext/core/Classes/Page/PageRenderer.php @@ -1594,6 +1594,8 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface { 'datatables' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/jquery.dataTables', 'nprogress' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/nprogress', 'moment' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/moment', + 'cropper' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/cropper.min', + 'imagesloaded' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/imagesloaded.pkgd.min', 'bootstrap' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/bootstrap/bootstrap', 'twbs/bootstrap-datetimepicker' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/bootstrap-datetimepicker', 'autosize' => $this->backPath . 'sysext/core/Resources/Public/JavaScript/Contrib/autosize', diff --git a/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php b/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php index 11e687bc3715a6fd20b960028cf015bf85d55650..91675a538587315c2fa090f6f304ff0e3e24f65f 100644 --- a/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php +++ b/typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php @@ -76,9 +76,18 @@ class LocalCropScaleMaskHelper { $croppedImage = NULL; if (!empty($configuration['crop'])) { + + // check if it is a json object + $cropData = json_decode($configuration['crop']); + if ($cropData) { + $crop = implode(',', array((int)$cropData->x, (int)$cropData->y, (int)$cropData->width, (int)$cropData->height)); + } else { + $crop = $configuration['crop']; + } + $im = $gifBuilder->imageCreateFromFile($originalFileName); $croppedImage = Utility\GeneralUtility::tempnam('crop_', '.' . $sourceFile->getExtension()); - $gifBuilder->crop($im, ['crop' => $configuration['crop']]); + $gifBuilder->crop($im, ['crop' => $crop]); if ($gifBuilder->ImageWrite($im, $croppedImage)) { $originalFileName = $croppedImage; } diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index 37b5fbdaf14a7085df08fa3761e91c31c0740963..0cfc56ff232140a0271fc2e53f98b35abc05e0c0 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -735,6 +735,10 @@ return array( 'UserSettings::process' => array( 'callbackMethod' => \TYPO3\CMS\Backend\Controller\UserSettingsController::class . '->processAjaxRequest', 'csrfTokenCheck' => TRUE + ), + 'ImageManipulationWizard::getHtmlForImageManipulationWizard' => array( + 'callbackMethod' => \TYPO3\CMS\Backend\Form\Wizard\ImageManipulationWizard::class . '->getHtmlForImageManipulationWizard', + 'csrfTokenCheck' => TRUE ) ), 'toolbarItems' => array(), // Array: Registered toolbar items classes diff --git a/typo3/sysext/core/Configuration/TCA/sys_file_reference.php b/typo3/sysext/core/Configuration/TCA/sys_file_reference.php index 9499f0c9eac70da81b4ab1be78264c0a1f0613c3..cde302826507a6ceb661e1a0054b7ae6bfdfa71a 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_file_reference.php +++ b/typo3/sysext/core/Configuration/TCA/sys_file_reference.php @@ -221,10 +221,7 @@ return array( 'exclude' => 1, 'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.crop', 'config' => array( - 'type' => 'input', - 'size' => '10', - 'max' => '30', - 'placeholder' => 'x,y,w,h', + 'type' => 'image_manipulation' ) ) ), diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-65585-AddTCATypeImage_manipulation.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-65585-AddTCATypeImage_manipulation.rst new file mode 100644 index 0000000000000000000000000000000000000000..02ea06140b41174e06a6e2766f4ff1b0bbc4959e --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-65585-AddTCATypeImage_manipulation.rst @@ -0,0 +1,37 @@ +================================================= +Feature - #65585: Add TCA type image_manipulation +================================================= + +Description +=========== + +TCA type `image_manipulation` brings a image manipulation wizard to the core. + +This first version brings image cropping with the possibility to +set a certain aspect ratio for the cropped area. The +sys_file_reference.crop property is extended and can now also hold +a json string to describe the image manipulation. + +The `LocalCropScaleMaskHelper` that is used by the core +to create adjusted images is also adjusted to handle the new format. + + +Impact +====== + +There is an new TCA type column type `image_manipulation` it supports the following config: + +- file_field: string, default `uid_local` +- enableZoom: bool, default `FALSE` +- allowedExtensions: string, default `$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']` +- ratios: array, default + '1.7777777777777777' => '16:9', + '1.3333333333333333' => '4:3', + '1' => '1:1', + 'NaN' => 'Free', + +When `ratios` is set in TCA the defaults are neglected. + + +Property `sys_file_reference.crop` can now hold a string representing a json object. `LocalCropScaleMaskHelper` checks +if the it can parse the string as json. If it can it assumes it holds the properties: `x`, `y`, `width` and `height`. \ No newline at end of file diff --git a/typo3/sysext/core/Resources/Public/JavaScript/Contrib/cropper.min.js b/typo3/sysext/core/Resources/Public/JavaScript/Contrib/cropper.min.js new file mode 100644 index 0000000000000000000000000000000000000000..cb9502475ef6bc0219f3ce849889c9e9c76aac62 --- /dev/null +++ b/typo3/sysext/core/Resources/Public/JavaScript/Contrib/cropper.min.js @@ -0,0 +1,10 @@ +/*! + * Cropper v0.9.1 + * https://github.com/fengyuanchen/cropper + * + * Copyright (c) 2014-2015 Fengyuan Chen and contributors + * Released under the MIT license + * + * Date: 2015-03-21T04:58:27.265Z + */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){"use strict";function b(a){return"number"==typeof a}function c(a){return"undefined"==typeof a}function d(a,c){var d=[];return b(c)&&d.push(c),d.slice.apply(a,d)}function e(a,b){var c=d(arguments,2);return function(){return a.apply(b,c.concat(d(arguments)))}}function f(a){var b=a.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);return b&&(b[1]!==n.protocol||b[2]!==n.hostname||b[3]!==n.port)}function g(a){var b="timestamp="+(new Date).getTime();return a+(-1===a.indexOf("?")?"?":"&")+b}function h(a){return a?"rotate("+a+"deg)":"none"}function i(a,b){var c,d,e=P(a.degree)%180,f=(e>90?180-e:e)*Math.PI/180,g=Q(f),h=R(f),i=a.width,j=a.height,k=a.aspectRatio;return b?(c=i/(h+g/k),d=c/k):(c=i*h+j*g,d=i*g+j*h),{width:c,height:d}}function j(b,c){var d=a("<canvas>")[0],e=d.getContext("2d"),f=c.naturalWidth,g=c.naturalHeight,h=c.rotate,j=i({width:f,height:g,degree:h});return h?(d.width=j.width,d.height=j.height,e.save(),e.translate(j.width/2,j.height/2),e.rotate(h*Math.PI/180),e.drawImage(b,-f/2,-g/2,f,g),e.restore()):(d.width=f,d.height=g,e.drawImage(b,0,0,f,g)),d}function k(b,c){this.$element=a(b),this.options=a.extend({},k.DEFAULTS,a.isPlainObject(c)&&c),this.ready=!1,this.built=!1,this.rotated=!1,this.cropped=!1,this.disabled=!1,this.load()}var l=a(window),m=a(document),n=window.location,o=".cropper",p=/^(e|n|w|s|ne|nw|sw|se|all|crop|move|zoom)$/,q="cropper-modal",r="cropper-hide",s="cropper-hidden",t="cropper-invisible",u="cropper-move",v="cropper-crop",w="cropper-disabled",x="cropper-bg",y="mousedown touchstart",z="mousemove touchmove",A="mouseup mouseleave touchend touchleave touchcancel",B="wheel mousewheel DOMMouseScroll",C="dblclick",D="resize"+o,E="build"+o,F="built"+o,G="dragstart"+o,H="dragmove"+o,I="dragend"+o,J="zoomin"+o,K="zoomout"+o,L=a.isFunction(a("<canvas>")[0].getContext),M=Math.sqrt,N=Math.min,O=Math.max,P=Math.abs,Q=Math.sin,R=Math.cos,S=parseFloat,T={};T.load=function(b){var c,d,e,h=this.options,i=this.$element;b||(i.is("img")?b=i.prop("src"):i.is("canvas")&&L&&(b=i[0].toDataURL())),b&&(d=a.Event(E),i.one(E,h.build).trigger(d),d.isDefaultPrevented()||(h.checkImageOrigin&&f(b)&&(c=" crossOrigin",i.prop("crossOrigin")||(b=g(b))),this.$clone=e=a("<img>"),e.one("load",a.proxy(function(){var a=e.prop("naturalWidth")||e.width(),c=e.prop("naturalHeight")||e.height();this.image={naturalWidth:a,naturalHeight:c,aspectRatio:a/c,rotate:0},this.url=b,this.ready=!0,this.build()},this)).attr({src:b,crossOrigin:c}),e.addClass(r).insertAfter(i)))},T.build=function(){var b,c,d=this.$element,e=this.$clone,f=this.options;this.ready&&(this.built&&this.unbuild(),this.$cropper=b=a(k.TEMPLATE),d.addClass(s),e.removeClass(r),this.$container=d.parent().append(b),this.$canvas=b.find(".cropper-canvas").append(e),this.$dragBox=b.find(".cropper-drag-box"),this.$cropBox=c=b.find(".cropper-crop-box"),this.$viewBox=b.find(".cropper-view-box"),this.addListeners(),this.initPreview(),f.aspectRatio=S(f.aspectRatio)||0/0,f.autoCrop?(this.cropped=!0,f.modal&&this.$dragBox.addClass(q)):c.addClass(s),f.background&&b.addClass(x),f.highlight||c.find(".cropper-face").addClass(t),f.guides||c.find(".cropper-dashed").addClass(s),f.movable||c.find(".cropper-face").data("drag","move"),f.resizable||c.find(".cropper-line, .cropper-point").addClass(s),this.setDragMode(f.dragCrop?"crop":"move"),this.built=!0,this.render(),d.one(F,f.built).trigger(F))},T.unbuild=function(){this.built&&(this.built=!1,this.removeListeners(),this.$preview.empty(),this.$preview=null,this.$viewBox=null,this.$cropBox=null,this.$dragBox=null,this.$canvas=null,this.$container=null,this.$cropper.remove(),this.$cropper=null)},a.extend(T,{render:function(){this.initContainer(),this.initCanvas(),this.initCropBox()},initContainer:function(){var a=this.$element,b=this.$container,c=this.$cropper,d=this.options;c.addClass(s),a.removeClass(s),c.css(this.container={width:O(b.width(),S(d.minContainerWidth)||200),height:O(b.height(),S(d.minContainerHeight)||100)}),a.addClass(s),c.removeClass(s)},initCanvas:function(){var b=this.options,c=this.container,d=c.width,e=c.height,f=this.image,g=f.aspectRatio,h={aspectRatio:g,width:d,height:e,left:0,top:0,minLeft:-d,minTop:-e,maxLeft:d,maxTop:e,minWidth:0,minHeight:0,maxWidth:1/0,maxHeight:1/0};e*g>d?b.strict?h.width=e*g:h.height=d/g:b.strict?h.height=d/g:h.width=e*g,h.oldLeft=h.left=(d-h.width)/2,h.oldTop=h.top=(e-h.height)/2,this.canvas=h,this.limitCanvas(),this.initialImage=a.extend({},f),this.initialCanvas=a.extend({},this.canvas),this.renderCanvas()},limitCanvas:function(){var a=this.options,b=this.container,c=b.width,d=b.height,e=this.canvas,f=e.aspectRatio,g=c,h=d;d*f>c?a.strict?g=d*f:h=c/f:a.strict?h=c/f:g=d*f,a.strict?(e.minWidth=g,e.minHeight=h,e.maxLeft=0,e.maxTop=0,e.minLeft=c-g,e.minTop=d-h):(e.minLeft=-g,e.minTop=-h)},renderCanvas:function(b){var c,d,e,f=this.options,g=this.container,j=this.canvas,k=this.image;this.rotated&&(this.rotated=!1,e=i({width:k.width,height:k.height,degree:k.rotate}),c=e.width/e.height,c!==j.aspectRatio&&(j.left-=(e.width-j.width)/2,j.top-=(e.height-j.height)/2,j.width=e.width,j.height=e.height,j.aspectRatio=c,this.limitCanvas())),(j.width>j.maxWidth||j.width<j.minWidth)&&(j.left=j.oldLeft),(j.height>j.maxHeight||j.height<j.minHeight)&&(j.top=j.oldTop),j.width=N(O(j.width,j.minWidth),j.maxWidth),j.height=N(O(j.height,j.minHeight),j.maxHeight),f.strict?(j.minLeft=g.width-j.width,j.minTop=g.height-j.height):(j.minLeft=-j.width,j.minTop=-j.height),j.oldLeft=j.left=N(O(j.left,j.minLeft),j.maxLeft),j.oldTop=j.top=N(O(j.top,j.minTop),j.maxTop),this.$canvas.css({width:j.width,height:j.height,left:j.left,top:j.top}),k.rotate?(d=i({width:j.width,height:j.height,degree:k.rotate,aspectRatio:k.aspectRatio},!0),a.extend(k,{width:d.width,height:d.height,left:(j.width-d.width)/2,top:(j.height-d.height)/2})):a.extend(k,{width:j.width,height:j.height,left:0,top:0}),this.$clone.css({width:k.width,height:k.height,marginLeft:k.left,marginTop:k.top,transform:h(k.rotate)}),b&&(this.preview(),f.crop&&f.crop.call(this.$element,this.getData()))},initCropBox:function(){var b=this.options,c=this.container,d=this.canvas,e=b.strict,f=b.aspectRatio,g=S(b.minCropBoxWidth)||0,h=S(b.minCropBoxHeight)||0,i=S(b.autoCropArea)||.8,j=c.width,k=c.height,l={width:e?j:d.width,height:e?k:d.height,minWidth:g,minHeight:h,maxWidth:j,maxHeight:k};f&&(k*f>j?(l.height=l.width/f,l.maxHeight=j/f):(l.width=l.height*f,l.maxWidth=k*f),e||(l.height*d.aspectRatio>l.width?(l.height=d.height,l.width=l.height*f):(l.width=d.width,l.height=l.width/f)),g?l.minHeight=l.minWidth/f:h&&(l.minWidth=l.minHeight*f)),l.minWidth=N(l.maxWidth,l.minWidth),l.minHeight=N(l.maxHeight,l.minHeight),l.width=O(l.minWidth,l.width*i),l.height=O(l.minHeight,l.height*i),l.oldLeft=l.left=(j-l.width)/2,l.oldTop=l.top=(k-l.height)/2,this.initialCropBox=a.extend({},l),this.cropBox=l,this.cropped&&this.renderCropBox()},renderCropBox:function(){var a=this.options,b=this.container,c=b.width,d=b.height,e=this.$cropBox,f=this.cropBox;(f.width>f.maxWidth||f.width<f.minWidth)&&(f.left=f.oldLeft),(f.height>f.maxHeight||f.height<f.minHeight)&&(f.top=f.oldTop),f.width=N(O(f.width,f.minWidth),f.maxWidth),f.height=N(O(f.height,f.minHeight),f.maxHeight),f.oldLeft=f.left=N(O(f.left,0),c-f.width),f.oldTop=f.top=N(O(f.top,0),d-f.height),a.movable&&e.find(".cropper-face").data("drag",f.width===c&&f.height===d?"move":"all"),e.css({width:f.width,height:f.height,left:f.left,top:f.top}),this.disabled||(this.preview(),a.crop&&a.crop.call(this.$element,this.getData()))}}),T.initPreview=function(){var b=this.url;this.$preview=a(this.options.preview),this.$viewBox.html('<img src="'+b+'">'),this.$preview.each(function(){var c=a(this);c.data({width:c.width(),height:c.height()}).html('<img src="'+b+'" style="display:block;width:100%;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important;image-orientation: 0deg!important">')})},T.preview=function(){var b=this.image,c=this.canvas,d=this.cropBox,e=b.width,f=b.height,g=d.left-c.left-b.left,i=d.top-c.top-b.top,j=b.rotate;this.cropped&&!this.disabled&&(this.$viewBox.find("img").css({width:e,height:f,marginLeft:-g,marginTop:-i,transform:h(j)}),this.$preview.each(function(){var b=a(this),c=b.data(),k=c.width/d.width,l=c.width,m=d.height*k;m>c.height&&(k=c.height/d.height,l=d.width*k,m=c.height),b.width(l).height(m).find("img").css({width:e*k,height:f*k,marginLeft:-g*k,marginTop:-i*k,transform:h(j)})}))},T.addListeners=function(){var b=this.options;this.$element.on(G,b.dragstart).on(H,b.dragmove).on(I,b.dragend).on(J,b.zoomin).on(K,b.zoomout),this.$cropper.on(y,a.proxy(this.dragstart,this)).on(C,a.proxy(this.dblclick,this)),b.zoomable&&b.mouseWheelZoom&&this.$cropper.on(B,a.proxy(this.wheel,this)),m.on(z,this._dragmove=e(this.dragmove,this)).on(A,this._dragend=e(this.dragend,this)),b.responsive&&l.on(D,this._resize=e(this.resize,this))},T.removeListeners=function(){var a=this.options;this.$element.off(G,a.dragstart).off(H,a.dragmove).off(I,a.dragend).off(J,a.zoomin).off(K,a.zoomout),this.$cropper.off(y,this.dragstart).off(C,this.dblclick),a.zoomable&&a.mouseWheelZoom&&this.$cropper.off(B,this.wheel),m.off(z,this._dragmove).off(A,this._dragend),a.responsive&&l.off(D,this._resize)},a.extend(T,{resize:function(){var b,c,d,e=this.$container,f=this.container;this.disabled||(d=e.width()/f.width,(1!==d||e.height()!==f.height)&&(b=this.getCanvasData(),c=this.getCropBoxData(),this.render(),this.setCanvasData(a.each(b,function(a,c){b[a]=c*d})),this.setCropBoxData(a.each(c,function(a,b){c[a]=b*d}))))},dblclick:function(){this.disabled||this.setDragMode(this.$dragBox.hasClass(v)?"move":"crop")},wheel:function(a){var b=a.originalEvent,c=1;this.disabled||(a.preventDefault(),b.deltaY?c=b.deltaY>0?1:-1:b.wheelDelta?c=-b.wheelDelta/120:b.detail&&(c=b.detail>0?1:-1),this.zoom(.1*c))},dragstart:function(b){var c,d,e,f=this.options,g=b.originalEvent,h=g&&g.touches,i=b;if(!this.disabled){if(h){if(e=h.length,e>1){if(!f.zoomable||!f.touchDragZoom||2!==e)return;i=h[1],this.startX2=i.pageX,this.startY2=i.pageY,c="zoom"}i=h[0]}if(c=c||a(i.target).data("drag"),p.test(c)){if(b.preventDefault(),d=a.Event(G,{originalEvent:g,dragType:c}),this.$element.trigger(d),d.isDefaultPrevented())return;this.dragType=c,this.cropping=!1,this.startX=i.pageX,this.startY=i.pageY,"crop"===c&&(this.cropping=!0,this.$dragBox.addClass(q))}}},dragmove:function(b){var c,d,e=this.options,f=b.originalEvent,g=f&&f.touches,h=b,i=this.dragType;if(!this.disabled){if(g){if(d=g.length,d>1){if(!e.zoomable||!e.touchDragZoom||2!==d)return;h=g[1],this.endX2=h.pageX,this.endY2=h.pageY}h=g[0]}if(i){if(b.preventDefault(),c=a.Event(H,{originalEvent:f,dragType:i}),this.$element.trigger(c),c.isDefaultPrevented())return;this.endX=h.pageX,this.endY=h.pageY,this.change()}}},dragend:function(b){var c,d=this.dragType;if(!this.disabled&&d){if(b.preventDefault(),c=a.Event(I,{originalEvent:b.originalEvent,dragType:d}),this.$element.trigger(c),c.isDefaultPrevented())return;this.cropping&&(this.cropping=!1,this.$dragBox.toggleClass(q,this.cropped&&this.options.modal)),this.dragType=""}}}),a.extend(T,{reset:function(){this.disabled||(this.image=a.extend({},this.initialImage),this.canvas=a.extend({},this.initialCanvas),this.renderCanvas(),this.cropped&&(this.cropBox=a.extend({},this.initialCropBox),this.renderCropBox()))},clear:function(){this.cropped&&!this.disabled&&(a.extend(this.cropBox,{left:0,top:0,width:0,height:0}),this.renderCropBox(),this.cropped=!1,this.$dragBox.removeClass(q),this.$cropBox.addClass(s))},destroy:function(){var a=this.$element;this.ready||this.$clone.off("load").remove(),this.unbuild(),a.removeClass(s).removeData("cropper")},replace:function(a){this.ready&&!this.disabled&&a&&this.load(a)},enable:function(){this.built&&(this.disabled=!1,this.$cropper.removeClass(w))},disable:function(){this.built&&(this.disabled=!0,this.$cropper.addClass(w))},move:function(a,c){var d=this.canvas;this.built&&!this.disabled&&b(a)&&b(c)&&(d.left+=a,d.top+=c,this.renderCanvas(!0))},zoom:function(b){var c,d,e,f=this.canvas;if(b=S(b),b&&this.built&&!this.disabled&&this.options.zoomable){if(c=a.Event(b>0?J:K),this.$element.trigger(c),c.isDefaultPrevented())return;b=-1>=b?1/(1-b):1>=b?1+b:b,d=f.width*b,e=f.height*b,f.left-=(d-f.width)/2,f.top-=(e-f.height)/2,f.width=d,f.height=e,this.renderCanvas(!0),this.setDragMode("move")}},rotate:function(a){var b=this.image;a=S(a),a&&this.built&&!this.disabled&&this.options.rotatable&&(b.rotate=(b.rotate+a)%360,this.rotated=!0,this.renderCanvas(!0))},getData:function(){var b,c,d=this.cropBox,e=this.canvas,f=this.image,g=f.rotate;return this.built&&this.cropped?(c={x:d.left-e.left,y:d.top-e.top,width:d.width,height:d.height},b=f.width/f.naturalWidth,a.each(c,function(a,d){d/=b,c[a]=d})):c={x:0,y:0,width:0,height:0},c.rotate=g,c},getContainerData:function(){return this.built?this.container:{}},getImageData:function(){return this.ready?this.image:{}},getCanvasData:function(){var a,b=this.canvas;return this.built&&(a={left:b.left,top:b.top,width:b.width,height:b.height}),a||{}},setCanvasData:function(c){var d=this.canvas,e=d.aspectRatio;this.built&&!this.disabled&&a.isPlainObject(c)&&(b(c.left)&&(d.left=c.left),b(c.top)&&(d.top=c.top),b(c.width)?(d.width=c.width,d.height=c.width/e):b(c.height)&&(d.height=c.height,d.width=c.height*e),this.renderCanvas(!0))},getCropBoxData:function(){var a,b=this.cropBox;return this.built&&this.cropped&&(a={left:b.left,top:b.top,width:b.width,height:b.height}),a||{}},setCropBoxData:function(c){var d=this.cropBox,e=this.options.aspectRatio;this.built&&this.cropped&&!this.disabled&&a.isPlainObject(c)&&(b(c.left)&&(d.left=c.left),b(c.top)&&(d.top=c.top),e?b(c.width)?(d.width=c.width,d.height=d.width/e):b(c.height)&&(d.height=c.height,d.width=d.height*e):(b(c.width)&&(d.width=c.width),b(c.height)&&(d.height=c.height)),this.renderCropBox())},getCroppedCanvas:function(b){var c,d,e,f,g,h,i,k,l,m,n;if(this.built&&this.cropped&&L)return a.isPlainObject(b)||(b={}),n=this.getData(),c=n.width,d=n.height,k=c/d,a.isPlainObject(b)&&(g=b.width,h=b.height,g?(h=g/k,i=g/c):h&&(g=h*k,i=h/d)),e=g||c,f=h||d,l=a("<canvas>")[0],l.width=e,l.height=f,m=l.getContext("2d"),b.fillColor&&(m.fillStyle=b.fillColor,m.fillRect(0,0,e,f)),m.drawImage.apply(m,function(){var a,b,e,f,g,h,k=j(this.$clone[0],this.image),l=k.width,m=k.height,o=[k],p=n.x,q=n.y;return-c>=p||p>l?p=a=e=g=0:0>=p?(e=-p,p=0,a=g=N(l,c+p)):l>=p&&(e=0,a=g=N(c,l-p)),0>=a||-d>=q||q>m?q=b=f=h=0:0>=q?(f=-q,q=0,b=h=N(m,d+q)):m>=q&&(f=0,b=h=N(d,m-q)),o.push(p,q,a,b),i&&(e*=i,f*=i,g*=i,h*=i),g>0&&h>0&&o.push(e,f,g,h),o}.call(this)),l},setAspectRatio:function(a){var b=this.options;this.disabled||c(a)||(b.aspectRatio=S(a)||0/0,this.built&&this.initCropBox())},setDragMode:function(a){var b=this.$dragBox,c=!1,d=!1;if(this.ready&&!this.disabled){switch(a){case"crop":this.options.dragCrop?(c=!0,b.data("drag",a)):d=!0;break;case"move":d=!0,b.data("drag",a);break;default:b.removeData("drag")}b.toggleClass(v,c).toggleClass(u,d)}}}),T.change=function(){var a,b=this.dragType,c=this.canvas,d=this.container,e=d.width,f=d.height,g=this.cropBox,h=g.width,i=g.height,j=g.left,k=g.top,l=j+h,m=k+i,n=!0,o=this.options.aspectRatio,p={x:this.endX-this.startX,y:this.endY-this.startY};switch(o&&(p.X=p.y*o,p.Y=p.x/o),b){case"all":j+=p.x,k+=p.y;break;case"e":if(p.x>=0&&(l>=e||o&&(0>=k||m>=f))){n=!1;break}h+=p.x,o&&(i=h/o,k-=p.Y/2),0>h&&(b="w",h=0);break;case"n":if(p.y<=0&&(0>=k||o&&(0>=j||l>=e))){n=!1;break}i-=p.y,k+=p.y,o&&(h=i*o,j+=p.X/2),0>i&&(b="s",i=0);break;case"w":if(p.x<=0&&(0>=j||o&&(0>=k||m>=f))){n=!1;break}h-=p.x,j+=p.x,o&&(i=h/o,k+=p.Y/2),0>h&&(b="e",h=0);break;case"s":if(p.y>=0&&(m>=f||o&&(0>=j||l>=e))){n=!1;break}i+=p.y,o&&(h=i*o,j-=p.X/2),0>i&&(b="n",i=0);break;case"ne":if(o){if(p.y<=0&&(0>=k||l>=e)){n=!1;break}i-=p.y,k+=p.y,h=i*o}else p.x>=0?e>l?h+=p.x:p.y<=0&&0>=k&&(n=!1):h+=p.x,p.y<=0?k>0&&(i-=p.y,k+=p.y):(i-=p.y,k+=p.y);0>h&&0>i?(b="sw",i=0,h=0):0>h?(b="nw",h=0):0>i&&(b="se",i=0);break;case"nw":if(o){if(p.y<=0&&(0>=k||0>=j)){n=!1;break}i-=p.y,k+=p.y,h=i*o,j+=p.X}else p.x<=0?j>0?(h-=p.x,j+=p.x):p.y<=0&&0>=k&&(n=!1):(h-=p.x,j+=p.x),p.y<=0?k>0&&(i-=p.y,k+=p.y):(i-=p.y,k+=p.y);0>h&&0>i?(b="se",i=0,h=0):0>h?(b="ne",h=0):0>i&&(b="sw",i=0);break;case"sw":if(o){if(p.x<=0&&(0>=j||m>=f)){n=!1;break}h-=p.x,j+=p.x,i=h/o}else p.x<=0?j>0?(h-=p.x,j+=p.x):p.y>=0&&m>=f&&(n=!1):(h-=p.x,j+=p.x),p.y>=0?f>m&&(i+=p.y):i+=p.y;0>h&&0>i?(b="ne",i=0,h=0):0>h?(b="se",h=0):0>i&&(b="nw",i=0);break;case"se":if(o){if(p.x>=0&&(l>=e||m>=f)){n=!1;break}h+=p.x,i=h/o}else p.x>=0?e>l?h+=p.x:p.y>=0&&m>=f&&(n=!1):h+=p.x,p.y>=0?f>m&&(i+=p.y):i+=p.y;0>h&&0>i?(b="nw",i=0,h=0):0>h?(b="sw",h=0):0>i&&(b="ne",i=0);break;case"move":c.left+=p.x,c.top+=p.y,this.renderCanvas(!0),n=!1;break;case"zoom":this.zoom(function(a,b,c,d){var e=M(a*a+b*b),f=M(c*c+d*d);return(f-e)/e}(P(this.startX-this.startX2),P(this.startY-this.startY2),P(this.endX-this.endX2),P(this.endY-this.endY2))),this.startX2=this.endX2,this.startY2=this.endY2,n=!1;break;case"crop":p.x&&p.y&&(a=this.$cropper.offset(),j=this.startX-a.left,k=this.startY-a.top,h=g.minWidth,i=g.minHeight,p.x>0?p.y>0?b="se":(b="ne",k-=i):p.y>0?(b="sw",j-=h):(b="nw",j-=h,k-=i),this.cropped||(this.cropped=!0,this.$cropBox.removeClass(s)))}n&&(g.width=h,g.height=i,g.left=j,g.top=k,this.dragType=b,this.renderCropBox()),this.startX=this.endX,this.startY=this.endY},a.extend(k.prototype,T),k.DEFAULTS={aspectRatio:0/0,autoCropArea:.8,crop:null,preview:"",strict:!0,responsive:!0,checkImageOrigin:!0,modal:!0,guides:!0,highlight:!0,background:!0,autoCrop:!0,dragCrop:!0,movable:!0,resizable:!0,rotatable:!0,zoomable:!0,touchDragZoom:!0,mouseWheelZoom:!0,minCropBoxWidth:0,minCropBoxHeight:0,minContainerWidth:200,minContainerHeight:100,build:null,built:null,dragstart:null,dragmove:null,dragend:null,zoomin:null,zoomout:null},k.setDefaults=function(b){a.extend(k.DEFAULTS,b)},k.TEMPLATE=function(a,b){return b=b.split(","),a.replace(/\d+/g,function(a){return b[a]})}('<0 6="5-container"><0 6="5-canvas"></0><0 6="5-2-9" 3-2="move"></0><0 6="5-crop-9"><1 6="5-view-9"></1><1 6="5-8 8-h"></1><1 6="5-8 8-v"></1><1 6="5-face" 3-2="all"></1><1 6="5-7 7-e" 3-2="e"></1><1 6="5-7 7-n" 3-2="n"></1><1 6="5-7 7-w" 3-2="w"></1><1 6="5-7 7-s" 3-2="s"></1><1 6="5-4 4-e" 3-2="e"></1><1 6="5-4 4-n" 3-2="n"></1><1 6="5-4 4-w" 3-2="w"></1><1 6="5-4 4-s" 3-2="s"></1><1 6="5-4 4-ne" 3-2="ne"></1><1 6="5-4 4-nw" 3-2="nw"></1><1 6="5-4 4-sw" 3-2="sw"></1><1 6="5-4 4-se" 3-2="se"></1></0></0>',"div,span,drag,data,point,cropper,class,line,dashed,box"),k.other=a.fn.cropper,a.fn.cropper=function(b){var e,f=d(arguments,1);return this.each(function(){var c,d=a(this),g=d.data("cropper");g||d.data("cropper",g=new k(this,b)),"string"==typeof b&&a.isFunction(c=g[b])&&(e=c.apply(g,f))}),c(e)?this:e},a.fn.cropper.Constructor=k,a.fn.cropper.setDefaults=k.setDefaults,a.fn.cropper.noConflict=function(){return a.fn.cropper=k.other,this}}); \ No newline at end of file diff --git a/typo3/sysext/core/Resources/Public/JavaScript/Contrib/imagesloaded.pkgd.min.js b/typo3/sysext/core/Resources/Public/JavaScript/Contrib/imagesloaded.pkgd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..d66f658937d8f5b6d96f969aa3ac5c421c397338 --- /dev/null +++ b/typo3/sysext/core/Resources/Public/JavaScript/Contrib/imagesloaded.pkgd.min.js @@ -0,0 +1,7 @@ +/*! + * imagesLoaded PACKAGED v3.1.8 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +(function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,o=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;e.length>t;t+=1)n.push(e[t].listener);return n},i.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},i.addListener=function(e,n){var i,r=this.getListenersAsObject(e),o="object"==typeof n;for(i in r)r.hasOwnProperty(i)&&-1===t(r[i],n)&&r[i].push(o?n:{listener:n,once:!1});return this},i.on=n("addListener"),i.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},i.once=n("addOnceListener"),i.defineEvent=function(e){return this.getListeners(e),this},i.defineEvents=function(e){for(var t=0;e.length>t;t+=1)this.defineEvent(e[t]);return this},i.removeListener=function(e,n){var i,r,o=this.getListenersAsObject(e);for(r in o)o.hasOwnProperty(r)&&(i=t(o[r],n),-1!==i&&o[r].splice(i,1));return this},i.off=n("removeListener"),i.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},i.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},i.manipulateListeners=function(e,t,n){var i,r,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(i=n.length;i--;)o.call(this,t,n[i]);else for(i in t)t.hasOwnProperty(i)&&(r=t[i])&&("function"==typeof r?o.call(this,i,r):s.call(this,i,r));return this},i.removeEvent=function(e){var t,n=typeof e,i=this._getEvents();if("string"===n)delete i[e];else if("object"===n)for(t in i)i.hasOwnProperty(t)&&e.test(t)&&delete i[t];else delete this._events;return this},i.removeAllListeners=n("removeEvent"),i.emitEvent=function(e,t){var n,i,r,o,s=this.getListenersAsObject(e);for(r in s)if(s.hasOwnProperty(r))for(i=s[r].length;i--;)n=s[r][i],n.once===!0&&this.removeListener(e,n.listener),o=n.listener.apply(this,t||[]),o===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},i.trigger=n("emitEvent"),i.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},i.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},i._getOnceReturnValue=function(){return this.hasOwnProperty("_onceReturnValue")?this._onceReturnValue:!0},i._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return r.EventEmitter=o,e},"function"==typeof define&&define.amd?define("eventEmitter/EventEmitter",[],function(){return e}):"object"==typeof module&&module.exports?module.exports=e:this.EventEmitter=e}).call(this),function(e){function t(t){var n=e.event;return n.target=n.target||n.srcElement||t,n}var n=document.documentElement,i=function(){};n.addEventListener?i=function(e,t,n){e.addEventListener(t,n,!1)}:n.attachEvent&&(i=function(e,n,i){e[n+i]=i.handleEvent?function(){var n=t(e);i.handleEvent.call(i,n)}:function(){var n=t(e);i.call(e,n)},e.attachEvent("on"+n,e[n+i])});var r=function(){};n.removeEventListener?r=function(e,t,n){e.removeEventListener(t,n,!1)}:n.detachEvent&&(r=function(e,t,n){e.detachEvent("on"+t,e[t+n]);try{delete e[t+n]}catch(i){e[t+n]=void 0}});var o={bind:i,unbind:r};"function"==typeof define&&define.amd?define("eventie/eventie",o):e.eventie=o}(this),function(e,t){"function"==typeof define&&define.amd?define(["eventEmitter/EventEmitter","eventie/eventie"],function(n,i){return t(e,n,i)}):"object"==typeof exports?module.exports=t(e,require("wolfy87-eventemitter"),require("eventie")):e.imagesLoaded=t(e,e.EventEmitter,e.eventie)}(window,function(e,t,n){function i(e,t){for(var n in t)e[n]=t[n];return e}function r(e){return"[object Array]"===d.call(e)}function o(e){var t=[];if(r(e))t=e;else if("number"==typeof e.length)for(var n=0,i=e.length;i>n;n++)t.push(e[n]);else t.push(e);return t}function s(e,t,n){if(!(this instanceof s))return new s(e,t);"string"==typeof e&&(e=document.querySelectorAll(e)),this.elements=o(e),this.options=i({},this.options),"function"==typeof t?n=t:i(this.options,t),n&&this.on("always",n),this.getImages(),a&&(this.jqDeferred=new a.Deferred);var r=this;setTimeout(function(){r.check()})}function f(e){this.img=e}function c(e){this.src=e,v[e]=this}var a=e.jQuery,u=e.console,h=u!==void 0,d=Object.prototype.toString;s.prototype=new t,s.prototype.options={},s.prototype.getImages=function(){this.images=[];for(var e=0,t=this.elements.length;t>e;e++){var n=this.elements[e];"IMG"===n.nodeName&&this.addImage(n);var i=n.nodeType;if(i&&(1===i||9===i||11===i))for(var r=n.querySelectorAll("img"),o=0,s=r.length;s>o;o++){var f=r[o];this.addImage(f)}}},s.prototype.addImage=function(e){var t=new f(e);this.images.push(t)},s.prototype.check=function(){function e(e,r){return t.options.debug&&h&&u.log("confirm",e,r),t.progress(e),n++,n===i&&t.complete(),!0}var t=this,n=0,i=this.images.length;if(this.hasAnyBroken=!1,!i)return this.complete(),void 0;for(var r=0;i>r;r++){var o=this.images[r];o.on("confirm",e),o.check()}},s.prototype.progress=function(e){this.hasAnyBroken=this.hasAnyBroken||!e.isLoaded;var t=this;setTimeout(function(){t.emit("progress",t,e),t.jqDeferred&&t.jqDeferred.notify&&t.jqDeferred.notify(t,e)})},s.prototype.complete=function(){var e=this.hasAnyBroken?"fail":"done";this.isComplete=!0;var t=this;setTimeout(function(){if(t.emit(e,t),t.emit("always",t),t.jqDeferred){var n=t.hasAnyBroken?"reject":"resolve";t.jqDeferred[n](t)}})},a&&(a.fn.imagesLoaded=function(e,t){var n=new s(this,e,t);return n.jqDeferred.promise(a(this))}),f.prototype=new t,f.prototype.check=function(){var e=v[this.img.src]||new c(this.img.src);if(e.isConfirmed)return this.confirm(e.isLoaded,"cached was confirmed"),void 0;if(this.img.complete&&void 0!==this.img.naturalWidth)return this.confirm(0!==this.img.naturalWidth,"naturalWidth"),void 0;var t=this;e.on("confirm",function(e,n){return t.confirm(e.isLoaded,n),!0}),e.check()},f.prototype.confirm=function(e,t){this.isLoaded=e,this.emit("confirm",this,t)};var v={};return c.prototype=new t,c.prototype.check=function(){if(!this.isChecked){var e=new Image;n.bind(e,"load",this),n.bind(e,"error",this),e.src=this.src,this.isChecked=!0}},c.prototype.handleEvent=function(e){var t="on"+e.type;this[t]&&this[t](e)},c.prototype.onload=function(e){this.confirm(!0,"onload"),this.unbindProxyEvents(e)},c.prototype.onerror=function(e){this.confirm(!1,"onerror"),this.unbindProxyEvents(e)},c.prototype.confirm=function(e,t){this.isConfirmed=!0,this.isLoaded=e,this.emit("confirm",this,t)},c.prototype.unbindProxyEvents=function(e){n.unbind(e.target,"load",this),n.unbind(e.target,"error",this)},s}); \ No newline at end of file diff --git a/typo3/sysext/core/ext_tables.sql b/typo3/sysext/core/ext_tables.sql index 7e714dddabca01971e12c1264d65f1120ac0c236..a1e6c76959ea05bc1104cfaac17cadb6e5ea6e8b 100644 --- a/typo3/sysext/core/ext_tables.sql +++ b/typo3/sysext/core/ext_tables.sql @@ -412,7 +412,7 @@ CREATE TABLE sys_file_reference ( alternative tinytext, link varchar(1024) DEFAULT '' NOT NULL, downloadname tinytext, - crop varchar(64) DEFAULT '' NOT NULL, + crop varchar(4000) DEFAULT '' NOT NULL, PRIMARY KEY (uid), KEY parent (pid,deleted), diff --git a/typo3/sysext/lang/locallang_tca.xlf b/typo3/sysext/lang/locallang_tca.xlf index 0bcd5a81baefea64e3670bfb6ba99ad308a0d4aa..abd0510fec7f9b8eb2938905bb911458e37ff993 100644 --- a/typo3/sysext/lang/locallang_tca.xlf +++ b/typo3/sysext/lang/locallang_tca.xlf @@ -544,7 +544,7 @@ <source>Link</source> </trans-unit> <trans-unit id="sys_file_reference.crop"> - <source>Crop image</source> + <source>Image manipulation</source> </trans-unit> <trans-unit id="sys_file_collection"> <source>File collection</source> diff --git a/typo3/sysext/lang/locallang_wizards.xlf b/typo3/sysext/lang/locallang_wizards.xlf index e77917f9e93e1efc528a0fe08e383ca532acba33..02d04ae9af72af3a035c24014f763fb0c3db0c76 100644 --- a/typo3/sysext/lang/locallang_wizards.xlf +++ b/typo3/sysext/lang/locallang_wizards.xlf @@ -258,6 +258,78 @@ <trans-unit id="grid_columnHelp"> <source>The column position defines in which area the content is rendered in the frontend.</source> </trans-unit> + <trans-unit id="imwizard.open-editor"> + <source>Open Editor</source> + </trans-unit> + <trans-unit id="imwizard.supported-types-message"> + <source>Image manipulation is only available for supported types:</source> + </trans-unit> + <trans-unit id="imwizard.no-image-found"> + <source>No image found</source> + </trans-unit> + <trans-unit id="imwizard.no-image-found-message"> + <source>No image found. Could also be that image is present but that the original dimensions are unknow.</source> + </trans-unit> + <trans-unit id="imwizard.image-manipulation"> + <source>Image manipulation</source> + </trans-unit> + <trans-unit id="imwizard.image-title"> + <source>Title</source> + </trans-unit> + <trans-unit id="imwizard.original-dimensions"> + <source>Original dimensions</source> + </trans-unit> + <trans-unit id="imwizard.reset"> + <source>Reset</source> + </trans-unit> + <trans-unit id="imwizard.accept"> + <source>Accept</source> + </trans-unit> + <trans-unit id="imwizard.cancel"> + <source>Cancel</source> + </trans-unit> + <trans-unit id="imwizard.aspect-ratio"> + <source>Aspect ratio</source> + </trans-unit> + <trans-unit id="imwizard.set-aspect-ratio"> + <source>Set Aspect Ratio</source> + </trans-unit> + <trans-unit id="imwizard.ratio.16_9"> + <source>16:9</source> + </trans-unit> + <trans-unit id="imwizard.ratio.4_3"> + <source>4:3</source> + </trans-unit> + <trans-unit id="imwizard.ratio.1_1"> + <source>1:1</source> + </trans-unit> + <trans-unit id="imwizard.ratio.free"> + <source>Free</source> + </trans-unit> + <trans-unit id="imwizard.zoom"> + <source>Zoom</source> + </trans-unit> + <trans-unit id="imwizard.zoom-in"> + <source>Zoom in</source> + </trans-unit> + <trans-unit id="imwizard.zoom-out"> + <source>Zoom out</source> + </trans-unit> + <trans-unit id="imwizard.selection"> + <source>Selected Size</source> + </trans-unit> + <trans-unit id="imwizard.crop-x"> + <source>x:</source> + </trans-unit> + <trans-unit id="imwizard.crop-y"> + <source>y:</source> + </trans-unit> + <trans-unit id="imwizard.crop-width"> + <source>width:</source> + </trans-unit> + <trans-unit id="imwizard.crop-height"> + <source>height:</source> + </trans-unit> </body> </file> </xliff> diff --git a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_cropper.less b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_cropper.less new file mode 100644 index 0000000000000000000000000000000000000000..708bb1802cf4e15f29dcbe8748d31f64733bbb30 --- /dev/null +++ b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_cropper.less @@ -0,0 +1,19 @@ +.cropper-line { + background-color: #FFFFFF; +} +.cropper-point { + background-color: #FFFFFF; +} +.cropper-point.point-se:before { + background-color: #FFFFFF; +} +.cropper-view-box { + outline: 1px dashed #6699ff; + outline-color: rgb(255, 255, 255, 0.75); +} +.cropper-bg { + background-image: data-uri("../../Images/cropper-background.png"); +} +.cropper-image-container { + +} \ No newline at end of file diff --git a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_modal.less b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_modal.less index 59a1ba7e8fab62148490dcbd80d9fbd800702c95..ca81489627f52baf2a2ebed8831c2384a2d94033 100644 --- a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_modal.less +++ b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_modal.less @@ -49,7 +49,6 @@ color: @modal-warning-color; border-bottom-color: @modal-warning-border; } - &.t3-modal-danger .modal-header { background-color: @modal-danger-bg; color: @modal-danger-color; @@ -84,4 +83,74 @@ .transition(margin-top .1s ease-in); border: none; overflow: hidden; + + .loadmessage { + text-align: center; + color: @gray-darker; + } +} + +.modal-resize { + &.fade .modal-dialog { + .transition-property(~"height, width"); + .transition-duration(.35s); + .transition-timing-function(ease); + } +} + +.modal-image-manipulation { + .modal-body { + .col-lg-12 { + padding-right: 450px; + .panel { + margin: 0; + width: 400px; + position: absolute; + top: 0px; + right: 15px; + } + } + } +} + +.modal.modal-dark { + + color: #FFF; + .modal-content { + background-color: #212424; + } + .modal-header { + color: #FFF; + background-color: #484848; + border-bottom-color: #000000; + } + .modal-body, .modal-footer { + background-color: #212424; + color: #FFF; + } + .modal-footer { + border-top: none; + } +} + +// Modal as panel +.modal-panel { + .modal-panel-body { + float: left; + width: 400px; + } + .modal-panel-sidebar-right { + width: 300px; + float: right; + border-left: 1px solid #000000; + } } + +.modal-image-manipulation { + .modal-panel-body { + padding: 20px; + img { + max-width: 100%; + } + } +} \ No newline at end of file diff --git a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_table.less b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_table.less index ef43277241b50e75c01c23420edb9f861746cf04..34a827bcfb10f9b22fd4545346d277f65108737d 100644 --- a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_table.less +++ b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_table.less @@ -192,7 +192,20 @@ table { } } } - +.table-no-borders { + border: none; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + border: none; + padding: 2px; + } + } + } +} // // Fits the table in the viewport and makes overflow possible // @@ -259,3 +272,16 @@ table { width: auto; } } +.table-fit-block { + max-width: 100%; + width: auto; + display: block; + margin: 0; + > .table { + width: auto; + } +} +.table-spacer-wrap { + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/typo3/sysext/t3skin/Resources/Private/Styles/t3skin.less b/typo3/sysext/t3skin/Resources/Private/Styles/t3skin.less index a740e7b18da7e4c1526af2a7547d1a5dead19856..42415af047e7b5bef59317db5f87e9f28c598d56 100644 --- a/typo3/sysext/t3skin/Resources/Private/Styles/t3skin.less +++ b/typo3/sysext/t3skin/Resources/Private/Styles/t3skin.less @@ -69,6 +69,10 @@ // Components from bootstrap plugins @import "@{library-directory-prefix}/eonasdan-bootstrap-datetimepicker/src/less/bootstrap-datetimepicker.less"; +// Image Manipulation Wizard base styles +@import "@{library-directory-prefix}/cropper/src/less/cropper.less"; + + /*! * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) @@ -103,6 +107,7 @@ @import "TYPO3/_element_animation.less"; @import "TYPO3/_element_clipboard.less"; +@import "TYPO3/_element_cropper.less"; @import "TYPO3/_element_csh.less"; @import "TYPO3/_element_csm.less"; @import "TYPO3/_element_docheader.less"; diff --git a/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css b/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css index c9019bbdd7f8035929db913585447664ccb86d0d..b9bdf937b8964c55f1789092f5b44649f9001ce3 100644 --- a/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css +++ b/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css @@ -5608,6 +5608,252 @@ button.close { width: 283px; } } +/*! + * Cropper v@VERSION + * https://github.com/fengyuanchen/cropper + * + * Copyright (c) 2014-@YEAR Fengyuan Chen and contributors + * Released under the MIT license + * + * Date: @DATE + */ +.cropper-container { + position: relative; + overflow: hidden; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; +} +.cropper-container img { + display: block; + image-orientation: 0deg !important; + width: 100%; + height: 100%; + min-width: 0 !important; + min-height: 0 !important; + max-width: none !important; + max-height: none !important; +} +.cropper-canvas, +.cropper-drag-box, +.cropper-crop-box, +.cropper-modal { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +.cropper-drag-box { + background-color: #fff; + opacity: 0; + filter: alpha(opacity=0); +} +.cropper-modal { + background-color: #000; + opacity: 0.5; + filter: alpha(opacity=50); +} +.cropper-view-box { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + outline: 1px solid #6699ff; + outline-color: rgba(102, 153, 255, 0.75); +} +.cropper-dashed { + position: absolute; + display: block; + border: 0 dashed #fff; + opacity: 0.5; + filter: alpha(opacity=50); +} +.cropper-dashed.dashed-h { + top: 33.33333333%; + left: 0; + width: 100%; + height: 33.33333333%; + border-top-width: 1px; + border-bottom-width: 1px; +} +.cropper-dashed.dashed-v { + top: 0; + left: 33.33333333%; + width: 33.33333333%; + height: 100%; + border-right-width: 1px; + border-left-width: 1px; +} +.cropper-face, +.cropper-line, +.cropper-point { + position: absolute; + display: block; + width: 100%; + height: 100%; + opacity: 0.1; + filter: alpha(opacity=10); +} +.cropper-face { + top: 0; + left: 0; + cursor: move; + background-color: #fff; +} +.cropper-line { + background-color: #6699ff; +} +.cropper-line.line-e { + top: 0; + right: -3px; + width: 5px; + cursor: e-resize; +} +.cropper-line.line-n { + top: -3px; + left: 0; + height: 5px; + cursor: n-resize; +} +.cropper-line.line-w { + top: 0; + left: -3px; + width: 5px; + cursor: w-resize; +} +.cropper-line.line-s { + bottom: -3px; + left: 0; + height: 5px; + cursor: s-resize; +} +.cropper-point { + width: 5px; + height: 5px; + background-color: #6699ff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.cropper-point.point-e { + top: 50%; + right: -3px; + margin-top: -3px; + cursor: e-resize; +} +.cropper-point.point-n { + top: -3px; + left: 50%; + margin-left: -3px; + cursor: n-resize; +} +.cropper-point.point-w { + top: 50%; + left: -3px; + margin-top: -3px; + cursor: w-resize; +} +.cropper-point.point-s { + bottom: -3px; + left: 50%; + margin-left: -3px; + cursor: s-resize; +} +.cropper-point.point-ne { + top: -3px; + right: -3px; + cursor: ne-resize; +} +.cropper-point.point-nw { + top: -3px; + left: -3px; + cursor: nw-resize; +} +.cropper-point.point-sw { + bottom: -3px; + left: -3px; + cursor: sw-resize; +} +.cropper-point.point-se { + right: -3px; + bottom: -3px; + width: 20px; + height: 20px; + cursor: se-resize; + opacity: 1; + filter: alpha(opacity=100); +} +.cropper-point.point-se:before { + position: absolute; + right: -50%; + bottom: -50%; + display: block; + width: 200%; + height: 200%; + content: " "; + background-color: #6699ff; + opacity: 0; + filter: alpha(opacity=0); +} +@media (min-width: 768px) { + .cropper-point.point-se { + width: 15px; + height: 15px; + } +} +@media (min-width: 992px) { + .cropper-point.point-se { + width: 10px; + height: 10px; + } +} +@media (min-width: 1200px) { + .cropper-point.point-se { + width: 5px; + height: 5px; + opacity: 0.75; + filter: alpha(opacity=75); + } +} +.cropper-bg { + background-image: url("../img/bg.png"); +} +.cropper-invisible { + opacity: 0; + filter: alpha(opacity=0); +} +.cropper-hide { + position: fixed; + top: 0; + left: 0; + z-index: -1; + width: auto!important; + height: auto!important; + min-width: 0!important; + min-height: 0!important; + max-width: none!important; + max-height: none!important; + opacity: 0; + filter: alpha(opacity=0); +} +.cropper-hidden { + display: none !important; +} +.cropper-move { + cursor: move; +} +.cropper-crop { + cursor: crosshair; +} +.cropper-disabled .cropper-canvas, +.cropper-disabled .cropper-face, +.cropper-disabled .cropper-line, +.cropper-disabled .cropper-point { + cursor: not-allowed; +} /*! * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) @@ -7794,6 +8040,22 @@ table#typo3-clipboard tr.bgColor5 td a { table#typo3-clipboard tr.bgColor5 td img { vertical-align: middle; } +.cropper-line { + background-color: #FFFFFF; +} +.cropper-point { + background-color: #FFFFFF; +} +.cropper-point.point-se:before { + background-color: #FFFFFF; +} +.cropper-view-box { + outline: 1px dashed #6699ff; + outline-color: #ffffff; +} +.cropper-bg { + background-image: url("../../Images/cropper-background.png"); +} .t3-help-link { display: inline!important; } @@ -8459,6 +8721,62 @@ table#typo3-history-item img { border: none; overflow: hidden; } +.modal-content .loadmessage { + text-align: center; + color: #1e1e1e; +} +.modal-resize.fade .modal-dialog { + -webkit-transition-property: height, width; + transition-property: height, width; + -webkit-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-timing-function: ease; + transition-timing-function: ease; +} +.modal-image-manipulation .modal-body .col-lg-12 { + padding-right: 450px; +} +.modal-image-manipulation .modal-body .col-lg-12 .panel { + margin: 0; + width: 400px; + position: absolute; + top: 0px; + right: 15px; +} +.modal.modal-dark { + color: #FFF; +} +.modal.modal-dark .modal-content { + background-color: #212424; +} +.modal.modal-dark .modal-header { + color: #FFF; + background-color: #484848; + border-bottom-color: #000000; +} +.modal.modal-dark .modal-body, +.modal.modal-dark .modal-footer { + background-color: #212424; + color: #FFF; +} +.modal.modal-dark .modal-footer { + border-top: none; +} +.modal-panel .modal-panel-body { + float: left; + width: 400px; +} +.modal-panel .modal-panel-sidebar-right { + width: 300px; + float: right; + border-left: 1px solid #000000; +} +.modal-image-manipulation .modal-panel-body { + padding: 20px; +} +.modal-image-manipulation .modal-panel-body img { + max-width: 100%; +} #typo3-pagetree { height: 100%; } @@ -9167,6 +9485,18 @@ fieldset[disabled] .table .btn-default.active { .table-vertical-bottom > tfoot > tr > td { vertical-align: bottom; } +.table-no-borders { + border: none; +} +.table-no-borders > thead > tr > th, +.table-no-borders > tbody > tr > th, +.table-no-borders > tfoot > tr > th, +.table-no-borders > thead > tr > td, +.table-no-borders > tbody > tr > td, +.table-no-borders > tfoot > tr > td { + border: none; + padding: 2px; +} .table-fit { width: 100%; margin-bottom: 1.5em; @@ -9216,6 +9546,19 @@ fieldset[disabled] .table .btn-default.active { .table-fit-inline-block > .table { width: auto; } +.table-fit-block { + max-width: 100%; + width: auto; + display: block; + margin: 0; +} +.table-fit-block > .table { + width: auto; +} +.table-spacer-wrap { + margin-top: 10px; + margin-bottom: 10px; +} .tooltip-inner { padding: 5px 10px; } diff --git a/typo3/sysext/t3skin/Resources/Public/Images/cropper-background.png b/typo3/sysext/t3skin/Resources/Public/Images/cropper-background.png new file mode 100644 index 0000000000000000000000000000000000000000..8eddf72bad35efba2cfeabdf79fafbbbe730fdfe Binary files /dev/null and b/typo3/sysext/t3skin/Resources/Public/Images/cropper-background.png differ