diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml index fc86c0d2dc68..3f2037f70b2d 100644 --- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml +++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml @@ -85,9 +85,11 @@ + validate-number validate-zero-or-greater + validate-number validate-zero-or-greater diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml index e9a194435e3e..eac4affcb9db 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml @@ -7,40 +7,43 @@ --> - + - - - - - - - - - - - - - - - - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml new file mode 100644 index 000000000000..cbb702c26f17 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml @@ -0,0 +1,31 @@ + + + + + + + + + <description value="Unable to configure Authorize.net Accept.js without required options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-17805"/> + <useCaseId value="MC-17753"/> + <group value="AuthorizenetAcceptjs"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <actionGroup ref="EnableAuthorizenetAcceptjs" stepKey="enableAuthorizenetAcceptjs"/> + <actionGroup ref="AssertAuthorizenetAcceptjsRequiredFieldsValidationIsPresentOnSave" stepKey="assertErrorMessages"/> + </test> +</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml index 6aa6792e0e0d..7f25482d627e 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml @@ -31,9 +31,11 @@ </createData> <!--Configure Auth.net--> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> <argument name="paymentAction" value="Authorize Only"/> </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> </before> <after> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml index 6f71bd180766..919c32d8f70d 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml @@ -36,9 +36,11 @@ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> <!--Configure Auth.net--> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> <argument name="paymentAction" value="Authorize and Capture"/> </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> </before> <after> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml index 279a904d916a..8623919cf5d6 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml @@ -17,7 +17,7 @@ <group id="authorizenet_acceptjs_required"/> </requires> </field> - <group id="required" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="5"> + <group id="required" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="15"> <label>Basic Authorize.Net Settings</label> <attribute type="expanded">1</attribute> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> @@ -39,25 +39,44 @@ <label>API Login ID</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <config_path>payment/authorizenet_acceptjs/login</config_path> + <validate>required-entry</validate> + <depends> + <field id="*/authorizenet_acceptjs/active">1</field> + </depends> </field> <field id="trans_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Transaction Key</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <config_path>payment/authorizenet_acceptjs/trans_key</config_path> + <validate>required-entry</validate> + <depends> + <field id="*/authorizenet_acceptjs/active">1</field> + </depends> </field> <field id="public_client_key" translate="label" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Public Client Key</label> <config_path>payment/authorizenet_acceptjs/public_client_key</config_path> + <validate>required-entry</validate> + <depends> + <field id="*/authorizenet_acceptjs/active">1</field> + </depends> </field> <field id="trans_signature_key" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Signature Key</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <config_path>payment/authorizenet_acceptjs/trans_signature_key</config_path> + <validate>required-entry</validate> + <depends> + <field id="*/authorizenet_acceptjs/active">1</field> + </depends> </field> <field id="trans_md5" translate="label" type="obscure" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Merchant MD5 (deprecated)</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <config_path>payment/authorizenet_acceptjs/trans_md5</config_path> + <depends> + <field id="*/authorizenet_acceptjs/active">1</field> + </depends> </field> </group> <group id="advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="0" sortOrder="20"> @@ -101,10 +120,12 @@ <field id="min_order_total" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Order Total</label> <config_path>payment/authorizenet_acceptjs/min_order_total</config_path> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="max_order_total" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Maximum Order Total</label> <config_path>payment/authorizenet_acceptjs/max_order_total</config_path> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Sort Order</label> diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js index 983318c4cdaa..bba1290a9eed 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js @@ -91,8 +91,10 @@ define([ return; } - authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey; - authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID; + authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey !== null ? + window.checkoutConfig.payment[this.getCode()].clientKey : ''; + authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID !== null ? + window.checkoutConfig.payment[this.getCode()].apiLoginID : ''; cardData.cardNumber = this.creditCardNumber(); cardData.month = this.creditCardExpMonth(); diff --git a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php index eb241d376e24..207d21994308 100644 --- a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php +++ b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php @@ -11,7 +11,7 @@ use Magento\Framework\Stdlib\ArrayManager; /** - * DataProvider Model for Authorizenet + * SetPaymentMethod additional data provider model for Authorizenet payment method */ class AuthorizenetDataProvider implements AdditionalDataProviderInterface { @@ -23,7 +23,6 @@ class AuthorizenetDataProvider implements AdditionalDataProviderInterface private $arrayManager; /** - * AuthorizenetDataProvider constructor. * @param ArrayManager $arrayManager */ public function __construct( @@ -42,19 +41,19 @@ public function getData(array $data): array { $additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data) ?? []; foreach ($additionalData as $key => $value) { - $additionalData[$this->snakeCaseToCamelCase($key)] = $value; + $additionalData[$this->convertSnakeCaseToCamelCase($key)] = $value; unset($additionalData[$key]); } return $additionalData; } /** - * Converts an input string from snake_case to camelCase. + * Convert an input string from snake_case to camelCase. * * @param string $input * @return string */ - private function snakeCaseToCamelCase($input) + private function convertSnakeCaseToCamelCase($input): string { return lcfirst(str_replace('_', '', ucwords($input, '_'))); } diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/ClickSaveActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/ClickSaveActionGroup.xml new file mode 100644 index 000000000000..4fa8bf1ce6ab --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/ClickSaveActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Click save button and see message--> + <actionGroup name="ClickSaveButtonActionGroup"> + <arguments> + <argument name="message" type="string"/> + </arguments> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitMessage" /> + <see userInput="{{message}}" selector="{{AdminMessagesSection.success}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/view/frontend/requirejs-config.js b/app/code/Magento/Braintree/view/frontend/requirejs-config.js index 8e6d7f8062eb..5c9bcd88de73 100644 --- a/app/code/Magento/Braintree/view/frontend/requirejs-config.js +++ b/app/code/Magento/Braintree/view/frontend/requirejs-config.js @@ -6,11 +6,11 @@ var config = { map: { '*': { - braintreeClient: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/client.min.js', - braintreeHostedFields: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/hosted-fields.min.js', - braintreePayPal: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/paypal-checkout.min.js', - braintree3DSecure: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/three-d-secure.min.js', - braintreeDataCollector: 'https://js.braintreegateway.com/web/3.46.0-beta-3ds.8/js/data-collector.min.js' + braintreeClient: 'https://js.braintreegateway.com/web/3.48.0/js/client.min.js', + braintreeHostedFields: 'https://js.braintreegateway.com/web/3.48.0/js/hosted-fields.min.js', + braintreePayPal: 'https://js.braintreegateway.com/web/3.48.0/js/paypal-checkout.min.js', + braintree3DSecure: 'https://js.braintreegateway.com/web/3.48.0/js/three-d-secure.min.js', + braintreeDataCollector: 'https://js.braintreegateway.com/web/3.48.0/js/data-collector.min.js' } }, paths: { diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php b/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php index d8ad1757ab2e..cdbe1906df84 100644 --- a/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php +++ b/app/code/Magento/Bundle/Setup/Patch/Data/ApplyAttributesUpdate.php @@ -14,8 +14,7 @@ use Magento\Eav\Setup\EavSetupFactory; /** - * Class ApplyAttributesUpdate - * @package Magento\Bundle\Setup\Patch + * Class \Magento\Bundle\Setup\Patch\ApplyAttributesUpdate */ class ApplyAttributesUpdate implements DataPatchInterface, PatchVersionInterface { @@ -44,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function apply() @@ -66,8 +65,8 @@ public function apply() ',', $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field, 'apply_to') ); - if (!in_array('bundle', $applyTo)) { - $applyTo[] = 'bundle'; + if (!in_array(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE, $applyTo)) { + $applyTo[] = \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE; $eavSetup->updateAttribute( \Magento\Catalog\Model\Product::ENTITY, $field, @@ -78,7 +77,7 @@ public function apply() } $applyTo = explode(',', $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'cost', 'apply_to')); - unset($applyTo[array_search('bundle', $applyTo)]); + unset($applyTo[array_search(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE, $applyTo)]); $eavSetup->updateAttribute(\Magento\Catalog\Model\Product::ENTITY, 'cost', 'apply_to', implode(',', $applyTo)); /** @@ -106,7 +105,7 @@ public function apply() 'visible_on_front' => false, 'used_in_product_listing' => true, 'unique' => false, - 'apply_to' => 'bundle' + 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE ] ); @@ -131,7 +130,7 @@ public function apply() 'comparable' => false, 'visible_on_front' => false, 'unique' => false, - 'apply_to' => 'bundle' + 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE ] ); @@ -157,7 +156,7 @@ public function apply() 'visible_on_front' => false, 'used_in_product_listing' => true, 'unique' => false, - 'apply_to' => 'bundle' + 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE ] ); @@ -184,7 +183,7 @@ public function apply() 'visible_on_front' => false, 'used_in_product_listing' => true, 'unique' => false, - 'apply_to' => 'bundle' + 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE ] ); @@ -210,13 +209,13 @@ public function apply() 'visible_on_front' => false, 'used_in_product_listing' => true, 'unique' => false, - 'apply_to' => 'bundle' + 'apply_to' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE ] ); } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -224,7 +223,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -232,7 +231,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php index adb178796f88..c6a67cc5a110 100644 --- a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ b/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php @@ -41,7 +41,10 @@ public function apply() 'catalog_product_index_price_bundle_opt_tmp', ]; foreach ($tables as $table) { - $this->schemaSetup->getConnection()->changeTableEngine($table, 'InnoDB'); + $tableName = $this->schemaSetup->getTable($table); + if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { + $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); + } } $this->schemaSetup->endSetup(); diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml index cac96a8aca7c..ec425ca7711a 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml @@ -26,7 +26,7 @@ $options = $block->decorateArray($block->getOptions($stripSelection)); </script> <fieldset class="fieldset fieldset-bundle-options"> <legend id="customizeTitle" class="legend title"> - <span><?= /* @noEscape */ __('Customize %1', $helper->productAttribute($product, $product->getName(), 'name')) ?></span> + <span><?= $block->escapeHtml(__('Customize %1', $helper->productAttribute($product, $product->getName(), 'name'))) ?></span> </legend><br /> <?= $block->getChildHtml('product_info_bundle_options_top') ?> <?php foreach ($options as $option) : ?> diff --git a/app/code/Magento/Bundle/view/frontend/web/js/float.js b/app/code/Magento/Bundle/view/frontend/web/js/float.js index 8e78fd069cb3..dd8b1443dcba 100644 --- a/app/code/Magento/Bundle/view/frontend/web/js/float.js +++ b/app/code/Magento/Bundle/view/frontend/web/js/float.js @@ -9,7 +9,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js index 1e7fe6b6673d..d5270569d5bb 100644 --- a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js +++ b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js @@ -9,7 +9,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui', + 'jquery-ui-modules/widget', 'Magento_Bundle/js/price-bundle' ], function ($, mageTemplate) { 'use strict'; diff --git a/app/code/Magento/Bundle/view/frontend/web/js/slide.js b/app/code/Magento/Bundle/view/frontend/web/js/slide.js index 99b01f340fb4..5afe4ad8a9ea 100644 --- a/app/code/Magento/Bundle/view/frontend/web/js/slide.js +++ b/app/code/Magento/Bundle/view/frontend/web/js/slide.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Captcha/view/frontend/web/js/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/captcha.js index 0d4d4812f64c..b5e5e6b006bf 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/captcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/captcha.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/CardinalCommerce/etc/config.xml b/app/code/Magento/CardinalCommerce/etc/config.xml index 60b111a59cbc..605ba89c1285 100644 --- a/app/code/Magento/CardinalCommerce/etc/config.xml +++ b/app/code/Magento/CardinalCommerce/etc/config.xml @@ -7,6 +7,13 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> <default> + <dev> + <js> + <minify_exclude> + <cardinal_commerce>/v1/songbird</cardinal_commerce> + </minify_exclude> + </js> + </dev> <three_d_secure> <cardinal> <environment>production</environment> diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php index 16fc76f81f34..6a035a4681a5 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Model\Category\Attribute\Backend; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Uploader; /** * Catalog category image attribute backend model @@ -71,7 +72,7 @@ public function __construct( /** * Gets image name from $value array. * - * Will return empty string in a case when $value is not an array + * Will return empty string in a case when $value is not an array. * * @param array $value Attribute value * @return string @@ -86,9 +87,28 @@ private function getUploadedImageName($value) } /** - * Avoiding saving potential upload data to DB + * Check that image name exists in catalog/category directory and return new image name if it already exists. * - * Will set empty image attribute value if image was not uploaded + * @param string $imageName + * @return string + */ + private function checkUniqueImageName(string $imageName): string + { + $imageUploader = $this->getImageUploader(); + $mediaDirectory = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $imageAbsolutePath = $mediaDirectory->getAbsolutePath( + $imageUploader->getBasePath() . DIRECTORY_SEPARATOR . $imageName + ); + + $imageName = Uploader::getNewFilename($imageAbsolutePath); + + return $imageName; + } + + /** + * Avoiding saving potential upload data to DB. + * + * Will set empty image attribute value if image was not uploaded. * * @param \Magento\Framework\DataObject $object * @return $this @@ -105,6 +125,7 @@ public function beforeSave($object) } if ($imageName = $this->getUploadedImageName($value)) { + $imageName = $this->checkUniqueImageName($imageName); $object->setData($this->additionalData . $attributeName, $value); $object->setData($attributeName, $imageName); } elseif (!is_string($value)) { @@ -160,6 +181,7 @@ private function fileResidesOutsideCategoryDir($value) if (!$baseMediaDir) { return false; } + return strpos($fileUrl, $baseMediaDir) === 0; } diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index b5ca0895d6d1..825823276261 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Model; +use Magento\Framework\File\Uploader; + /** * Catalog image uploader */ @@ -199,7 +201,14 @@ public function moveFileFromTmp($imageName) $baseTmpPath = $this->getBaseTmpPath(); $basePath = $this->getBasePath(); - $baseImagePath = $this->getFilePath($basePath, $imageName); + $baseImagePath = $this->getFilePath( + $basePath, + Uploader::getNewFileName( + $this->mediaDirectory->getAbsolutePath( + $this->getFilePath($basePath, $imageName) + ) + ) + ); $baseTmpImagePath = $this->getFilePath($baseTmpPath, $imageName); try { diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php index e6c098ab0254..c0722901e3b1 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -6,9 +6,12 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat; use Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterfaceFactory; +use Magento\Store\Model\Store; /** * Class TableBuilder + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TableBuilder { @@ -83,6 +86,7 @@ public function build($storeId, $changedIds, $valueFieldSuffix) //Create list of temporary tables based on available attributes attributes $valueTables = []; foreach ($temporaryEavAttributes as $tableName => $columns) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $valueTables = array_merge( $valueTables, $this->_createTemporaryTable($this->_getTemporaryTableName($tableName), $columns, $valueFieldSuffix) @@ -272,75 +276,71 @@ protected function _fillTemporaryTable( $valueFieldSuffix, $storeId ) { - $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); if (!empty($tableColumns)) { - $columnsChunks = array_chunk( - $tableColumns, - Action\Indexer::ATTRIBUTES_CHUNK_SIZE, - true - ); + $columnsChunks = array_chunk($tableColumns, Action\Indexer::ATTRIBUTES_CHUNK_SIZE / 2, true); + + $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); + $entityTemporaryTableName = $this->_getTemporaryTableName($entityTableName); + $temporaryTableName = $this->_getTemporaryTableName($tableName); + $temporaryValueTableName = $temporaryTableName . $valueFieldSuffix; + $attributeOptionValueTableName = $this->_productIndexerHelper->getTable('eav_attribute_option_value'); + + $flatColumns = $this->_productIndexerHelper->getFlatColumns(); + $defaultStoreId = Store::DEFAULT_STORE_ID; + $linkField = $this->getMetadataPool() + ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + foreach ($columnsChunks as $columnsList) { $select = $this->_connection->select(); $selectValue = $this->_connection->select(); - $entityTableName = $this->_getTemporaryTableName( - $this->_productIndexerHelper->getTable('catalog_product_entity') - ); - $temporaryTableName = $this->_getTemporaryTableName($tableName); - $temporaryValueTableName = $temporaryTableName . $valueFieldSuffix; - $keyColumn = array_unique([$metadata->getLinkField(), 'entity_id']); + $keyColumn = array_unique([$linkField, 'entity_id']); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $columns = array_merge($keyColumn, array_keys($columnsList)); $valueColumns = $keyColumn; - $flatColumns = $this->_productIndexerHelper->getFlatColumns(); $iterationNum = 1; - $select->from(['et' => $entityTableName], $keyColumn) - ->join( - ['e' => $this->resource->getTableName('catalog_product_entity')], - 'e.entity_id = et.entity_id', - [] - ); + $select->from(['et' => $entityTemporaryTableName], $keyColumn) + ->join(['e' => $entityTableName], 'e.entity_id = et.entity_id', []); $selectValue->from(['e' => $temporaryTableName], $keyColumn); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ foreach ($columnsList as $columnName => $attribute) { $countTableName = 't' . ($iterationNum++); - $joinCondition = sprintf( - 'e.%3$s = %1$s.%3$s' . - ' AND %1$s.attribute_id = %2$d' . - ' AND (%1$s.store_id = %4$d' . - ' OR %1$s.store_id = 0)', - $countTableName, - $attribute->getId(), - $metadata->getLinkField(), - $storeId - ); - + $joinCondition = 'e.%3$s = %1$s.%3$s AND %1$s.attribute_id = %2$d AND %1$s.store_id = %4$d'; $select->joinLeft( [$countTableName => $tableName], - $joinCondition, - [$columnName => 'value'] + sprintf($joinCondition, $countTableName, $attribute->getId(), $linkField, $defaultStoreId), + [] + )->joinLeft( + ['s' . $countTableName => $tableName], + sprintf($joinCondition, 's' . $countTableName, $attribute->getId(), $linkField, $storeId), + [] ); + $columnValue = $this->_connection->getIfNullSql( + 's' . $countTableName . '.value', + $countTableName . '.value' + ); + $select->columns([$columnName => $columnValue]); + if ($attribute->getFlatUpdateSelect($storeId) instanceof \Magento\Framework\DB\Select) { $attributeCode = $attribute->getAttributeCode(); $columnValueName = $attributeCode . $valueFieldSuffix; if (isset($flatColumns[$columnValueName])) { - $valueJoinCondition = sprintf( - 'e.%1$s = %2$s.option_id AND (%2$s.store_id = %3$d OR %2$s.store_id = 0)', - $attributeCode, - $countTableName, - $storeId - ); + $valueJoinCondition = 'e.%1$s = %2$s.option_id AND %2$s.store_id = %3$d'; $selectValue->joinLeft( - [ - $countTableName => $this->_productIndexerHelper->getTable( - 'eav_attribute_option_value' - ), - ], - $valueJoinCondition, - [$columnValueName => $countTableName . '.value'] + [$countTableName => $attributeOptionValueTableName], + sprintf($valueJoinCondition, $attributeCode, $countTableName, $defaultStoreId), + [] + )->joinLeft( + ['s' . $countTableName => $attributeOptionValueTableName], + sprintf($valueJoinCondition, $attributeCode, 's' . $countTableName, $storeId), + [] ); + + $selectValue->columns([$columnValueName => $columnValue]); $valueColumns[] = $columnValueName; } } diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index b53a02aa4201..dc73baef3f76 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -93,11 +93,6 @@ class Price */ private $tierPriceExtensionFactory; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * Constructor * @@ -111,7 +106,6 @@ class Price * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory * @param \Magento\Framework\App\Config\ScopeConfigInterface $config * @param ProductTierPriceExtensionFactory|null $tierPriceExtensionFactory - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -124,8 +118,7 @@ public function __construct( GroupManagementInterface $groupManagement, \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory, \Magento\Framework\App\Config\ScopeConfigInterface $config, - ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null ) { $this->_ruleFactory = $ruleFactory; $this->_storeManager = $storeManager; @@ -138,8 +131,6 @@ public function __construct( $this->config = $config; $this->tierPriceExtensionFactory = $tierPriceExtensionFactory ?: ObjectManager::getInstance() ->get(ProductTierPriceExtensionFactory::class); - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); } /** @@ -605,10 +596,7 @@ public function calculatePrice( ) { \Magento\Framework\Profiler::start('__PRODUCT_CALCULATE_PRICE__'); if ($wId instanceof Store) { - $sId = $wId->getId(); $wId = $wId->getWebsiteId(); - } else { - $sId = $this->_storeManager->getWebsite($wId)->getDefaultGroup()->getDefaultStoreId(); } $finalPrice = $basePrice; @@ -622,7 +610,7 @@ public function calculatePrice( ); if ($rulePrice === false) { - $date = $this->ruleDateFormatter->getDate($sId); + $date = $this->_localeDate->date(null, null, false); $rulePrice = $this->_ruleFactory->create()->getRulePrice($date, $wId, $gId, $productId); } diff --git a/app/code/Magento/Catalog/Pricing/Price/Collection.php b/app/code/Magento/Catalog/Pricing/Price/Collection.php new file mode 100644 index 000000000000..b48d7e9e3836 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/Collection.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Catalog\Pricing\Price; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Pricing\Price\Factory; +use Magento\Framework\Pricing\Price\Pool; + +/** + * Price models collection class. + */ +class Collection extends \Magento\Framework\Pricing\Price\Collection +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param SaleableInterface $saleableItem + * @param Factory $priceFactory + * @param Pool $pool + * @param float $quantity + * @param StoreManagerInterface|null $storeManager + */ + public function __construct( + SaleableInterface $saleableItem, + Factory $priceFactory, + Pool $pool, + $quantity, + StoreManagerInterface $storeManager = null + ) { + parent::__construct($saleableItem, $priceFactory, $pool, $quantity); + $this->storeManager = $storeManager ?? ObjectManager::getInstance()->get(StoreManagerInterface::class); + } + + /** + * @inheritdoc + */ + public function get($code) + { + $customerGroupId = $this->saleableItem->getCustomerGroupId() ?? ''; + $websiteId = $this->storeManager->getStore($this->saleableItem->getStoreId())->getWebsiteId(); + $codeKey = $code . '-' . $customerGroupId . '-' . $websiteId; + + if (!isset($this->priceModels[$codeKey])) { + $this->priceModels[$codeKey] = $this->priceFactory->create( + $this->saleableItem, + $this->pool[$code], + $this->quantity + ); + } + + return $this->priceModels[$codeKey]; + } +} diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index e0a92ea0e0be..71b5f505f97a 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -60,7 +60,7 @@ public function __construct( } /** - * @return string + * @inheritdoc */ protected function _toHtml() { @@ -182,25 +182,27 @@ public function showMinimalPrice() */ public function getCacheKey() { - return parent::getCacheKey() . ($this->getData('list_category_page') ? '-list-category-page': ''); + return parent::getCacheKey() + . ($this->getData('list_category_page') ? '-list-category-page': '') + . ($this->getSaleableItem()->getCustomerGroupId() ?? ''); } /** - * {@inheritdoc} - * - * @return array + * @inheritdoc */ public function getCacheKeyInfo() { $cacheKeys = parent::getCacheKeyInfo(); $cacheKeys['display_minimal_price'] = $this->getDisplayMinimalPrice(); $cacheKeys['is_product_list'] = $this->isProductList(); + $cacheKeys['customer_group_id'] = $this->getSaleableItem()->getCustomerGroupId(); return $cacheKeys; } /** - * Get flag that price rendering should be done for the list of products - * By default (if flag is not set) is false + * Get flag that price rendering should be done for the list of products. + * + * By default (if flag is not set) is false. * * @return bool */ diff --git a/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php index 39e2e0cb0963..c39247f9b30d 100644 --- a/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ b/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php @@ -47,7 +47,10 @@ public function apply() 'catalog_category_product_index_tmp', ]; foreach ($tables as $table) { - $this->schemaSetup->getConnection()->changeTableEngine($table, 'InnoDB'); + $tableName = $this->schemaSetup->getTable($table); + if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { + $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); + } } $this->schemaSetup->endSetup(); diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductsGridIsEmptyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductsGridIsEmptyActionGroup.xml new file mode 100644 index 000000000000..1ed186f34146 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductsGridIsEmptyActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertProductsGridIsEmptyActionGroup"> + <see selector="{{AdminCategoryProductsGridSection.productsGridEmpty}}" userInput="We couldn't find any records" stepKey="assertGridEmpty"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductsInGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductsInGridActionGroup.xml new file mode 100644 index 000000000000..440739875c0c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductsInGridActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCheckProductIsMissingInCategoryProductsGrid"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <dontSee selector="{{AdminCategoryProductsGridSection.nameColumn}}" userInput="{{productName}}" stepKey="dontSeeProduct"/> + </actionGroup> + <actionGroup name="AdminCheckProductPositionInCategoryProductsGrid"> + <arguments> + <argument name="position" type="string"/> + <argument name="productName" type="string"/> + </arguments> + <see selector="{{AdminCategoryProductsGridSection.rowProductName(position)}}" userInput="{{productName}}" stepKey="assertProductPosition"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml index cd850f8a7004..ec73001976dc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml @@ -15,4 +15,16 @@ <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attributeName}}" stepKey="fillDefaultLabel"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{attributeType}}" stepKey="selectInputType"/> </actionGroup> + + <!--You are on AdminProductEditPage--> + <!-- Switch scope for product attribute--> + <!-- !Note! Scope : 0 - Store View; 1 - Global; 2 - Website; --> + <actionGroup name="AdminSwitchScopeForProductAttributeActionGroup"> + <arguments> + <argument name="scope" type="string" defaultValue="1"/> + </arguments> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <waitForElementVisible selector="{{AttributePropertiesSection.Scope}}" stepKey="waitOpenAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectNecessaryScope"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 0fad3e14f6c8..f419e8dcdb19 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -576,4 +576,13 @@ <conditionalClick selector="{{AdminProductContentSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandContentSection"/> <seeInField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="{{productDescription}}" stepKey="seeProductDescription"/> </actionGroup> + + <!-- You are on StorefrontProductPage --> + <!-- Check active product image --> + <actionGroup name="StorefrontAssertActiveProductImage"> + <arguments> + <argument name="fileName" defaultValue="magento-logo" type="string"/> + </arguments> + <seeElement selector="{{StorefrontProductMediaSection.productImageActive(fileName)}}" stepKey="seeActiveImageDefault"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml index 1bb7c179dfca..95f894b1293a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml @@ -19,4 +19,12 @@ <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> <waitForPageLoad stepKey="waitForGalleryDisappear" /> </actionGroup> + + <!--Check availability image in fotorama--> + <actionGroup name="StorefrontAssertFotoramaImageAvailablity"> + <arguments> + <argument name="fileName" type="string" defaultValue="magento-logo"/> + </arguments> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(fileName)}}" stepKey="seeActiveImageDefault"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml new file mode 100644 index 000000000000..01751a32d2e0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert product is missing in category products page --> + <actionGroup name="StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(productName)}}" stepKey="dontSeeCorrectProductsOnStorefront"/> + </actionGroup> + <actionGroup name="StorefrontCheckProductPositionActionGroup"> + <arguments> + <argument name="position" type="string"/> + <argument name="productName" type="string"/> + </arguments> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber(position)}}" userInput="{{productName}}" stepKey="assertProductPosition"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryPageActionGroup.xml new file mode 100644 index 000000000000..39cb9ef1a63d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Navigates storefront category page --> + <actionGroup name="StorefrontNavigateCategoryPageActionGroup"> + <arguments> + <argument name="category"/> + </arguments> + <!-- Open category page on storefront --> + <amOnPage url="{{StorefrontCategoryPage.url(category.custom_attributes[url_key])}}" stepKey="navigateStorefrontCategoryPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 02e5ae5ae36a..653ec843cab3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -343,4 +343,8 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="VisualSwatchProductAttribute" type="ProductAttribute"> + <data key="frontend_input">swatch_visual</data> + <data key="attribute_code" unique="suffix">visual_swatch</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml index 977e63b9ec92..92961cc48212 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml @@ -49,7 +49,7 @@ </section> <section name="CatalogWYSIWYGSection"> <element name="ShowHideBtn" type="button" selector="#togglecategory_form_description"/> - <element name="TinyMCE4" type="text" selector=".mce-branding-powered-by"/> + <element name="TinyMCE4" type="text" selector=".mce-branding"/> <element name="Style" type="button" selector=".mce-txt" /> <element name="Bold" type="button" selector=".mce-i-bold" /> <element name="Italic" type="button" selector=".mce-i-italic" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml index df79ec61ef73..6618b0e1a48d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml @@ -17,5 +17,6 @@ <element name="productGridNameProduct" type="text" selector="//table[@id='catalog_category_products_table']//td[contains(., '{{productName}}')]" parameterized="true"/> <element name="productVisibility" type="select" selector="//*[@name='product[visibility]']"/> <element name="productSelectAll" type="checkbox" selector="input.admin__control-checkbox"/> + <element name="productsGridEmpty" type="text" selector="#catalog_category_products_table .data-grid-tr-no-data .empty-text"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml index 9b75f7e6908a..4c60ebe78b88 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -20,7 +20,7 @@ <element name="Save" type="button" selector="#save" timeout="30"/> <element name="DeleteAttribute" type="button" selector="#delete" timeout="30"/> <element name="SaveAndEdit" type="button" selector="#save_and_edit_button" timeout="30"/> - <element name="TinyMCE4" type="button" selector="//span[text()='Default Value']/parent::label/following-sibling::div//div[@class='mce-branding-powered-by']"/> + <element name="TinyMCE4" type="button" selector="//span[text()='Default Value']/parent::label/following-sibling::div//*[contains(@class,'mce-branding')]"/> <element name="checkIfTabOpen" selector="//div[@id='advanced_fieldset-wrapper' and not(contains(@class,'opened'))]" type="button"/> <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 0dd9e6d948c1..80b415916745 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -121,10 +121,10 @@ <element name="InsertImageIcon" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-image']" parameterized="true"/> <element name="InsertTable" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-table']" parameterized="true"/> <element name="SpecialCharacter" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-charmap']" parameterized="true"/> - <element name="TinyMCE4" type="text" selector="//div[contains(@id, '{{var1}}')]//div[@class='mce-branding-powered-by']" parameterized="true"/> + <element name="TinyMCE4" type="text" selector="//div[contains(@id, '{{var1}}')]//*[contains(@class,'mce-branding')]" parameterized="true"/> </section> <section name="ProductDescriptionWYSIWYGToolbarSection"> - <element name="TinyMCE4" type ="button" selector="//div[@id='editorproduct_form_description']//div[@class='mce-branding-powered-by']" /> + <element name="TinyMCE4" type ="button" selector="//div[@id='editorproduct_form_description']//*[contains(@class,'mce-branding')]" /> <element name="showHideBtn" type="button" selector="#toggleproduct_form_description"/> <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_description > .scalable.action-add-image.plugin" /> <element name="Style" type="button" selector="//div[@id='editorproduct_form_description']//span[text()='Paragraph']" /> @@ -162,7 +162,7 @@ <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> </section> <section name="ProductShortDescriptionWYSIWYGToolbarSection"> - <element name="TinyMCE4" type ="button" selector="//div[@id='editorproduct_form_short_description']//div[@class='mce-branding-powered-by']" /> + <element name="TinyMCE4" type ="button" selector="//div[@id='editorproduct_form_short_description']//*[contains(@class,'mce-branding')]" /> <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_short_description > .scalable.action-add-image.plugin" /> <element name="showHideBtn" type="button" selector="#toggleproduct_form_short_description"/> <element name="Style" type="button" selector="//div[@id='editorproduct_form_short_description']//span[text()='Paragraph']" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml index ea10e12fb73f..6ed359e35ab5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml @@ -16,5 +16,6 @@ <element name="closeFullscreenImage" type="button" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//*[@data-gallery-role='fotorama__fullscreen-icon']" /> <element name="imageFile" type="text" selector="//*[@class='product media']//img[contains(@src, '{{filename}}')]" parameterized="true"/> <element name="productImageActive" type="text" selector=".product.media div[data-active=true] > img[src*='{{filename}}']" parameterized="true"/> + <element name="productImageInFotorama" type="file" selector=".fotorama__nav__shaft img[src*='{{imageName}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml index 26ad7a46a73d..0b929eaddc96 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml @@ -19,11 +19,15 @@ <group value="mtf_migrated"/> </annotations> <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteVirtualProduct"> + <argument name="sku" value="{{virtualProductOutOfStock.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilter"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 17769c79677f..58737dd50974 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -19,11 +19,15 @@ <group value="mtf_migrated"/> </annotations> <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteVirtualProduct"> + <argument name="sku" value="{{virtualProductCustomImportOptions.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetOrderFilter"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml index a33c7bb12879..bcc8636c65b1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml @@ -34,6 +34,7 @@ </before> <after> <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> <actionGroup ref="logout" stepKey="logout"/> </after> <!-- Assert attribute presence in storefront product additional information --> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php index f1672d842de4..dc74cdfc642e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -6,7 +6,12 @@ namespace Magento\Catalog\Test\Unit\Model\Category\Attribute\Backend; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\WriteInterface; +/** + * Test for Magento\Catalog\Model\Category\Attribute\Backend\Image class. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ImageTest extends \PHPUnit\Framework\TestCase { /** @@ -67,7 +72,7 @@ protected function setUp() $this->imageUploader = $this->createPartialMock( \Magento\Catalog\Model\ImageUploader::class, - ['moveFileFromTmp'] + ['moveFileFromTmp', 'getBasePath'] ); $this->filesystem = $this->getMockBuilder(\Magento\Framework\Filesystem::class)->disableOriginalConstructor() @@ -95,9 +100,7 @@ public function testBeforeSaveValueDeletion($value) $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => $value - ]); + $object = new \Magento\Framework\DataObject(['test_attribute' => $value]); $model->beforeSave($object); @@ -132,9 +135,7 @@ public function testBeforeSaveValueInvalid($value) $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => $value - ]); + $object = new \Magento\Framework\DataObject(['test_attribute' => $value]); $model->beforeSave($object); @@ -146,14 +147,25 @@ public function testBeforeSaveValueInvalid($value) */ public function testBeforeSaveAttributeFileName() { - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); - $model->setAttribute($this->attribute); + $model = $this->setUpModelForAfterSave(); + $mediaDirectoryMock = $this->createMock(WriteInterface::class); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + $this->imageUploader->expects($this->once())->method('getBasePath')->willReturn('base/path'); + $mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with('base/path/test123.jpg') + ->willReturn('absolute/path/base/path/test123.jpg'); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => [ - ['name' => 'test123.jpg'] + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => [ + ['name' => 'test123.jpg'], + ], ] - ]); + ); $model->beforeSave($object); @@ -165,30 +177,37 @@ public function testBeforeSaveAttributeFileName() */ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() { - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class, [ - 'filesystem' => $this->filesystem - ]); - + $model = $this->setUpModelForAfterSave(); $model->setAttribute($this->attribute); + $mediaDirectoryMock = $this->createMock(WriteInterface::class); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); $this->filesystem ->expects($this->once()) ->method('getUri') ->with(DirectoryList::MEDIA) ->willReturn('pub/media'); + $mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->willReturn('/pub/media/wysiwyg/test123.jpg'); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => [ - [ - 'name' => '/test123.jpg', - 'url' => '/pub/media/wysiwyg/test123.jpg', - ] + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => [ + [ + 'name' => 'test123.jpg', + 'url' => '/pub/media/wysiwyg/test123.jpg', + ], + ], ] - ]); + ); $model->beforeSave($object); - $this->assertEquals('/pub/media/wysiwyg/test123.jpg', $object->getTestAttribute()); + $this->assertEquals('test123.jpg', $object->getTestAttribute()); $this->assertEquals( [['name' => '/pub/media/wysiwyg/test123.jpg', 'url' => '/pub/media/wysiwyg/test123.jpg']], $object->getData('_additional_data_test_attribute') @@ -200,20 +219,31 @@ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() */ public function testBeforeSaveTemporaryAttribute() { - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model = $this->setUpModelForAfterSave(); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => [ - ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'] + $mediaDirectoryMock = $this->createMock(WriteInterface::class); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => [ + ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'], + ], ] - ]); + ); $model->beforeSave($object); - $this->assertEquals([ - ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'] - ], $object->getData('_additional_data_test_attribute')); + $this->assertEquals( + [ + ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'], + ], + $object->getData('_additional_data_test_attribute') + ); } /** @@ -224,9 +254,7 @@ public function testBeforeSaveAttributeStringValue() $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => 'test123.jpg' - ]); + $object = new \Magento\Framework\DataObject(['test_attribute' => 'test123.jpg']); $model->beforeSave($object); @@ -245,18 +273,26 @@ private function setUpModelForAfterSave() $objectManagerMock->expects($this->any()) ->method('get') - ->will($this->returnCallback(function ($class, $params = []) use ($imageUploaderMock) { - if ($class == \Magento\Catalog\CategoryImageUpload::class) { - return $imageUploaderMock; - } - - return $this->objectManager->get($class, $params); - })); - - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class, [ - 'objectManager' => $objectManagerMock, - 'logger' => $this->logger - ]); + ->will( + $this->returnCallback( + function ($class, $params = []) use ($imageUploaderMock) { + if ($class == \Magento\Catalog\CategoryImageUpload::class) { + return $imageUploaderMock; + } + + return $this->objectManager->get($class, $params); + } + ) + ); + + $model = $this->objectManager->getObject( + \Magento\Catalog\Model\Category\Attribute\Backend\Image::class, + [ + 'objectManager' => $objectManagerMock, + 'logger' => $this->logger, + 'filesystem' => $this->filesystem, + ] + ); $this->objectManager->setBackwardCompatibleProperty($model, 'imageUploader', $this->imageUploader); return $model->setAttribute($this->attribute); diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index e30577a39766..1c0b2daf4d6f 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -377,11 +377,11 @@ </argument> </arguments> </virtualType> - <virtualType name="Magento\Catalog\Pricing\Price\Collection" type="Magento\Framework\Pricing\Price\Collection"> + <type name="Magento\Catalog\Pricing\Price\Collection"> <arguments> <argument name="pool" xsi:type="object">Magento\Catalog\Pricing\Price\Pool</argument> </arguments> - </virtualType> + </type> <type name="Magento\Framework\Pricing\PriceInfo\Factory"> <arguments> <argument name="types" xsi:type="array"> diff --git a/app/code/Magento/Catalog/view/base/web/js/price-box.js b/app/code/Magento/Catalog/view/base/web/js/price-box.js index 783d39cddbc7..02ae6eadef67 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-box.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-box.js @@ -11,7 +11,7 @@ define([ 'Magento_Catalog/js/price-utils', 'underscore', 'mage/template', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, utils, _, mageTemplate) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/base/web/js/price-option-date.js b/app/code/Magento/Catalog/view/base/web/js/price-option-date.js index 4ebfed097e27..6c6299ca9a1f 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-option-date.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-option-date.js @@ -7,7 +7,7 @@ define([ 'jquery', 'priceUtils', 'priceOptions', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, utils) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/base/web/js/price-option-file.js b/app/code/Magento/Catalog/view/base/web/js/price-option-file.js index fffc2910b3fa..0be38707869e 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-option-file.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-option-file.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/base/web/js/price-options.js b/app/code/Magento/Catalog/view/base/web/js/price-options.js index e18abe3af38a..d916d466fe97 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-options.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-options.js @@ -9,7 +9,7 @@ define([ 'mage/template', 'priceUtils', 'priceBox', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, _, mageTemplate, utils) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml index e9551793c86f..e0443d5a55d9 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml @@ -34,7 +34,7 @@ id="product-gallery-image" class="image" data-mage-init='{"catalogGallery":{}}'/> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= /* @noEscape */ __('Close Window') ?></span></a></div> + <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> <div class="nav"> <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index bcb7c668657d..d74105fe531e 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -8,7 +8,7 @@ define([ 'mage/translate', 'underscore', 'Magento_Catalog/js/product/view/product-ids-resolver', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, $t, _, idsResolver) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js index df503cb42287..f6be6fd58ca2 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js @@ -9,7 +9,7 @@ if (typeof define === 'function' && define.amd) { define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], factory); } else { factory(jQuery); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/list.js b/app/code/Magento/Catalog/view/frontend/web/js/list.js index 8017aef2413a..6b1fb7f86a97 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/list.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/list.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js index b8b6ff65be2b..6589f7eb0ba4 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js b/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js index 3e29e1ebd4d9..da0364386444 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js @@ -6,7 +6,7 @@ define([ 'jquery', 'mage/translate', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, $t) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js index 66df48c28bfa..d17c25d421e0 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/js/upsell-products.js b/app/code/Magento/Catalog/view/frontend/web/js/upsell-products.js index 28e5daabdc3b..da2526d5679c 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/upsell-products.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/upsell-products.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/js/zoom.js b/app/code/Magento/Catalog/view/frontend/web/js/zoom.js index f6444223e2c5..1c68818ed9c4 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/zoom.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/zoom.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js index ad652b8ef36f..ab1753e7b9ed 100644 --- a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js +++ b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js @@ -9,7 +9,7 @@ if (typeof define === 'function' && define.amd) { define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/validation/validation' ], factory); } else { diff --git a/app/code/Magento/CatalogGraphQl/Model/MediaGalleryTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/MediaGalleryTypeResolver.php new file mode 100644 index 000000000000..7cdeb33e39db --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/MediaGalleryTypeResolver.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model; + +use \Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolver for Media Gallery type. + */ +class MediaGalleryTypeResolver implements TypeResolverInterface +{ + /** + * @inheritdoc + * + * @param array $data + * @return string + */ + public function resolveType(array $data) : string + { + // resolve type based on the data + if (isset($data['media_type']) && $data['media_type'] == 'image') { + return 'ProductImage'; + } + if (isset($data['media_type']) && $data['media_type'] == 'external-video') { + return 'ProductVideo'; + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php new file mode 100644 index 000000000000..810de0f1f4b5 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * @inheritdoc + * + * Format a product's media gallery information to conform to GraphQL schema representation + */ +class MediaGallery implements ResolverInterface +{ + /** + * @inheritdoc + * + * Format product's media gallery entry data to conform to GraphQL schema + * + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return array + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var ProductInterface $product */ + $product = $value['model']; + + $mediaGalleryEntries = []; + foreach ($product->getMediaGalleryEntries() ?? [] as $key => $entry) { + $mediaGalleryEntries[$key] = $entry->getData(); + $mediaGalleryEntries[$key]['model'] = $product; + if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { + $mediaGalleryEntries[$key]['video_content'] + = $entry->getExtensionAttributes()->getVideoContent()->getData(); + } + } + return $mediaGalleryEntries; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php similarity index 83% rename from app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php rename to app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php index 978bbfb01ff1..4ec76fe59ca8 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\CatalogGraphQl\Model\Resolver\Product\ProductImage; +namespace Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; @@ -13,9 +13,10 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Store\Api\Data\StoreInterface; /** - * Returns product's image label + * Return media label */ class Label implements ResolverInterface { @@ -43,8 +44,9 @@ public function resolve( array $value = null, array $args = null ) { - if (!isset($value['image_type'])) { - throw new LocalizedException(__('"image_type" value should be specified')); + + if (isset($value['label'])) { + return $value['label']; } if (!isset($value['model'])) { @@ -53,18 +55,16 @@ public function resolve( /** @var Product $product */ $product = $value['model']; - $imageType = $value['image_type']; - $imagePath = $product->getData($imageType); $productId = (int)$product->getEntityId(); - $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); - - // null if image is not set - if (null === $imagePath) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + if (!isset($value['image_type'])) { return $this->getAttributeValue($productId, 'name', $storeId); } - + $imageType = $value['image_type']; $imageLabel = $this->getAttributeValue($productId, $imageType . '_label', $storeId); - if (null === $imageLabel) { + if ($imageLabel == null) { $imageLabel = $this->getAttributeValue($productId, 'name', $storeId); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php similarity index 77% rename from app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php rename to app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php index 23a8c2d15c09..eaab159cddae 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\CatalogGraphQl\Model\Resolver\Product\ProductImage; +namespace Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\ImageFactory; @@ -16,7 +16,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** - * Returns product's image url + * Returns media url */ class Url implements ResolverInterface { @@ -51,7 +51,7 @@ public function resolve( array $value = null, array $args = null ) { - if (!isset($value['image_type'])) { + if (!isset($value['image_type']) && !isset($value['file'])) { throw new LocalizedException(__('"image_type" value should be specified')); } @@ -61,9 +61,17 @@ public function resolve( /** @var Product $product */ $product = $value['model']; - $imagePath = $product->getData($value['image_type']); - - return $this->getImageUrl($value['image_type'], $imagePath); + if (isset($value['image_type'])) { + $imagePath = $product->getData($value['image_type']); + return $this->getImageUrl($value['image_type'], $imagePath); + } + if (isset($value['file'])) { + $image = $this->productImageFactory->create(); + $image->setDestinationSubdir('image')->setBaseFile($value['file']); + $imageUrl = $image->getUrl(); + return $imageUrl; + } + return []; } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php index c8f167da583d..e1338930afe5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php @@ -18,6 +18,7 @@ * @inheritdoc * * Format a product's media gallery information to conform to GraphQL schema representation + * @deprecated */ class MediaGalleryEntries implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index 7f1fd7194225..e5e0d1aea428 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -89,6 +89,9 @@ public function getList( if (in_array('media_gallery_entries', $attributes)) { $collection->addMediaGalleryData(); } + if (in_array('media_gallery', $attributes)) { + $collection->addMediaGalleryData(); + } if (in_array('options', $attributes)) { $collection->addOptionsToResult(); } diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index bbc01ac0854c..ea56faf94408 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -92,13 +92,14 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.") websites: [Website] @doc(description: "An array of websites in which the product is available.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductLinks") - media_gallery_entries: [MediaGalleryEntry] @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") + media_gallery_entries: [MediaGalleryEntry] @deprecated(reason: "Use product's `media_gallery` instead") @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\TierPrices") price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") gift_message_available: String @doc(description: "Indicates whether a gift message is available.") manufacturer: Int @doc(description: "A number representing the product's manufacturer.") categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") canonical_url: String @doc(description: "Canonical URL.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") + media_gallery: [MediaGalleryInterface] @doc(description: "An array of Media Gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery") } interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "PhysicalProductInterface contains attributes specific to tangible products.") { @@ -184,9 +185,12 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the image_size_y: Int @doc(description: "The maximum height of an image.") } -type ProductImage @doc(description: "Product image information. Contains image relative path, URL and label.") { - url: String @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage\\Url") - label: String @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage\\Label") +interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { + url: String @doc(description: "The URL of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Url") + label: String @doc(description: "The label of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Label") +} + +type ProductImage implements MediaGalleryInterface @doc(description: "Product image information. Contains the image URL and label.") { } interface CustomizableOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CustomizableOptionTypeResolver") @doc(description: "The CustomizableOptionInterface contains basic information about a customizable option. It can be implemented by several types of configurable options.") { @@ -413,3 +417,7 @@ type StoreConfig @doc(description: "The type contains information about a store list_per_page : Int @doc(description: "Products per Page on List Default Value.") catalog_default_sort_by : String @doc(description: "Default Sort By.") } + +type ProductVideo @doc(description: "Contains information about a product video.") implements MediaGalleryInterface { + video_content: ProductMediaGalleryEntriesVideoContent @doc(description: "Contains a ProductMediaGalleryEntriesVideoContent object.") +} diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml index 281c8c0db307..74345e64a7c9 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-14008"/> <group value="catalog_import_export"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-15934"/> - </skip> </annotations> <before> <!--Create bundle product with dynamic price with two simple products --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml index c47b7dc83af2..b0ac6a4bc95a 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-14009"/> <group value="catalog_import_export"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-15934"/> - </skip> </annotations> <before> <!-- Create first simple product and add special price --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml index 160abe617995..1870cb21bd55 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-14005"/> <group value="catalog_import_export"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-15934"/> - </skip> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml index ec46b09808e1..f6690199d63f 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-14004"/> <group value="catalog_import_export"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml index f958978a9efa..238a3286dc40 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-14007"/> <group value="catalog_import_export"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-15934"/> - </skip> </annotations> <before> <!-- Create simple product with custom attribute set --> diff --git a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php index 27a70d0c908f..7f43cd279d4e 100644 --- a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ b/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php @@ -35,7 +35,10 @@ public function apply() { $this->schemaSetup->startSetup(); - $this->schemaSetup->getConnection()->changeTableEngine('cataloginventory_stock_status_tmp', 'InnoDB'); + $tableName = $this->schemaSetup->getTable('cataloginventory_stock_status_tmp'); + if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { + $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); + } $this->schemaSetup->endSetup(); } diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php index 79585cd8e857..0dee9eda5b6e 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php @@ -10,7 +10,6 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\CatalogRule\Pricing\Price\CatalogRulePrice; -use Magento\Framework\App\ObjectManager; /** * Add catalog rule prices to collection @@ -44,34 +43,25 @@ class CollectionProcessor */ private $localeDate; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Customer\Model\Session $customerSession, \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->customerSession = $customerSession; $this->dateTime = $dateTime; $this->localeDate = $localeDate; - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); } /** @@ -100,7 +90,7 @@ public function addPriceData(ProductCollection $productCollection, $joinColumn = ), $connection->quoteInto( 'catalog_rule.rule_date = ?', - $this->dateTime->formatDate($this->ruleDateFormatter->getDate($store->getId()), false) + $this->dateTime->formatDate($this->localeDate->date(null, null, false), false) ), ] ), diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php index 11c33633bdfa..02d2631058a1 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php @@ -55,11 +55,6 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec */ private $baseSelectProcessor; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection @@ -68,7 +63,6 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param BaseSelectProcessorInterface $baseSelectProcessor - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, @@ -77,8 +71,7 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\EntityManager\MetadataPool $metadataPool, - BaseSelectProcessorInterface $baseSelectProcessor = null, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; @@ -88,8 +81,6 @@ public function __construct( $this->metadataPool = $metadataPool; $this->baseSelectProcessor = (null !== $baseSelectProcessor) ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); } /** @@ -97,8 +88,7 @@ public function __construct( */ public function build($productId) { - $timestamp = $this->ruleDateFormatter->getTimeStamp($this->storeManager->getStore()); - $currentDate = $this->dateTime->formatDate($timestamp, false); + $currentDate = $this->dateTime->formatDate($this->localeDate->date(null, null, false), false); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); diff --git a/app/code/Magento/CatalogRule/Model/RuleDateFormatter.php b/app/code/Magento/CatalogRule/Model/RuleDateFormatter.php deleted file mode 100644 index 8d55c5bfa6b3..000000000000 --- a/app/code/Magento/CatalogRule/Model/RuleDateFormatter.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogRule\Model; - -/** - * Local date for catalog rule - */ -class RuleDateFormatter implements \Magento\CatalogRule\Model\RuleDateFormatterInterface -{ - /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface - */ - private $localeDate; - - /** - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - */ - public function __construct(\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate) - { - $this->localeDate = $localeDate; - } - - /** - * @inheritdoc - */ - public function getDate($scope = null): \DateTime - { - return $this->localeDate->scopeDate($scope, null, true); - } - - /** - * @inheritdoc - */ - public function getTimeStamp($scope = null): int - { - return $this->localeDate->scopeTimeStamp($scope); - } -} diff --git a/app/code/Magento/CatalogRule/Model/RuleDateFormatterInterface.php b/app/code/Magento/CatalogRule/Model/RuleDateFormatterInterface.php deleted file mode 100644 index a836f049eb5e..000000000000 --- a/app/code/Magento/CatalogRule/Model/RuleDateFormatterInterface.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogRule\Model; - -/** - * Local date for catalog rule - */ -interface RuleDateFormatterInterface -{ - /** - * Create \DateTime object with date converted to scope timezone for catalog rule - * - * @param mixed $scope Information about scope - * @return \DateTime - */ - public function getDate($scope = null): \DateTime; - - /** - * Get scope timestamp for catalog rule - * - * @param mixed $scope Information about scope - * @return int - */ - public function getTimeStamp($scope = null): int; -} diff --git a/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php b/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php index 2fcdfa9d71d6..a635c5611eff 100644 --- a/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php +++ b/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php @@ -12,14 +12,12 @@ namespace Magento\CatalogRule\Observer; use Magento\Catalog\Model\Product; -use Magento\CatalogRule\Model\Rule; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Customer\Model\Session as CustomerModelSession; -use Magento\Framework\Event\Observer as EventObserver; use Magento\Customer\Api\GroupManagementInterface; use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\App\ObjectManager; /** * Observer for applying catalog rules on product collection @@ -59,11 +57,6 @@ class PrepareCatalogProductCollectionPricesObserver implements ObserverInterface */ protected $groupManagement; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * @param RulePricesStorage $rulePricesStorage * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $resourceRuleFactory @@ -71,7 +64,6 @@ class PrepareCatalogProductCollectionPricesObserver implements ObserverInterface * @param TimezoneInterface $localeDate * @param CustomerModelSession $customerSession * @param GroupManagementInterface $groupManagement - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter */ public function __construct( RulePricesStorage $rulePricesStorage, @@ -79,8 +71,7 @@ public function __construct( StoreManagerInterface $storeManager, TimezoneInterface $localeDate, CustomerModelSession $customerSession, - GroupManagementInterface $groupManagement, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + GroupManagementInterface $groupManagement ) { $this->rulePricesStorage = $rulePricesStorage; $this->resourceRuleFactory = $resourceRuleFactory; @@ -88,8 +79,6 @@ public function __construct( $this->localeDate = $localeDate; $this->customerSession = $customerSession; $this->groupManagement = $groupManagement; - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); } /** @@ -100,7 +89,7 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { - /* @var $collection ProductCollection */ + /** @var ProductCollection $collection */ $collection = $observer->getEvent()->getCollection(); $store = $this->storeManager->getStore($observer->getEvent()->getStoreId()); $websiteId = $store->getWebsiteId(); @@ -116,7 +105,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($observer->getEvent()->hasDate()) { $date = new \DateTime($observer->getEvent()->getDate()); } else { - $date = (new \DateTime())->setTimestamp($this->ruleDateFormatter->getTimeStamp($store)); + $date = $this->localeDate->date(null, null, false); } $productIds = []; diff --git a/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php b/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php index bd9593693814..2fd23ae39147 100644 --- a/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php +++ b/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php @@ -7,14 +7,9 @@ namespace Magento\CatalogRule\Observer; -use Magento\Catalog\Model\Product; -use Magento\CatalogRule\Model\Rule; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -use Magento\Customer\Model\Session as CustomerModelSession; -use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Registry; use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\App\ObjectManager; /** * Observer for applying catalog rules on product for admin area @@ -43,31 +38,22 @@ class ProcessAdminFinalPriceObserver implements ObserverInterface */ protected $rulePricesStorage; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * @param RulePricesStorage $rulePricesStorage * @param Registry $coreRegistry * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $resourceRuleFactory * @param TimezoneInterface $localeDate - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter */ public function __construct( RulePricesStorage $rulePricesStorage, Registry $coreRegistry, \Magento\CatalogRule\Model\ResourceModel\RuleFactory $resourceRuleFactory, - TimezoneInterface $localeDate, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + TimezoneInterface $localeDate ) { $this->rulePricesStorage = $rulePricesStorage; $this->coreRegistry = $coreRegistry; $this->resourceRuleFactory = $resourceRuleFactory; $this->localeDate = $localeDate; - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); } /** @@ -79,8 +65,7 @@ public function __construct( public function execute(\Magento\Framework\Event\Observer $observer) { $product = $observer->getEvent()->getProduct(); - $storeId = $product->getStoreId(); - $date = $this->ruleDateFormatter->getDate($storeId); + $date = $this->localeDate->date(null, null, false); $key = false; $ruleData = $this->coreRegistry->registry('rule_data'); diff --git a/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php b/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php index 8a2e660ed9aa..b27768ae091e 100644 --- a/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php +++ b/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php @@ -8,13 +8,9 @@ namespace Magento\CatalogRule\Observer; use Magento\Framework\Event\ObserverInterface; -use Magento\Catalog\Model\Product; -use Magento\CatalogRule\Model\Rule; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Customer\Model\Session as CustomerModelSession; -use Magento\Framework\Event\Observer as EventObserver; -use Magento\Framework\App\ObjectManager; /** * Observer for applying catalog rules on product for frontend area @@ -48,34 +44,25 @@ class ProcessFrontFinalPriceObserver implements ObserverInterface */ protected $rulePricesStorage; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * @param RulePricesStorage $rulePricesStorage * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $resourceRuleFactory * @param StoreManagerInterface $storeManager * @param TimezoneInterface $localeDate * @param CustomerModelSession $customerSession - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter */ public function __construct( RulePricesStorage $rulePricesStorage, \Magento\CatalogRule\Model\ResourceModel\RuleFactory $resourceRuleFactory, StoreManagerInterface $storeManager, TimezoneInterface $localeDate, - CustomerModelSession $customerSession, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + CustomerModelSession $customerSession ) { $this->rulePricesStorage = $rulePricesStorage; $this->resourceRuleFactory = $resourceRuleFactory; $this->storeManager = $storeManager; $this->localeDate = $localeDate; $this->customerSession = $customerSession; - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); } /** @@ -93,7 +80,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($observer->hasDate()) { $date = new \DateTime($observer->getEvent()->getDate()); } else { - $date = $this->ruleDateFormatter->getDate($storeId); + $date = $this->localeDate->date(null, null, false); } if ($observer->hasWebsiteId()) { diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php index b9db988dd20b..8bce5456ffa7 100644 --- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php @@ -9,14 +9,13 @@ use Magento\Catalog\Model\Product; use Magento\CatalogRule\Model\ResourceModel\Rule; -use Magento\CatalogRule\Model\ResourceModel\RuleFactory; use Magento\Customer\Model\Session; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\Adjustment\Calculator; use Magento\Framework\Pricing\Price\AbstractPrice; use Magento\Framework\Pricing\Price\BasePriceProviderInterface; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -use Magento\Store\Model\StoreManager; +use Magento\Store\Model\StoreManagerInterface; /** * Class CatalogRulePrice @@ -31,65 +30,50 @@ class CatalogRulePrice extends AbstractPrice implements BasePriceProviderInterfa const PRICE_CODE = 'catalog_rule_price'; /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @var TimezoneInterface */ protected $dateTime; /** - * @var \Magento\Store\Model\StoreManager + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Customer\Model\Session + * @var Session */ protected $customerSession; /** - * @var \Magento\CatalogRule\Model\ResourceModel\RuleFactory - * @deprecated 100.1.1 - */ - protected $resourceRuleFactory; - - /** - * @var \Magento\CatalogRule\Model\ResourceModel\Rule + * @var Rule */ private $ruleResource; - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface - */ - private $ruleDateFormatter; - /** * @param Product $saleableItem * @param float $quantity * @param Calculator $calculator - * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param PriceCurrencyInterface $priceCurrency * @param TimezoneInterface $dateTime - * @param StoreManager $storeManager + * @param StoreManagerInterface $storeManager * @param Session $customerSession - * @param RuleFactory $catalogRuleResourceFactory - * @param \Magento\CatalogRule\Model\RuleDateFormatterInterface|null $ruleDateFormatter + * @param Rule $ruleResource */ public function __construct( Product $saleableItem, $quantity, Calculator $calculator, - \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + PriceCurrencyInterface $priceCurrency, TimezoneInterface $dateTime, - StoreManager $storeManager, + StoreManagerInterface $storeManager, Session $customerSession, - RuleFactory $catalogRuleResourceFactory, - \Magento\CatalogRule\Model\RuleDateFormatterInterface $ruleDateFormatter = null + Rule $ruleResource ) { parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); $this->dateTime = $dateTime; $this->storeManager = $storeManager; $this->customerSession = $customerSession; - $this->resourceRuleFactory = $catalogRuleResourceFactory; - $this->ruleDateFormatter = $ruleDateFormatter ?: ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class); + $this->ruleResource = $ruleResource; } /** @@ -103,13 +87,12 @@ public function getValue() if ($this->product->hasData(self::PRICE_CODE)) { $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false; } else { - $this->value = $this->getRuleResource() - ->getRulePrice( - $this->ruleDateFormatter->getDate($this->storeManager->getStore()->getId()), - $this->storeManager->getStore()->getWebsiteId(), - $this->customerSession->getCustomerGroupId(), - $this->product->getId() - ); + $this->value = $this->ruleResource->getRulePrice( + $this->dateTime->date(null, null, false), + $this->storeManager->getStore()->getWebsiteId(), + $this->customerSession->getCustomerGroupId(), + $this->product->getId() + ); $this->value = $this->value ? (float)$this->value : false; } if ($this->value) { @@ -119,19 +102,4 @@ public function getValue() return $this->value; } - - /** - * Retrieve rule resource - * - * @return Rule - * @deprecated 100.1.1 - */ - private function getRuleResource() - { - if (null === $this->ruleResource) { - $this->ruleResource = ObjectManager::getInstance()->get(Rule::class); - } - - return $this->ruleResource; - } } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php index 29b040d7299d..cb1a7f53f752 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php @@ -7,8 +7,17 @@ namespace Magento\CatalogRule\Test\Unit\Pricing\Price; +use Magento\Catalog\Model\Product; +use Magento\CatalogRule\Model\ResourceModel\Rule; use Magento\CatalogRule\Pricing\Price\CatalogRulePrice; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Customer\Model\Session; +use Magento\Framework\Pricing\Adjustment\Calculator; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; /** * Class CatalogRulePriceTest @@ -18,129 +27,73 @@ class CatalogRulePriceTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\CatalogRule\Pricing\Price\CatalogRulePrice + * @var CatalogRulePrice */ - protected $object; + private $object; /** - * @var \Magento\Framework\Pricing\SaleableInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ - protected $saleableItemMock; + private $saleableItemMock; /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + * @var TimezoneInterface|MockObject */ - protected $dataTimeMock; + private $dataTimeMock; /** - * @var \Magento\Store\Model\StoreManager|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ - protected $storeManagerMock; + private $storeManagerMock; /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - protected $customerSessionMock; + private $customerSessionMock; /** - * @var \Magento\Framework\Pricing\PriceInfo\Base | \PHPUnit_Framework_MockObject_MockObject + * @var Rule|MockObject */ - protected $priceInfoMock; + private $catalogRuleResourceMock; /** - * @var \Magento\CatalogRule\Model\ResourceModel\RuleFactory|\PHPUnit_Framework_MockObject_MockObject + * @var WebsiteInterface|MockObject */ - protected $catalogRuleResourceFactoryMock; + private $coreWebsiteMock; /** - * @var \Magento\CatalogRule\Model\ResourceModel\Rule|\PHPUnit_Framework_MockObject_MockObject + * @var StoreInterface|MockObject */ - protected $catalogRuleResourceMock; + private $coreStoreMock; /** - * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject + * @var Calculator|MockObject */ - protected $coreWebsiteMock; + private $calculator; /** - * @var \Magento\Store\Model\Website|\PHPUnit_Framework_MockObject_MockObject + * @var PriceCurrencyInterface|MockObject */ - protected $coreStoreMock; - - /** - * @var \Magento\Framework\Pricing\Adjustment\Calculator|\PHPUnit_Framework_MockObject_MockObject - */ - protected $calculator; - - /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $priceCurrencyMock; - - /** - * @var \Magento\CatalogRule\Model\RuleDateFormatterInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $ruleDateFormatter; + private $priceCurrencyMock; /** * Set up */ protected function setUp() { - $this->saleableItemMock = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, - ['getId', '__wakeup', 'getPriceInfo', 'hasData', 'getData'] - ); - $this->dataTimeMock = $this->getMockForAbstractClass( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class, - [], - '', - false, - true, - true, - [] - ); - $this->ruleDateFormatter = $this->getMockBuilder(\Magento\CatalogRule\Model\RuleDateFormatterInterface::class) - ->setMethods([]) - ->disableOriginalConstructor() - ->getMock(); - - $this->coreStoreMock = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManager::class); + $this->saleableItemMock = $this->createMock(Product::class); + $this->dataTimeMock = $this->createMock(TimezoneInterface::class); + $this->coreStoreMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); $this->storeManagerMock->expects($this->any()) ->method('getStore') - ->will($this->returnValue($this->coreStoreMock)); - - $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->priceInfoMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo::class) - ->setMethods(['getAdjustments']) - ->disableOriginalConstructor() - ->getMock(); - $this->catalogRuleResourceFactoryMock = $this->createPartialMock( - \Magento\CatalogRule\Model\ResourceModel\RuleFactory::class, - ['create'] - ); - $this->catalogRuleResourceMock = $this->createMock(\Magento\CatalogRule\Model\ResourceModel\Rule::class); - - $this->coreWebsiteMock = $this->createMock(\Magento\Store\Model\Website::class); - - $this->priceInfoMock->expects($this->any()) - ->method('getAdjustments') - ->will($this->returnValue([])); - $this->saleableItemMock->expects($this->any()) - ->method('getPriceInfo') - ->will($this->returnValue($this->priceInfoMock)); - - $this->catalogRuleResourceFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($this->catalogRuleResourceMock)); - - $this->calculator = $this->getMockBuilder(\Magento\Framework\Pricing\Adjustment\Calculator::class) - ->disableOriginalConstructor() - ->getMock(); + ->willReturn($this->coreStoreMock); + $this->customerSessionMock = $this->createMock(Session::class); + $this->catalogRuleResourceMock = $this->createMock(Rule::class); + $this->coreWebsiteMock = $this->createMock(WebsiteInterface::class); + $this->calculator = $this->createMock(Calculator::class); $qty = 1; - - $this->priceCurrencyMock = $this->createMock(\Magento\Framework\Pricing\PriceCurrencyInterface::class); + $this->priceCurrencyMock = $this->createMock(PriceCurrencyInterface::class); $this->object = new CatalogRulePrice( $this->saleableItemMock, @@ -150,13 +103,6 @@ protected function setUp() $this->dataTimeMock, $this->storeManagerMock, $this->customerSessionMock, - $this->catalogRuleResourceFactoryMock, - $this->ruleDateFormatter - ); - - (new ObjectManager($this))->setBackwardCompatibleProperty( - $this->object, - 'ruleResource', $this->catalogRuleResourceMock ); } @@ -166,38 +112,35 @@ protected function setUp() */ public function testGetValue() { - $coreStoreId = 1; - $coreWebsiteId = 1; - $productId = 1; - $customerGroupId = 1; - $dateTime = new \DateTime(); + $coreWebsiteId = 2; + $productId = 4; + $customerGroupId = 3; + $date = new \DateTime(); $catalogRulePrice = 55.12; $convertedPrice = 45.34; - $this->coreStoreMock->expects($this->once()) - ->method('getId') - ->will($this->returnValue($coreStoreId)); + $this->dataTimeMock->expects($this->once()) + ->method('date') + ->with(null, null, false) + ->willReturn($date); $this->coreStoreMock->expects($this->once()) ->method('getWebsiteId') - ->will($this->returnValue($coreWebsiteId)); - $this->ruleDateFormatter->expects($this->once()) - ->method('getDate') - ->with($this->equalTo($coreStoreId)) - ->will($this->returnValue($dateTime)); + ->willReturn($coreWebsiteId); $this->customerSessionMock->expects($this->once()) ->method('getCustomerGroupId') - ->will($this->returnValue($customerGroupId)); + ->willReturn($customerGroupId); $this->catalogRuleResourceMock->expects($this->once()) ->method('getRulePrice') - ->will($this->returnValue($catalogRulePrice)); - $this->saleableItemMock->expects($this->any()) + ->with($date, $coreWebsiteId, $customerGroupId, $productId) + ->willReturn($catalogRulePrice); + $this->saleableItemMock->expects($this->once()) ->method('getId') - ->will($this->returnValue($productId)); - $this->priceCurrencyMock->expects($this->any()) + ->willReturn($productId); + $this->priceCurrencyMock->expects($this->once()) ->method('convertAndRound') ->with($catalogRulePrice) - ->will($this->returnValue($convertedPrice)); + ->willReturn($convertedPrice); $this->assertEquals($convertedPrice, $this->object->getValue()); } diff --git a/app/code/Magento/CatalogRule/etc/di.xml b/app/code/Magento/CatalogRule/etc/di.xml index c9e0fa46fc67..e0d91db54239 100644 --- a/app/code/Magento/CatalogRule/etc/di.xml +++ b/app/code/Magento/CatalogRule/etc/di.xml @@ -164,5 +164,4 @@ <argument name="customConditionProvider" xsi:type="object">CatalogRuleCustomConditionProvider</argument> </arguments> </type> - <preference for="Magento\CatalogRule\Model\RuleDateFormatterInterface" type="Magento\CatalogRule\Model\RuleDateFormatter" /> </config> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml index 7937092965f2..797a712c6476 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml @@ -19,4 +19,27 @@ <see selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" userInput="{{orderSummaryShippingTotal}}" stepKey="seeCorrectShipping"/> <see selector="{{CheckoutPaymentSection.orderSummaryTotal}}" userInput="{{orderSummaryTotal}}" stepKey="seeCorrectOrderTotal"/> </actionGroup> + <!-- Assert Order Summary SubTotal You should be on checkout page --> + <actionGroup name="AssertStorefrontCheckoutPaymentSummarySubtotalActionGroup"> + <arguments> + <argument name="orderSubtotal" type="string"/> + </arguments> + <waitForPageLoad time="30" stepKey="waitForCartFullyLoaded"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" time="30" stepKey="waitForOrderSummaryBlock"/> + <see selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" userInput="{{orderSubtotal}}" stepKey="seeCorrectSubtotal"/> + </actionGroup> + <!-- Assert Order Summary Total You should be on checkout page --> + <actionGroup name="AssertStorefrontCheckoutPaymentSummaryTotalActionGroup"> + <arguments> + <argument name="orderTotal" type="string"/> + </arguments> + <waitForPageLoad time="30" stepKey="waitForCartFullyLoaded"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.orderSummaryTotal}}" time="30" stepKey="waitForOrderSummaryBlock"/> + <see selector="{{CheckoutPaymentSection.orderSummaryTotal}}" userInput="{{orderTotal}}" stepKey="seeCorrectOrderTotal"/> + </actionGroup> + <!-- Assert Order Summary Total Is Not Shown You should be on checkout page --> + <actionGroup name="AssertStorefrontCheckoutPaymentSummaryTotalMissingActionGroup"> + <waitForPageLoad time="30" stepKey="waitForCartFullyLoaded"/> + <dontSeeElement selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="seeTotalElement"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js index 1920bc4d7ac4..a8e70b65019c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -6,7 +6,7 @@ define([ 'Magento_Ui/js/modal/alert', 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/validation' ], function (alert, $) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/discount-codes.js b/app/code/Magento/Checkout/view/frontend/web/js/discount-codes.js index db6e8fef2445..1f38ff28a860 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/discount-codes.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/discount-codes.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index 80481826260f..a9cbb1194cfd 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -7,7 +7,7 @@ define([ 'jquery', 'mage/template', 'underscore', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/validation' ], function ($, mageTemplate, _) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index eecfa65b189d..39bd07f0c73a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index e66c66006246..e67b04e6104c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -10,7 +10,7 @@ define([ 'Magento_Ui/js/modal/alert', 'Magento_Ui/js/modal/confirm', 'underscore', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/decorate', 'mage/collapsible', 'mage/cookies' diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml index ff6167ffc10e..b1d0faa7507f 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml @@ -11,7 +11,7 @@ <section name="TinyMCESection"> <element name="checkIfContentTabOpen" type="button" selector="//span[text()='Content']/parent::strong/parent::*[@data-state-collapsible='closed']"/> <element name="CheckIfTabExpand" type="button" selector="//div[@data-state-collapsible='closed']//span[text()='Content']"/> - <element name="TinyMCE4" type="text" selector=".mce-branding-powered-by" /> + <element name="TinyMCE4" type="text" selector=".mce-branding" /> <element name="InsertWidgetBtn" type="button" selector=".action-add-widget"/> <element name="InsertWidgetIcon" type="button" selector="div[aria-label='Insert Widget']"/> <element name="InsertVariableBtn" type="button" selector=".scalable.add-variable.plugin"/> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml index b5bfe9cc2ea0..fd49c1482c13 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml @@ -13,5 +13,6 @@ <element name="generalTabOpened" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='true' or @aria-expanded='1']//strong[text()='General']"/> <element name="defaultConfigButton" type="button" selector="#store-change-button" timeout="30"/> <element name="defaultConfigDropdown" type="button" selector="//ul[@class='dropdown-menu']" timeout="30"/> + <element name="fieldError" type="text" selector="label.mage-error"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 5e222d511234..332b2b765e1c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -194,6 +194,12 @@ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> </actionGroup> + <!--Generate and save configurable product after setting options--> + <actionGroup name="GenerateAndSaveConfiguredProductAfterSettingOptions" extends="saveConfiguredProduct"> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" before="clickOnSaveButton2" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" after="clickOnNextButton" stepKey="clickOnGenerateProductsButton"/> + </actionGroup> + <actionGroup name="addNewProductConfigurationAttribute"> <arguments> <argument name="attribute" type="entity"/> @@ -358,4 +364,11 @@ <click selector="{{AdminProductFormConfigurationsSection.disableProductBtn}}" stepKey="clickDisableChildProduct"/> <see selector="{{AdminProductFormConfigurationsSection.confProductOptionStatusCell(productName)}}" userInput="Disabled" stepKey="seeConfigDisabled"/> </actionGroup> + + <!--You are on AdminProductEditPage--> + <!--Start create configurations for attribute and fill quantity--> + <actionGroup name="StartCreateConfigurationsForAttribute" extends="generateConfigurationsByAttributeCode"> + <remove keyForRemoval="clickOnNextButton3"/> + <remove keyForRemoval="clickOnNextButton4"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml index 5633c3675ca8..ad30c91967c3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml @@ -304,6 +304,7 @@ <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> </after> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/modal-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/modal-configurable.js index 61dfe04a90f7..4aaa8aaffd2b 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/modal-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/modal-configurable.js @@ -34,6 +34,8 @@ define([ } this._super(); + } else { + this.form().focusInvalid(); } } }); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index ef40dcb9a732..c0128dffe704 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -12,7 +12,7 @@ define([ 'mage/translate', 'priceUtils', 'priceBox', - 'jquery/ui', + 'jquery-ui-modules/widget', 'jquery/jquery.parsequery' ], function ($, _, mageTemplate, $t, priceUtils) { 'use strict'; diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php index 3216fb69c074..f1971e228ac0 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -7,8 +7,14 @@ namespace Magento\ConfigurableProductGraphQl\Model\Cart\BuyRequest; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\Stdlib\ArrayManager; use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection; +use Magento\Framework\EntityManager\MetadataPool; /** * DataProvider for building super attribute options in buy requests @@ -20,13 +26,37 @@ class SuperAttributeDataProvider implements BuyRequestDataProviderInterface */ private $arrayManager; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var OptionCollection + */ + private $optionCollection; + + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param ArrayManager $arrayManager + * @param ProductRepositoryInterface $productRepository + * @param OptionCollection $optionCollection + * @param MetadataPool $metadataPool */ public function __construct( - ArrayManager $arrayManager + ArrayManager $arrayManager, + ProductRepositoryInterface $productRepository, + OptionCollection $optionCollection, + MetadataPool $metadataPool ) { $this->arrayManager = $arrayManager; + $this->productRepository = $productRepository; + $this->optionCollection = $optionCollection; + $this->metadataPool = $metadataPool; } /** @@ -34,13 +64,32 @@ public function __construct( */ public function execute(array $cartItemData): array { - $superAttributes = $this->arrayManager->get('configurable_attributes', $cartItemData, []); + $parentSku = $this->arrayManager->get('parent_sku', $cartItemData); + if ($parentSku === null) { + return []; + } + $sku = $this->arrayManager->get('data/sku', $cartItemData); - $superAttributesData = []; - foreach ($superAttributes as $superAttribute) { - $superAttributesData[$superAttribute['id']] = $superAttribute['value']; + try { + $parentProduct = $this->productRepository->get($parentSku); + $product = $this->productRepository->get($sku); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__('Could not find specified product.')); } + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $this->optionCollection->addProductId((int)$parentProduct->getData($linkField)); + $options = $this->optionCollection->getAttributesByProductId((int)$parentProduct->getData($linkField)); + $superAttributesData = []; + foreach ($options as $option) { + $code = $option['attribute_code']; + foreach ($option['values'] as $optionValue) { + if ($optionValue['value_index'] === $product->getData($code)) { + $superAttributesData[$option['attribute_id']] = $optionValue['value_index']; + break; + } + } + } return ['super_attribute' => $superAttributesData]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index a7fb82e40220..5053ed848b4e 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -50,16 +50,11 @@ type AddConfigurableProductsToCartOutput { input ConfigurableProductCartItemInput { data: CartItemInput! - variant_sku: String @deprecated(reason: "Use CartItemInput.sku instead") - configurable_attributes: [ConfigurableCartItemAttributesInput]! + variant_sku: String @deprecated(reason: "Use CartItemInput.sku instead.") + parent_sku: String @doc(description: "Configurable product SKU.") customizable_options:[CustomizableOptionInput!] } -input ConfigurableCartItemAttributesInput { - id: Int! - value: Int! -} - type ConfigurableCartItem implements CartItemInterface { customizable_options: [SelectedCustomizableOption]! configurable_options: [SelectedConfigurableOption!]! @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableCartItemOptions") diff --git a/app/code/Magento/Cookie/view/frontend/web/js/notices.js b/app/code/Magento/Cookie/view/frontend/web/js/notices.js index f1f3754ea54b..2c4a07013080 100644 --- a/app/code/Magento/Cookie/view/frontend/web/js/notices.js +++ b/app/code/Magento/Cookie/view/frontend/web/js/notices.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/cookies' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js b/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js index 8fd329ee0e0e..0a175136f034 100644 --- a/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js +++ b/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Customer/view/frontend/web/js/address.js b/app/code/Magento/Customer/view/frontend/web/js/address.js index c6d05b51bdf0..6ee269e9235c 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/address.js +++ b/app/code/Magento/Customer/view/frontend/web/js/address.js @@ -6,7 +6,7 @@ define([ 'jquery', 'Magento_Ui/js/modal/confirm', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], function ($, confirm) { 'use strict'; diff --git a/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js b/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js index c014b814ea98..dc2c4bb82af5 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js +++ b/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js @@ -9,7 +9,7 @@ define([ 'mageUtils', 'mage/translate', 'Magento_Checkout/js/model/postcode-validator', - 'jquery/ui', + 'jquery-ui-modules/widget', 'validation' ], function ($, __, utils, $t, postCodeValidator) { 'use strict'; diff --git a/app/code/Magento/Customer/view/frontend/web/js/change-email-password.js b/app/code/Magento/Customer/view/frontend/web/js/change-email-password.js index cf3ea48844b9..8ac5ef8bae99 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/change-email-password.js +++ b/app/code/Magento/Customer/view/frontend/web/js/change-email-password.js @@ -4,7 +4,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Customer/view/frontend/web/js/checkout-balance.js b/app/code/Magento/Customer/view/frontend/web/js/checkout-balance.js index 8d5242e70379..9bcea2f8fa18 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/checkout-balance.js +++ b/app/code/Magento/Customer/view/frontend/web/js/checkout-balance.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php index 37c6a96542e2..caf2f7745a3d 100644 --- a/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ b/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php @@ -35,7 +35,10 @@ public function apply() { $this->schemaSetup->startSetup(); - $this->schemaSetup->getConnection()->changeTableEngine('catalog_product_index_price_downlod_tmp', 'InnoDB'); + $tableName = $this->schemaSetup->getTable('catalog_product_index_price_downlod_tmp'); + if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { + $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); + } $this->schemaSetup->endSetup(); } diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml index 36032d58ab25..699b384a8cda 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml @@ -210,7 +210,7 @@ var uploaderTemplate = '<div class="no-display" id="[[idName]]-template">' + <input type="checkbox" data-action="change-type-product-downloadable" class="admin__control-checkbox" name="is_downloadable" id="is-downloaodable" <?= $block->isDownloadable() ? 'checked="checked"' : ''?> /> <label class="admin__field-label" for="is-downloaodable"> - <span><?= /* @noEscape */ __('Is this a downloadable Product?'); ?></span> + <span><?= $block->escapeHtml(__('Is this a downloadable Product?')); ?></span> </label> </div> <div class="entry-edit" id="product_info_tabs_downloadable_items"> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml index 33c2afe827d0..c4f7ffa51f89 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml @@ -51,7 +51,7 @@ $block->getConfigJson(); </table> </div> <div class="admin__field-note"> - <?= /* @noEscape */ __('Alphanumeric, dash and underscore characters are recommended for filenames. Improper characters are replaced with \'_\'.') ?> + <?= $block->escapeHtml(__('Alphanumeric, dash and underscore characters are recommended for filenames. Improper characters are replaced with \'_\'.')) ?> </div> </div> </div> diff --git a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js index 64cca1efd8b0..09a5ad1afa9e 100644 --- a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js +++ b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js @@ -7,7 +7,7 @@ */ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'Magento_Catalog/js/price-box' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Email/Model/Transport.php b/app/code/Magento/Email/Model/Transport.php index 90a4e6571c9b..cbce1682cb5f 100644 --- a/app/code/Magento/Email/Model/Transport.php +++ b/app/code/Magento/Email/Model/Transport.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Email\Model; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\MailException; +use Magento\Framework\Mail\EmailMessageInterface; use Magento\Framework\Mail\MessageInterface; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Phrase; @@ -59,12 +62,12 @@ class Transport implements TransportInterface private $message; /** - * @param MessageInterface $message Email message object + * @param EmailMessageInterface $message Email message object * @param ScopeConfigInterface $scopeConfig Core store config * @param null|string|array|\Traversable $parameters Config options for sendmail parameters */ public function __construct( - MessageInterface $message, + EmailMessageInterface $message, ScopeConfigInterface $scopeConfig, $parameters = null ) { diff --git a/app/code/Magento/GiftMessage/view/frontend/web/js/extra-options.js b/app/code/Magento/GiftMessage/view/frontend/web/js/extra-options.js index 1c40f6b7c32e..45e67b5b5937 100644 --- a/app/code/Magento/GiftMessage/view/frontend/web/js/extra-options.js +++ b/app/code/Magento/GiftMessage/view/frontend/web/js/extra-options.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/GiftMessage/view/frontend/web/js/gift-options.js b/app/code/Magento/GiftMessage/view/frontend/web/js/gift-options.js index ca190bf28911..cff3324fceaf 100644 --- a/app/code/Magento/GiftMessage/view/frontend/web/js/gift-options.js +++ b/app/code/Magento/GiftMessage/view/frontend/web/js/gift-options.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php index 571b725e7335..fc2207dcd7c8 100644 --- a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php +++ b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php @@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Magento\Framework\MessageQueue\ConsumerFactory; -use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; +use Magento\Framework\Lock\LockManagerInterface; /** * Command for starting MessageQueue consumers. @@ -22,6 +22,7 @@ class StartConsumerCommand extends Command const OPTION_NUMBER_OF_MESSAGES = 'max-messages'; const OPTION_BATCH_SIZE = 'batch-size'; const OPTION_AREACODE = 'area-code'; + const OPTION_SINGLE_THREAD = 'single-thread'; const PID_FILE_PATH = 'pid-file-path'; const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start'; @@ -36,9 +37,9 @@ class StartConsumerCommand extends Command private $appState; /** - * @var PidConsumerManager + * @var LockManagerInterface */ - private $pidConsumerManager; + private $lockManager; /** * StartConsumerCommand constructor. @@ -47,23 +48,23 @@ class StartConsumerCommand extends Command * @param \Magento\Framework\App\State $appState * @param ConsumerFactory $consumerFactory * @param string $name - * @param PidConsumerManager $pidConsumerManager + * @param LockManagerInterface $lockManager */ public function __construct( \Magento\Framework\App\State $appState, ConsumerFactory $consumerFactory, $name = null, - PidConsumerManager $pidConsumerManager = null + LockManagerInterface $lockManager = null ) { $this->appState = $appState; $this->consumerFactory = $consumerFactory; - $this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(PidConsumerManager::class); + $this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(LockManagerInterface::class); parent::__construct($name); } /** - * {@inheritdoc} + * @inheritdoc */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -71,30 +72,36 @@ protected function execute(InputInterface $input, OutputInterface $output) $numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES); $batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE); $areaCode = $input->getOption(self::OPTION_AREACODE); - $pidFilePath = $input->getOption(self::PID_FILE_PATH); - if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) { - $output->writeln('<error>Consumer with the same PID is running</error>'); - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + if ($input->getOption(self::PID_FILE_PATH)) { + $input->setOption(self::OPTION_SINGLE_THREAD, true); } - if ($pidFilePath) { - $this->pidConsumerManager->savePid($pidFilePath); + $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); + + if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + $output->writeln('<error>Consumer with the same name is running</error>'); + return \Magento\Framework\Console\Cli::RETURN_FAILURE; } - if ($areaCode !== null) { - $this->appState->setAreaCode($areaCode); - } else { - $this->appState->setAreaCode('global'); + if ($singleThread) { + $this->lockManager->lock(md5($consumerName)); //phpcs:ignore } + $this->appState->setAreaCode($areaCode ?? 'global'); + $consumer = $this->consumerFactory->get($consumerName, $batchSize); $consumer->process($numberOfMessages); + + if ($singleThread) { + $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore + } + return \Magento\Framework\Console\Cli::RETURN_SUCCESS; } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -125,11 +132,17 @@ protected function configure() 'The preferred area (global, adminhtml, etc...) ' . 'default is global.' ); + $this->addOption( + self::OPTION_SINGLE_THREAD, + null, + InputOption::VALUE_NONE, + 'This option prevents running multiple copies of one consumer simultaneously.' + ); $this->addOption( self::PID_FILE_PATH, null, InputOption::VALUE_REQUIRED, - 'The file path for saving PID' + 'The file path for saving PID (This option is deprecated, use --single-thread instead)' ); $this->setHelp( <<<HELP @@ -150,8 +163,12 @@ protected function configure() To specify the preferred area: <comment>%command.full_name% someConsumer --area-code='adminhtml'</comment> + +To do not run multiple copies of one consumer simultaneously: + + <comment>%command.full_name% someConsumer --single-thread'</comment> -To save PID enter path: +To save PID enter path (This option is deprecated, use --single-thread instead): <comment>%command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid'</comment> HELP diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php index f5011248d777..056cf4fc57a2 100644 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php +++ b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php @@ -13,18 +13,13 @@ use Magento\Framework\App\DeploymentConfig; use Psr\Log\LoggerInterface; use Symfony\Component\Process\PhpExecutableFinder; -use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; +use Magento\Framework\Lock\LockManagerInterface; /** * Class for running consumers processes by cron */ class ConsumersRunner { - /** - * Extension of PID file - */ - const PID_FILE_EXT = '.pid'; - /** * Shell command line wrapper for executing command in background * @@ -53,13 +48,6 @@ class ConsumersRunner */ private $phpExecutableFinder; - /** - * The class for checking status of process by PID - * - * @var PidConsumerManager - */ - private $pidConsumerManager; - /** * @var ConnectionTypeResolver */ @@ -70,13 +58,20 @@ class ConsumersRunner */ private $logger; + /** + * Lock Manager + * + * @var LockManagerInterface + */ + private $lockManager; + /** * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed * for the PHP executable * @param ConsumerConfigInterface $consumerConfig The consumer config provider * @param DeploymentConfig $deploymentConfig The application deployment configuration * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background - * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID + * @param LockManagerInterface $lockManager The lock manager * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver * @param LoggerInterface $logger Logger */ @@ -85,7 +80,7 @@ public function __construct( ConsumerConfigInterface $consumerConfig, DeploymentConfig $deploymentConfig, ShellInterface $shellBackground, - PidConsumerManager $pidConsumerManager, + LockManagerInterface $lockManager, ConnectionTypeResolver $mqConnectionTypeResolver = null, LoggerInterface $logger = null ) { @@ -93,7 +88,7 @@ public function __construct( $this->consumerConfig = $consumerConfig; $this->deploymentConfig = $deploymentConfig; $this->shellBackground = $shellBackground; - $this->pidConsumerManager = $pidConsumerManager; + $this->lockManager = $lockManager; $this->mqConnectionTypeResolver = $mqConnectionTypeResolver ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); $this->logger = $logger @@ -120,11 +115,9 @@ public function run() continue; } - $consumerName = $consumer->getName(); - $arguments = [ - $consumerName, - '--pid-file-path=' . $this->getPidFilePath($consumerName), + $consumer->getName(), + '--single-thread' ]; if ($maxMessages) { @@ -154,7 +147,7 @@ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $al return false; } - if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) { + if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore return false; } @@ -162,28 +155,17 @@ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $al try { $this->mqConnectionTypeResolver->getConnectionType($connectionName); } catch (\LogicException $e) { - $this->logger->info(sprintf( - 'Consumer "%s" skipped as required connection "%s" is not configured. %s', - $consumerName, - $connectionName, - $e->getMessage() - )); + $this->logger->info( + sprintf( + 'Consumer "%s" skipped as required connection "%s" is not configured. %s', + $consumerName, + $connectionName, + $e->getMessage() + ) + ); return false; } return true; } - - /** - * Returns default path to file with PID by consumers name - * - * @param string $consumerName The consumers name - * @return string The path to file with PID - */ - private function getPidFilePath($consumerName) - { - $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); - - return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; - } } diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner/PidConsumerManager.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner/PidConsumerManager.php deleted file mode 100644 index d5f827320ac7..000000000000 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner/PidConsumerManager.php +++ /dev/null @@ -1,127 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MessageQueue\Model\Cron\ConsumersRunner; - -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem; -use Magento\Framework\Exception\FileSystemException; - -/** - * The class for checking status of process by PID - */ -class PidConsumerManager -{ - /** - * Extension of PID file - * @deprecated Moved to the correct responsibility area - * @see \Magento\MessageQueue\Model\Cron\ConsumersRunner::PID_FILE_EXT - */ - const PID_FILE_EXT = '.pid'; - - /** - * The class for working with FS - * - * @var Filesystem - */ - private $filesystem; - - /** - * @param Filesystem $filesystem The class for working with FS - */ - public function __construct(Filesystem $filesystem) - { - $this->filesystem = $filesystem; - } - - /** - * Checks if consumer process is run by pid from pidFile - * - * @param string $pidFilePath The path to file with PID - * @return bool Returns true if consumer process is run - * @throws FileSystemException - */ - public function isRun($pidFilePath) - { - $pid = $this->getPid($pidFilePath); - if ($pid) { - if (function_exists('posix_getpgid')) { - return (bool) posix_getpgid($pid); - } else { - return $this->checkIsProcessExists($pid); - } - } - - return false; - } - - /** - * Checks that process is running - * - * If php function exec is not available throws RuntimeException - * If shell command returns non-zero code and this code is not 1 throws RuntimeException - * - * @param int $pid A pid of process - * @return bool Returns true if consumer process is run - * @throws \RuntimeException - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - private function checkIsProcessExists($pid) - { - if (!function_exists('exec')) { - throw new \RuntimeException('Function exec is not available'); - } - - exec(escapeshellcmd('ps -p ' . $pid), $output, $code); - - $code = (int) $code; - - switch ($code) { - case 0: - return true; - break; - case 1: - return false; - break; - default: - throw new \RuntimeException('Exec returned non-zero code', $code); - break; - } - } - - /** - * Returns pid by pidFile path - * - * @param string $pidFilePath The path to file with PID - * @return int Returns pid if pid file exists for consumer else returns 0 - * @throws FileSystemException - */ - public function getPid($pidFilePath) - { - /** @var ReadInterface $directory */ - $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); - - if ($directory->isExist($pidFilePath)) { - return (int) $directory->readFile($pidFilePath); - } - - return 0; - } - - /** - * Saves pid of current process to file - * - * @param string $pidFilePath The path to file with pid - * @throws FileSystemException - */ - public function savePid($pidFilePath) - { - /** @var WriteInterface $directory */ - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); - $directory->writeFile($pidFilePath, function_exists('posix_getpid') ? posix_getpid() : getmypid(), 'w'); - } -} diff --git a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php index 922da3bfc877..f62a7afe323f 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php @@ -8,7 +8,7 @@ use Magento\MessageQueue\Console\StartConsumerCommand; use Magento\Framework\Filesystem\File\WriteFactory; -use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; +use Magento\Framework\Lock\LockManagerInterface; /** * Unit tests for StartConsumerCommand. @@ -36,9 +36,9 @@ class StartConsumerCommandTest extends \PHPUnit\Framework\TestCase private $writeFactoryMock; /** - * @var PidConsumerManager|\PHPUnit_Framework_MockObject_MockObject + * @var LockManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $pidConsumerManagerMock; + private $lockManagerMock; /** * @var StartConsumerCommand @@ -50,9 +50,8 @@ class StartConsumerCommandTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->pidConsumerManagerMock = $this->getMockBuilder(PidConsumerManager::class) - ->disableOriginalConstructor() - ->getMock(); + $this->lockManagerMock = $this->getMockBuilder(LockManagerInterface::class) + ->getMockForAbstractClass(); $this->consumerFactory = $this->getMockBuilder(\Magento\Framework\MessageQueue\ConsumerFactory::class) ->disableOriginalConstructor()->getMock(); $this->appState = $this->getMockBuilder(\Magento\Framework\App\State::class) @@ -68,7 +67,7 @@ protected function setUp() 'consumerFactory' => $this->consumerFactory, 'appState' => $this->appState, 'writeFactory' => $this->writeFactoryMock, - 'pidConsumerManager' => $this->pidConsumerManagerMock, + 'lockManager' => $this->lockManagerMock, ] ); parent::setUp(); @@ -78,9 +77,11 @@ protected function setUp() * Test for execute method. * * @param string|null $pidFilePath - * @param int $savePidExpects - * @param int $isRunExpects - * @param bool $isRun + * @param bool $singleThread + * @param int $lockExpects + * @param int $isLockedExpects + * @param bool $isLocked + * @param int $unlockExpects * @param int $runProcessExpects * @param int $expectedReturn * @return void @@ -88,9 +89,11 @@ protected function setUp() */ public function testExecute( $pidFilePath, - $savePidExpects, - $isRunExpects, - $isRun, + $singleThread, + $lockExpects, + $isLockedExpects, + $isLocked, + $unlockExpects, $runProcessExpects, $expectedReturn ) { @@ -105,17 +108,19 @@ public function testExecute( $input->expects($this->once())->method('getArgument') ->with(\Magento\MessageQueue\Console\StartConsumerCommand::ARGUMENT_CONSUMER) ->willReturn($consumerName); - $input->expects($this->exactly(4))->method('getOption') + $input->expects($this->exactly(5))->method('getOption') ->withConsecutive( [\Magento\MessageQueue\Console\StartConsumerCommand::OPTION_NUMBER_OF_MESSAGES], [\Magento\MessageQueue\Console\StartConsumerCommand::OPTION_BATCH_SIZE], [\Magento\MessageQueue\Console\StartConsumerCommand::OPTION_AREACODE], - [\Magento\MessageQueue\Console\StartConsumerCommand::PID_FILE_PATH] + [\Magento\MessageQueue\Console\StartConsumerCommand::PID_FILE_PATH], + [\Magento\MessageQueue\Console\StartConsumerCommand::OPTION_SINGLE_THREAD] )->willReturnOnConsecutiveCalls( $numberOfMessages, $batchSize, $areaCode, - $pidFilePath + $pidFilePath, + $singleThread ); $this->appState->expects($this->exactly($runProcessExpects))->method('setAreaCode')->with($areaCode); $consumer = $this->getMockBuilder(\Magento\Framework\MessageQueue\ConsumerInterface::class) @@ -124,14 +129,17 @@ public function testExecute( ->method('get')->with($consumerName, $batchSize)->willReturn($consumer); $consumer->expects($this->exactly($runProcessExpects))->method('process')->with($numberOfMessages); - $this->pidConsumerManagerMock->expects($this->exactly($isRunExpects)) - ->method('isRun') - ->with($pidFilePath) - ->willReturn($isRun); + $this->lockManagerMock->expects($this->exactly($isLockedExpects)) + ->method('isLocked') + ->with(md5($consumerName)) //phpcs:ignore + ->willReturn($isLocked); - $this->pidConsumerManagerMock->expects($this->exactly($savePidExpects)) - ->method('savePid') - ->with($pidFilePath); + $this->lockManagerMock->expects($this->exactly($lockExpects)) + ->method('lock') + ->with(md5($consumerName)); //phpcs:ignore + $this->lockManagerMock->expects($this->exactly($unlockExpects)) + ->method('unlock') + ->with(md5($consumerName)); //phpcs:ignore $this->assertEquals( $expectedReturn, @@ -147,25 +155,31 @@ public function executeDataProvider() return [ [ 'pidFilePath' => null, - 'savePidExpects' => 0, - 'isRunExpects' => 0, - 'isRun' => false, + 'singleThread' => false, + 'lockExpects' => 0, + 'isLockedExpects' => 0, + 'isLocked' => false, + 'unlockExpects' => 0, 'runProcessExpects' => 1, 'expectedReturn' => \Magento\Framework\Console\Cli::RETURN_SUCCESS, ], [ 'pidFilePath' => '/var/consumer.pid', - 'savePidExpects' => 1, - 'isRunExpects' => 1, - 'isRun' => false, + 'singleThread' => true, + 'lockExpects' => 1, + 'isLockedExpects' => 1, + 'isLocked' => false, + 'unlockExpects' => 1, 'runProcessExpects' => 1, 'expectedReturn' => \Magento\Framework\Console\Cli::RETURN_SUCCESS, ], [ 'pidFilePath' => '/var/consumer.pid', - 'savePidExpects' => 0, - 'isRunExpects' => 1, - 'isRun' => true, + 'singleThread' => true, + 'lockExpects' => 0, + 'isLockedExpects' => 1, + 'isLocked' => true, + 'unlockExpects' => 0, 'runProcessExpects' => 0, 'expectedReturn' => \Magento\Framework\Console\Cli::RETURN_FAILURE, ], @@ -186,6 +200,7 @@ public function testConfigure() $this->command->getDefinition()->getOption(StartConsumerCommand::OPTION_NUMBER_OF_MESSAGES); $this->command->getDefinition()->getOption(StartConsumerCommand::OPTION_AREACODE); $this->command->getDefinition()->getOption(StartConsumerCommand::PID_FILE_PATH); + $this->command->getDefinition()->getOption(StartConsumerCommand::OPTION_SINGLE_THREAD); $this->assertContains('To start consumer which will process', $this->command->getHelp()); } } diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunner/PidConsumerManagerTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunner/PidConsumerManagerTest.php deleted file mode 100644 index 3d48e0b19ef2..000000000000 --- a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunner/PidConsumerManagerTest.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MessageQueue\Test\Unit\Model\Cron\ConsumersRunner; - -use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem; -use \PHPUnit_Framework_MockObject_MockObject as MockObject; - -class PidConsumerManagerTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Filesystem|MockObject - */ - private $filesystemMock; - - /** - * @var PidConsumerManager - */ - private $pidConsumerManager; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - require_once __DIR__ . '/../../../_files/pid_consumer_functions_mocks.php'; - - $this->filesystemMock = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->pidConsumerManager = new PidConsumerManager($this->filesystemMock); - } - - /** - * @param bool $fileExists - * @param int|null $pid - * @param bool $expectedResult - * @dataProvider isRunDataProvider - */ - public function testIsRun($fileExists, $pid, $expectedResult) - { - $pidFilePath = 'somepath/consumerName.pid'; - - /** @var ReadInterface|MockObject $directoryMock */ - $directoryMock = $this->getMockBuilder(ReadInterface::class) - ->getMockForAbstractClass(); - $directoryMock->expects($this->once()) - ->method('isExist') - ->willReturn($fileExists); - $directoryMock->expects($this->any()) - ->method('readFile') - ->with($pidFilePath) - ->willReturn($pid); - - $this->filesystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->with(DirectoryList::VAR_DIR) - ->willReturn($directoryMock); - - $this->assertSame($expectedResult, $this->pidConsumerManager->isRun($pidFilePath)); - } - - /** - * @return array - */ - public function isRunDataProvider() - { - return [ - ['fileExists' => false, 'pid' => null, false], - ['fileExists' => false, 'pid' => 11111, false], - ['fileExists' => true, 'pid' => 11111, true], - ['fileExists' => true, 'pid' => 77777, false], - ]; - } - - public function testSavePid() - { - $pidFilePath = '/var/somePath/pidfile.pid'; - - /** @var WriteInterface|MockObject $writeMock */ - $writeMock = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - $this->filesystemMock->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::VAR_DIR) - ->willReturn($writeMock); - $writeMock->expects($this->once()) - ->method('writeFile') - ->with( - $pidFilePath, - function_exists('posix_getpid') ? posix_getpid() : getmypid(), - 'w' - ); - - $this->pidConsumerManager->savePid($pidFilePath); - } -} diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php index 006354b997d3..e19467f798a1 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php @@ -12,15 +12,18 @@ use Magento\Framework\MessageQueue\Consumer\Config\ConsumerConfigItemInterface; use Magento\Framework\App\DeploymentConfig; use Magento\MessageQueue\Model\Cron\ConsumersRunner; -use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; use Symfony\Component\Process\PhpExecutableFinder; +use Magento\Framework\Lock\LockManagerInterface; +/** + * Unit tests for ConsumersRunner. + */ class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase { /** - * @var PidConsumerManager|MockObject + * @var LockManagerInterface|MockObject */ - private $pidConsumerManagerMock; + private $lockManagerMock; /** * @var ShellInterface|MockObject @@ -62,9 +65,8 @@ protected function setUp() $this->phpExecutableFinderMock = $this->getMockBuilder(phpExecutableFinder::class) ->disableOriginalConstructor() ->getMock(); - $this->pidConsumerManagerMock = $this->getMockBuilder(PidConsumerManager::class) - ->disableOriginalConstructor() - ->getMock(); + $this->lockManagerMock = $this->getMockBuilder(LockManagerInterface::class) + ->getMockForAbstractClass(); $this->shellBackgroundMock = $this->getMockBuilder(ShellInterface::class) ->getMockForAbstractClass(); $this->consumerConfigMock = $this->getMockBuilder(ConsumerConfigInterface::class) @@ -82,7 +84,7 @@ protected function setUp() $this->consumerConfigMock, $this->deploymentConfigMock, $this->shellBackgroundMock, - $this->pidConsumerManagerMock, + $this->lockManagerMock, $this->connectionTypeResover ); } @@ -91,16 +93,18 @@ public function testRunDisabled() { $this->deploymentConfigMock->expects($this->once()) ->method('get') - ->willReturnMap([ - ['cron_consumers_runner/cron_run', true, false], - ['cron_consumers_runner/max_messages', 10000, 10000], - ['cron_consumers_runner/consumers', [], []], - ]); + ->willReturnMap( + [ + ['cron_consumers_runner/cron_run', true, false], + ['cron_consumers_runner/max_messages', 10000, 10000], + ['cron_consumers_runner/consumers', [], []], + ] + ); $this->consumerConfigMock->expects($this->never()) ->method('getConsumers'); - $this->pidConsumerManagerMock->expects($this->never()) - ->method('isRun'); + $this->lockManagerMock->expects($this->never()) + ->method('isLocked'); $this->shellBackgroundMock->expects($this->never()) ->method('execute'); @@ -109,7 +113,7 @@ public function testRunDisabled() /** * @param int $maxMessages - * @param bool $isRun + * @param bool $isLocked * @param string $php * @param string $command * @param array $arguments @@ -120,7 +124,7 @@ public function testRunDisabled() */ public function testRun( $maxMessages, - $isRun, + $isLocked, $php, $command, $arguments, @@ -129,15 +133,16 @@ public function testRun( $isRunExpects ) { $consumerName = 'consumerName'; - $pidFilePath = 'consumerName-myHostName.pid'; $this->deploymentConfigMock->expects($this->exactly(3)) ->method('get') - ->willReturnMap([ - ['cron_consumers_runner/cron_run', true, true], - ['cron_consumers_runner/max_messages', 10000, $maxMessages], - ['cron_consumers_runner/consumers', [], $allowedConsumers], - ]); + ->willReturnMap( + [ + ['cron_consumers_runner/cron_run', true, true], + ['cron_consumers_runner/max_messages', 10000, $maxMessages], + ['cron_consumers_runner/consumers', [], $allowedConsumers], + ] + ); /** @var ConsumerConfigInterface|MockObject $firstCunsumer */ $consumer = $this->getMockBuilder(ConsumerConfigItemInterface::class) @@ -154,10 +159,10 @@ public function testRun( ->method('getConsumers') ->willReturn([$consumer]); - $this->pidConsumerManagerMock->expects($this->exactly($isRunExpects)) - ->method('isRun') - ->with($pidFilePath) - ->willReturn($isRun); + $this->lockManagerMock->expects($this->exactly($isRunExpects)) + ->method('isLocked') + ->with(md5($consumerName)) //phpcs:ignore + ->willReturn($isLocked); $this->shellBackgroundMock->expects($this->exactly($shellBackgroundExpects)) ->method('execute') @@ -174,80 +179,80 @@ public function runDataProvider() return [ [ 'maxMessages' => 20000, - 'isRun' => false, + 'isLocked' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=20000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=20000'], 'allowedConsumers' => [], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, ], [ 'maxMessages' => 10000, - 'isRun' => false, + 'isLocked' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=10000'], 'allowedConsumers' => [], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, ], [ 'maxMessages' => 10000, - 'isRun' => false, + 'isLocked' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=10000'], 'allowedConsumers' => ['someConsumer'], 'shellBackgroundExpects' => 0, 'isRunExpects' => 0, ], [ 'maxMessages' => 10000, - 'isRun' => true, + 'isLocked' => true, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=10000'], 'allowedConsumers' => ['someConsumer'], 'shellBackgroundExpects' => 0, 'isRunExpects' => 0, ], [ 'maxMessages' => 10000, - 'isRun' => true, + 'isLocked' => true, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=10000'], 'allowedConsumers' => [], 'shellBackgroundExpects' => 0, 'isRunExpects' => 1, ], [ 'maxMessages' => 10000, - 'isRun' => true, + 'isLocked' => true, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=10000'], 'allowedConsumers' => ['consumerName'], 'shellBackgroundExpects' => 0, 'isRunExpects' => 1, ], [ 'maxMessages' => 10000, - 'isRun' => false, + 'isLocked' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--single-thread', '--max-messages=10000'], 'allowedConsumers' => ['consumerName'], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, ], [ 'maxMessages' => 0, - 'isRun' => false, + 'isLocked' => false, 'php' => '/bin/php', 'command' => '/bin/php '. BP . '/bin/magento queue:consumers:start %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid'], + 'arguments' => ['consumerName', '--single-thread'], 'allowedConsumers' => ['consumerName'], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, diff --git a/app/code/Magento/Msrp/view/base/web/js/msrp.js b/app/code/Magento/Msrp/view/base/web/js/msrp.js index a0bd3ec132de..2789491137bc 100644 --- a/app/code/Magento/Msrp/view/base/web/js/msrp.js +++ b/app/code/Magento/Msrp/view/base/web/js/msrp.js @@ -6,7 +6,7 @@ define([ 'jquery', 'Magento_Catalog/js/price-utils', 'underscore', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/dropdown', 'mage/template' ], function ($, priceUtils, _) { diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js b/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js index 88f384758f71..537abb3aa207 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js index 3a6d73e30497..4906a2066cec 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/payment.js b/app/code/Magento/Multishipping/view/frontend/web/js/payment.js index da24b99597d4..e185123372e2 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/payment.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/payment.js @@ -7,7 +7,7 @@ define([ 'jquery', 'mage/template', 'Magento_Ui/js/modal/alert', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], function ($, mageTemplate, alert) { 'use strict'; diff --git a/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php b/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php index 502a19d298c4..aa3a2bcfe0f5 100644 --- a/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php +++ b/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php @@ -3,10 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Newsletter\Model\Queue; use Magento\Email\Model\AbstractTemplate; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Mail\EmailMessageInterfaceFactory; +use Magento\Framework\Mail\AddressConverter; +use Magento\Framework\Mail\MessageInterface; +use Magento\Framework\Mail\MessageInterfaceFactory; +use Magento\Framework\Mail\MimeMessageInterfaceFactory; +use Magento\Framework\Mail\MimePartInterfaceFactory; +use Magento\Framework\Mail\Template\FactoryInterface; +use Magento\Framework\Mail\Template\SenderResolverInterface; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TransportInterfaceFactory; +use Magento\Framework\ObjectManagerInterface; +/** + * Class TransportBuilder + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class TransportBuilder extends \Magento\Framework\Mail\Template\TransportBuilder { /** @@ -16,6 +35,194 @@ class TransportBuilder extends \Magento\Framework\Mail\Template\TransportBuilder */ protected $templateData = []; + /** + * Param that used for storing all message data until it will be used + * + * @var array + */ + private $messageData = []; + + /** + * @var EmailMessageInterfaceFactory + */ + private $emailMessageInterfaceFactory; + + /** + * @var MimeMessageInterfaceFactory + */ + private $mimeMessageInterfaceFactory; + + /** + * @var MimePartInterfaceFactory + */ + private $mimePartInterfaceFactory; + + /** + * @var AddressConverter|null + */ + private $addressConverter; + + /** + * TransportBuilder constructor + * + * @param FactoryInterface $templateFactory + * @param MessageInterface $message + * @param SenderResolverInterface $senderResolver + * @param ObjectManagerInterface $objectManager + * @param TransportInterfaceFactory $mailTransportFactory + * @param MessageInterfaceFactory|null $messageFactory + * @param EmailMessageInterfaceFactory|null $emailMessageInterfaceFactory + * @param MimeMessageInterfaceFactory|null $mimeMessageInterfaceFactory + * @param MimePartInterfaceFactory|null $mimePartInterfaceFactory + * @param AddressConverter|null $addressConverter + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + FactoryInterface $templateFactory, + MessageInterface $message, + SenderResolverInterface $senderResolver, + ObjectManagerInterface $objectManager, + TransportInterfaceFactory $mailTransportFactory, + MessageInterfaceFactory $messageFactory = null, + EmailMessageInterfaceFactory $emailMessageInterfaceFactory = null, + MimeMessageInterfaceFactory $mimeMessageInterfaceFactory = null, + MimePartInterfaceFactory $mimePartInterfaceFactory = null, + AddressConverter $addressConverter = null + ) { + parent::__construct( + $templateFactory, + $message, + $senderResolver, + $objectManager, + $mailTransportFactory, + $messageFactory, + $emailMessageInterfaceFactory, + $mimeMessageInterfaceFactory, + $mimePartInterfaceFactory, + $addressConverter + ); + $this->emailMessageInterfaceFactory = $emailMessageInterfaceFactory ?: $this->objectManager + ->get(EmailMessageInterfaceFactory::class); + $this->mimeMessageInterfaceFactory = $mimeMessageInterfaceFactory ?: $this->objectManager + ->get(MimeMessageInterfaceFactory::class); + $this->mimePartInterfaceFactory = $mimePartInterfaceFactory ?: $this->objectManager + ->get(MimePartInterfaceFactory::class); + $this->addressConverter = $addressConverter ?: $this->objectManager + ->get(AddressConverter::class); + } + + /** + * Add cc address + * + * @param array|string $address + * @param string $name + * + * @return \Magento\Framework\Mail\Template\TransportBuilder + * @throws MailException + */ + public function addCc($address, $name = '') + { + $this->addAddressByType('cc', $address, $name); + + return $this; + } + + /** + * Add to address + * + * @param array|string $address + * @param string $name + * + * @return $this + * @throws MailException + */ + public function addTo($address, $name = '') + { + $this->addAddressByType('to', $address, $name); + + return $this; + } + + /** + * Add bcc address + * + * @param array|string $address + * + * @return $this + * @throws MailException + */ + public function addBcc($address) + { + $this->addAddressByType('bcc', $address); + + return $this; + } + + /** + * Set Reply-To Header + * + * @param string $email + * @param string|null $name + * + * @return $this + * @throws MailException + */ + public function setReplyTo($email, $name = null) + { + + $this->addAddressByType('replyTo', $email, $name); + + return $this; + } + + /** + * Set mail from address + * + * @param string|array $from + * + * @return $this + * @throws MailException + * @see setFromByScope() + * + * @deprecated This function sets the from address but does not provide + * a way of setting the correct from addresses based on the scope. + */ + public function setFrom($from) + { + return $this->setFromByScope($from); + } + + /** + * Set mail from address by scopeId + * + * @param string|array $from + * @param string|int $scopeId + * + * @return $this + * @throws MailException + */ + public function setFromByScope($from, $scopeId = null) + { + $result = $this->_senderResolver->resolve($from, $scopeId); + $this->addAddressByType('from', $result['email'], $result['name']); + + return $this; + } + + /** + * @inheritDoc + */ + protected function reset() + { + $this->messageData = []; + $this->templateIdentifier = null; + $this->templateVars = null; + $this->templateOptions = null; + + return $this; + } + /** * Set template data * @@ -25,11 +232,15 @@ class TransportBuilder extends \Magento\Framework\Mail\Template\TransportBuilder public function setTemplateData($data) { $this->templateData = $data; + return $this; } /** + * Sets up template filter + * * @param AbstractTemplate $template + * * @return void */ protected function setTemplateFilter(AbstractTemplate $template) @@ -44,16 +255,44 @@ protected function setTemplateFilter(AbstractTemplate $template) */ protected function prepareMessage() { - /** @var AbstractTemplate $template */ + /** @var AbstractTemplate|TemplateInterface $template */ $template = $this->getTemplate()->setData($this->templateData); $this->setTemplateFilter($template); + $content = $template->getProcessedTemplate($this->templateVars); + $this->messageData['subject'] = $template->getSubject(); - $this->message->setBodyHtml( - $template->getProcessedTemplate($this->templateVars) - )->setSubject( - $template->getSubject() + $mimePart = $this->mimePartInterfaceFactory->create( + ['content' => $content] + ); + $this->messageData['body'] = $this->mimeMessageInterfaceFactory->create( + ['parts' => [$mimePart]] ); + $this->message = $this->emailMessageInterfaceFactory->create($this->messageData); + return $this; } + + /** + * Handles possible incoming types of email (string or array) + * + * @param string $addressType + * @param string|array $email + * @param string|null $name + * + * @return void + * @throws MailException + */ + private function addAddressByType(string $addressType, $email, ?string $name = null): void + { + if (is_array($email)) { + $this->messageData[$addressType] = array_merge( + $this->messageData[$addressType], + $this->addressConverter->convertMany($email) + ); + + return; + } + $this->messageData[$addressType][] = $this->addressConverter->convert($email, $name); + } } diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml index 3525a976cd1c..4c8f641f78ae 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml @@ -18,7 +18,7 @@ </section> <section name="NewsletterWYSIWYGSection"> <element name="TextArea" type="text" selector="#text" /> - <element name="TinyMCE4" type="text" selector=".mce-branding-powered-by" /> + <element name="TinyMCE4" type="text" selector=".mce-branding" /> <element name="ShowHideBtn" type="button" selector="#toggletext" timeout="60"/> <element name="InsertWidgetBtn" type="button" selector=".action-add-widget"/> <element name="InsertWidgetIcon" type="button" selector="div[aria-label='Insert Widget']"/> diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php index e8b141a24c9e..8f5626b42ff3 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php @@ -1,78 +1,112 @@ -<?php +<?php /** @noinspection PhpDeprecationInspection */ +/** @noinspection PhpUndefinedClassInspection */ /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Newsletter\Test\Unit\Model\Queue; +use Magento\Email\Model\Template; +use Magento\Email\Model\Template\Filter; use Magento\Framework\App\TemplateTypesInterface; +use Magento\Framework\Mail\EmailMessageInterface; +use Magento\Framework\Mail\EmailMessageInterfaceFactory; +use Magento\Framework\Mail\Message; use Magento\Framework\Mail\MessageInterface; +use Magento\Framework\Mail\MessageInterfaceFactory; +use Magento\Framework\Mail\MimePartInterface; +use Magento\Framework\Mail\MimePartInterfaceFactory; +use Magento\Framework\Mail\Template\FactoryInterface; +use Magento\Framework\Mail\Template\SenderResolverInterface; +use Magento\Framework\Mail\TransportInterfaceFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Newsletter\Model\Queue\TransportBuilder; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; -class TransportBuilderTest extends \PHPUnit\Framework\TestCase +/** + * Class TransportBuilderTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class TransportBuilderTest extends TestCase { /** * @var string */ - protected $builderClassName = \Magento\Newsletter\Model\Queue\TransportBuilder::class; + protected $builderClassName = TransportBuilder::class; /** - * @var \Magento\Newsletter\Model\Queue\TransportBuilder + * @var TransportBuilder */ protected $builder; /** - * @var \Magento\Framework\Mail\Template\FactoryInterface | \PHPUnit_Framework_MockObject_MockObject + * @var FactoryInterface|PHPUnit_Framework_MockObject_MockObject */ protected $templateFactoryMock; /** - * @var \Magento\Framework\Mail\Message | \PHPUnit_Framework_MockObject_MockObject + * @var Message|PHPUnit_Framework_MockObject_MockObject */ protected $messageMock; /** - * @var \Magento\Framework\ObjectManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var ObjectManagerInterface|PHPUnit_Framework_MockObject_MockObject */ protected $objectManagerMock; /** - * @var \Magento\Framework\Mail\Template\SenderResolverInterface | \PHPUnit_Framework_MockObject_MockObject + * @var SenderResolverInterface|PHPUnit_Framework_MockObject_MockObject */ protected $senderResolverMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var PHPUnit_Framework_MockObject_MockObject */ protected $mailTransportFactoryMock; /** - * @var \Magento\Framework\Mail\MessageInterfaceFactory | \PHPUnit_Framework_MockObject_MockObject + * @var MessageInterfaceFactory|PHPUnit_Framework_MockObject_MockObject */ private $messageFactoryMock; + /** + * @var MockObject + */ + private $emailMessageInterfaceFactoryMock; + + /** + * @var MockObject + */ + private $mimePartFactoryMock; + /** * @return void */ - public function setUp() + public function setUp(): void { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->templateFactoryMock = $this->createMock(\Magento\Framework\Mail\Template\FactoryInterface::class); - $this->messageMock = $this->getMockBuilder(\Magento\Framework\Mail\MessageInterface::class) + $objectManagerHelper = new ObjectManager($this); + $this->templateFactoryMock = $this->createMock(FactoryInterface::class); + $this->messageMock = $this->getMockBuilder(MessageInterface::class) ->disableOriginalConstructor() ->setMethods(['setBodyHtml', 'setSubject']) ->getMockForAbstractClass(); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->senderResolverMock = $this->createMock(\Magento\Framework\Mail\Template\SenderResolverInterface::class); - $this->mailTransportFactoryMock = $this->getMockBuilder( - \Magento\Framework\Mail\TransportInterfaceFactory::class - )->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->messageFactoryMock = $this->getMockBuilder(\Magento\Framework\Mail\MessageInterfaceFactory::class) + + $this->emailMessageInterfaceFactoryMock = $this->createMock(EmailMessageInterfaceFactory::class); + $this->mimePartFactoryMock = $this->createMock(MimePartInterfaceFactory::class); + + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $this->senderResolverMock = $this->createMock(SenderResolverInterface::class); + $this->mailTransportFactoryMock = $this->getMockBuilder(TransportInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) - ->getMockForAbstractClass(); - $this->messageFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($this->messageMock); + ->getMock(); + $this->builder = $objectManagerHelper->getObject( $this->builderClassName, [ @@ -81,7 +115,9 @@ public function setUp() 'objectManager' => $this->objectManagerMock, 'senderResolver' => $this->senderResolverMock, 'mailTransportFactory' => $this->mailTransportFactoryMock, - 'messageFactory' => $this->messageFactoryMock + 'messageFactory' => $this->messageFactoryMock, + 'emailMessageInterfaceFactory' => $this->emailMessageInterfaceFactoryMock, + 'mimePartInterfaceFactory' => $this->mimePartFactoryMock, ] ); } @@ -91,12 +127,13 @@ public function setUp() * @param string $bodyText * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Magento\Framework\Exception\LocalizedException */ public function testGetTransport( $templateType = TemplateTypesInterface::TYPE_HTML, $bodyText = '<h1>Html message</h1>' - ) { - $filter = $this->createMock(\Magento\Email\Model\Template\Filter::class); + ): void { + $filter = $this->createMock(Filter::class); $data = [ 'template_subject' => 'Email Subject', 'template_text' => $bodyText, @@ -106,39 +143,39 @@ public function testGetTransport( ]; $vars = ['reason' => 'Reason', 'customer' => 'Customer']; $options = ['area' => 'frontend', 'store' => 1]; - $template = $this->createMock(\Magento\Email\Model\Template::class); - $template->expects($this->once())->method('setVars')->with($this->equalTo($vars))->will($this->returnSelf()); - $template->expects( - $this->once() - )->method( - 'setOptions' - )->with( - $this->equalTo($options) - )->will( - $this->returnSelf() - ); - $template->expects($this->once())->method('getSubject')->will($this->returnValue('Email Subject')); - $template->expects($this->once())->method('setData')->with($this->equalTo($data))->will($this->returnSelf()); - $template->expects($this->once()) - ->method('getProcessedTemplate') - ->with($vars) - ->willReturn($bodyText); - $template->expects($this->once()) - ->method('setTemplateFilter') - ->with($filter); - $this->templateFactoryMock->expects( - $this->once() - )->method( - 'get' - )->with( - $this->equalTo('identifier') - )->will( - $this->returnValue($template) - ); + /** @var MimePartInterface|MockObject $mimePartMock */ + $mimePartMock = $this->createMock(MimePartInterface::class); + + $this->mimePartFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($mimePartMock); + + /** @var EmailMessageInterface|MockObject $emailMessage */ + $emailMessage = $this->createMock(EmailMessageInterface::class); + + $this->emailMessageInterfaceFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($emailMessage); + + $template = $this->createMock(Template::class); + $template->expects($this->once())->method('setVars') + ->with($this->equalTo($vars))->will($this->returnSelf()); + $template->expects($this->once())->method('setOptions') + ->with($this->equalTo($options))->will($this->returnSelf()); + $template->expects($this->once())->method('getSubject') + ->willReturn('Email Subject'); + $template->expects($this->once())->method('setData') + ->with($this->equalTo($data))->will($this->returnSelf()); + $template->expects($this->once())->method('getProcessedTemplate') + ->with($vars)->willReturn($bodyText); + $template->expects($this->once())->method('setTemplateFilter') + ->with($filter); - $this->messageMock->expects($this->once())->method('setBodyHtml')->willReturnSelf(); - $this->messageMock->expects($this->once())->method('setSubject')->willReturnSelf(); + $this->templateFactoryMock->expects($this->once()) + ->method('get') + ->with($this->equalTo('identifier')) + ->willReturn($template); $this->builder->setTemplateIdentifier( 'identifier' diff --git a/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml b/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml index 01f041bcb68c..aedab33239f9 100644 --- a/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml +++ b/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml @@ -42,9 +42,11 @@ </field> <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Maximum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="model"></field> </group> @@ -76,9 +78,11 @@ </field> <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Maximum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="model"></field> </group> @@ -109,12 +113,15 @@ </field> <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Maximum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Sort Order</label> + <validate>validate-number</validate> </field> </group> <group id="cashondelivery" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -144,12 +151,15 @@ </field> <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Maximum Order Total</label> + <validate>validate-number validate-zero-or-greater</validate> </field> <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Sort Order</label> + <validate>validate-number</validate> </field> </group> </section> diff --git a/app/code/Magento/PageCache/Model/Config.php b/app/code/Magento/PageCache/Model/Config.php index 079e371d3436..10ae41be21d4 100644 --- a/app/code/Magento/PageCache/Model/Config.php +++ b/app/code/Magento/PageCache/Model/Config.php @@ -214,18 +214,18 @@ protected function _getReplacements() */ protected function _getAccessList() { - $result = ''; - $tpl = " \"%s\";"; + $tpl = ' "%s";'; $accessList = $this->_scopeConfig->getValue(self::XML_VARNISH_PAGECACHE_ACCESS_LIST); if (!empty($accessList)) { - $result = []; + $ipsList = []; $ips = explode(',', $accessList); foreach ($ips as $ip) { - $result[] = sprintf($tpl, trim($ip)); + $ipsList[] = sprintf($tpl, trim($ip)); } - return implode("\n", $result); + return implode("\n", $ipsList); } - return $result; + + return ''; } /** diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js index 2e8a4769be10..735fe9a6cb23 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js @@ -7,7 +7,7 @@ define([ 'jquery', 'domReady', 'consoleLogger', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/cookies' ], function ($, domReady, consoleLogger) { 'use strict'; diff --git a/app/code/Magento/Payment/view/frontend/web/js/cc-type.js b/app/code/Magento/Payment/view/frontend/web/js/cc-type.js index e970c77888d4..d6997e536eaa 100644 --- a/app/code/Magento/Payment/view/frontend/web/js/cc-type.js +++ b/app/code/Magento/Payment/view/frontend/web/js/cc-type.js @@ -6,7 +6,7 @@ /* @api */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Payment/view/frontend/web/js/transparent.js b/app/code/Magento/Payment/view/frontend/web/js/transparent.js index d7c2aa368e7e..d5c51d2d100b 100644 --- a/app/code/Magento/Payment/view/frontend/web/js/transparent.js +++ b/app/code/Magento/Payment/view/frontend/web/js/transparent.js @@ -8,7 +8,7 @@ define([ 'jquery', 'mage/template', 'Magento_Ui/js/modal/alert', - 'jquery/ui', + 'jquery-ui-modules/widget', 'Magento_Payment/js/model/credit-card-validation/validator', 'Magento_Checkout/js/model/full-screen-loader' ], function ($, mageTemplate, alert, ui, validator, fullScreenLoader) { diff --git a/app/code/Magento/Paypal/Model/Hostedpro.php b/app/code/Magento/Paypal/Model/Hostedpro.php index 24701e084d7d..848a6e0247f5 100644 --- a/app/code/Magento/Paypal/Model/Hostedpro.php +++ b/app/code/Magento/Paypal/Model/Hostedpro.php @@ -140,6 +140,7 @@ public function __construct( /** * Return available CC types for gateway based on merchant country. + * * We do not have to check the availability of card types. * * @return true @@ -150,8 +151,9 @@ public function getAllowedCcTypes() } /** - * Return merchant country code from config, - * use default country if it not specified in General settings + * Return merchant country code from config + * + * Use default country if it not specified in General settings * * @return string */ @@ -274,7 +276,9 @@ protected function buildBasicRequest() */ public function getReturnUrl($storeId = null) { - return $this->getUrl('paypal/hostedpro/return', $storeId); + $payment = $this->getInfoInstance(); + $urlRoute = $payment->getAdditionalInformation('return_url') ?? 'paypal/hostedpro/return'; + return $this->getUrl($urlRoute, $storeId); } /** @@ -296,7 +300,9 @@ public function getNotifyUrl($storeId = null) */ public function getCancelUrl($storeId = null) { - return $this->getUrl('paypal/hostedpro/cancel', $storeId); + $payment = $this->getInfoInstance(); + $urlRoute = $payment->getAdditionalInformation('cancel_url') ?? 'paypal/hostedpro/cancel'; + return $this->getUrl($urlRoute, $storeId); } /** diff --git a/app/code/Magento/Paypal/Model/Payflowlink.php b/app/code/Magento/Paypal/Model/Payflowlink.php index 47efd562570f..690e09ffd877 100644 --- a/app/code/Magento/Paypal/Model/Payflowlink.php +++ b/app/code/Magento/Paypal/Model/Payflowlink.php @@ -425,7 +425,6 @@ protected function _buildTokenRequest(\Magento\Sales\Model\Order\Payment $paymen $request->setCreatesecuretoken('Y') ->setSecuretokenid($this->mathRandom->getUniqueHash()) ->setTrxtype($this->_getTrxTokenType()); - $request = $this->updateRequestReturnUrls($request, $payment); $order = $payment->getOrder(); $request->setAmt(sprintf('%.2F', $order->getBaseTotalDue())) @@ -601,30 +600,4 @@ protected function _getCallbackUrl($actionName) return $websiteUrl . 'paypal/' . $this->_callbackController . '/' . $actionName; } - - /** - * Update the redirect urls on the request if they are set on the payment - * - * @param \Magento\Paypal\Model\Payflow\Request $request - * @param \Magento\Sales\Model\Order\Payment $payment - * @return \Magento\Paypal\Model\Payflow\Request - */ - private function updateRequestReturnUrls( - \Magento\Paypal\Model\Payflow\Request $request, - \Magento\Sales\Model\Order\Payment $payment - ): \Magento\Paypal\Model\Payflow\Request { - $paymentData = $payment->getAdditionalInformation(); - - if (!empty($paymentData['cancel_url'])) { - $request->setCancelurl($paymentData['cancel_url']); - } - if (!empty($paymentData['return_url'])) { - $request->setReturnurl($paymentData['return_url']); - } - if (!empty($paymentData['error_url'])) { - $request->setErrorurl($paymentData['error_url']); - } - - return $request; - } } diff --git a/app/code/Magento/Paypal/view/frontend/web/js/order-review.js b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js index 2155b52f4081..1deee1bd7659 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/order-review.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js @@ -6,7 +6,7 @@ define([ 'jquery', 'Magento_Ui/js/modal/alert', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate', 'mage/mage', 'mage/validation' diff --git a/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js index 62c9fed0a7dd..6704540ec060 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/paypal-checkout.js @@ -7,7 +7,7 @@ define([ 'jquery', 'Magento_Ui/js/modal/confirm', 'Magento_Customer/js/customer-data', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/mage' ], function ($, confirm, customerData) { 'use strict'; diff --git a/app/code/Magento/PaypalGraphQl/Model/HostedProAdditionalDataProvider.php b/app/code/Magento/PaypalGraphQl/Model/HostedProAdditionalDataProvider.php new file mode 100644 index 000000000000..3e41045f34b3 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/HostedProAdditionalDataProvider.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\PaypalGraphQl\Model\Resolver\Store\Url as UrlService; +use Magento\Paypal\Model\Config; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface; + +/** + * Get payment additional data for Paypal HostedPro payment + */ +class HostedProAdditionalDataProvider implements AdditionalDataProviderInterface +{ + /** + * @var UrlService + */ + private $urlService; + + /** + * @param UrlService $urlService + */ + public function __construct(UrlService $urlService) + { + $this->urlService = $urlService; + } + + /** + * Returns additional data + * + * @param array $data + * @return array + * @throws GraphQlInputException + */ + public function getData(array $data): array + { + $additionalData = $data[Config::METHOD_HOSTEDPRO] ?? []; + $this->validateUrlPaths($additionalData); + + return $additionalData; + } + + /** + * Validate redirect url paths + * + * @param array $data + * @throws GraphQlInputException + */ + private function validateUrlPaths(array $data): void + { + $urlKeys = ['cancel_url', 'return_url']; + + foreach ($urlKeys as $urlKey) { + if (isset($data[$urlKey])) { + if (!$this->urlService->isPath($data[$urlKey])) { + throw new GraphQlInputException(__('Invalid Url.')); + } + } + } + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php b/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php index 2bd10f474bfc..8ff7ce80ce49 100644 --- a/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php +++ b/app/code/Magento/PaypalGraphQl/Model/PayflowLinkAdditionalDataProvider.php @@ -8,30 +8,30 @@ namespace Magento\PaypalGraphQl\Model; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\Url\Validator as UrlValidator; use Magento\Paypal\Model\Config; use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface; +use Magento\PaypalGraphQl\Model\Resolver\Store\Url; /** - * Get payment additional data for Paypal Payflow Link payment + * Get payment additional data for Paypal Payflow Link payment method */ class PayflowLinkAdditionalDataProvider implements AdditionalDataProviderInterface { /** - * @var UrlValidator + * @var Url */ - private $urlValidator; + private $urlService; /** - * @param UrlValidator $urlValidator + * @param Url $urlService */ - public function __construct(UrlValidator $urlValidator) + public function __construct(Url $urlService) { - $this->urlValidator = $urlValidator; + $this->urlService = $urlService; } /** - * Returns additional data + * Return additional data from payflow_link paymentMethodInput * * @param array $data * @return array @@ -40,26 +40,25 @@ public function __construct(UrlValidator $urlValidator) public function getData(array $data): array { $additionalData = $data[Config::METHOD_PAYFLOWLINK] ?? []; - $this->validateUrls($additionalData); + $this->validatePathsInArray($additionalData); return $additionalData; } /** - * Validate redirect urls + * Validate paths in known keys of the additional data array * * @param array $data * @throws GraphQlInputException */ - private function validateUrls(array $data): void + private function validatePathsInArray(array $data): void { $urlKeys = ['cancel_url', 'return_url', 'error_url']; foreach ($urlKeys as $urlKey) { if (isset($data[$urlKey])) { - if (!$this->urlValidator->isValid($data[$urlKey])) { - $errorMessage = $this->urlValidator->getMessages()['invalidUrl'] ?? "Invalid Url."; - throw new GraphQlInputException(__($errorMessage)); + if (!$this->urlService->isPath($data[$urlKey])) { + throw new GraphQlInputException(__('Invalid Url.')); } } } diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/HostedPro/SetPaymentMethodOnCart.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/HostedPro/SetPaymentMethodOnCart.php new file mode 100644 index 000000000000..f3353afb7f5b --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/HostedPro/SetPaymentMethodOnCart.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Plugin\Cart\HostedPro; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Paypal\Model\Config; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderPool; +use Magento\Sales\Model\Order\Payment\Repository as PaymentRepository; + +/** + * Set additionalInformation on payment for Hosted Pro method + */ +class SetPaymentMethodOnCart +{ + /** + * @var PaymentRepository + */ + private $paymentRepository; + + /** + * @var AdditionalDataProviderPool + */ + private $additionalDataProviderPool; + + /** + * @param PaymentRepository $paymentRepository + * @param AdditionalDataProviderPool $additionalDataProviderPool + */ + public function __construct( + PaymentRepository $paymentRepository, + AdditionalDataProviderPool $additionalDataProviderPool + ) { + $this->paymentRepository = $paymentRepository; + $this->additionalDataProviderPool = $additionalDataProviderPool; + } + + /** + * Set redirect URL paths on payment additionalInformation + * + * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject + * @param mixed $result + * @param Quote $cart + * @param array $paymentData + * @return void + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject, + $result, + Quote $cart, + array $paymentData + ): void { + $paymentData = $this->additionalDataProviderPool->getData(Config::METHOD_HOSTEDPRO, $paymentData); + + if (!empty($paymentData)) { + $urlKeys = ['cancel_url', 'return_url']; + $payment = $cart->getPayment(); + foreach ($urlKeys as $urlKey) { + if (isset($paymentData[$urlKey])) { + $payment->setAdditionalInformation($urlKey, $paymentData[$urlKey]); + } + } + $payment->save(); + } + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Payflowlink.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Payflowlink.php new file mode 100644 index 000000000000..0c949341f952 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Payflowlink.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Plugin; + +use Magento\Framework\DataObject; +use Magento\Store\Model\StoreRepository; +use Magento\PaypalGraphQl\Model\Resolver\Store\Url; + +/** + * Plugin for PayflowLink payment model class + */ +class Payflowlink +{ + /** + * @var Url + */ + private $url; + + /** + * @var StoreRepository + */ + private $storeRepository; + + /** + * @param Url $url + * @param StoreRepository $storeRepository + */ + public function __construct(Url $url, StoreRepository $storeRepository) + { + $this->url = $url; + $this->storeRepository = $storeRepository; + } + + /** + * Update redirect URLs in request with values stored in payment additionalInformation + * + * Relative URL paths are converted to absolute URLs + * + * @param \Magento\Paypal\Model\Payflowlink $subject + * @param DataObject $request + * @return mixed + */ + public function afterBuildBasicRequest( + \Magento\Paypal\Model\Payflowlink $subject, + DataObject $request + ): DataObject { + $payment = $subject->getInfoInstance(); + $storeId = $subject->getData('store'); + $store = $this->storeRepository->getById($storeId); + + $cancelUrl = $payment->getAdditionalInformation('cancel_url'); + if ($cancelUrl) { + $request->setCancelurl($this->url->getUrlFromPath($cancelUrl, $store)); + } + + $returnUrl = $payment->getAdditionalInformation('return_url'); + if ($returnUrl) { + $request->setReturnurl($this->url->getUrlFromPath($returnUrl, $store)); + } + + $errorUrl = $payment->getAdditionalInformation('error_url'); + if ($errorUrl) { + $request->setErrorurl($this->url->getUrlFromPath($errorUrl, $store)); + } + + return $request; + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/HostedProUrl.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/HostedProUrl.php new file mode 100644 index 000000000000..136944d9091c --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/HostedProUrl.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactoryInterface; + +/** + * Resolver to pull HostedProUrl payment information + */ +class HostedProUrl implements ResolverInterface +{ + /** + * @var MaskedQuoteIdToQuoteIdInterface + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var CollectionFactoryInterface + */ + private $orderCollectionFactory; + + /** + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param CollectionFactoryInterface $orderCollectionFactory + */ + public function __construct( + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + CollectionFactoryInterface $orderCollectionFactory + ) { + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->orderCollectionFactory = $orderCollectionFactory; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customerId = $context->getUserId(); + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + $maskedCartId = $args['input']['cart_id'] ?? ''; + try { + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } + + $order = $this->getOrderFromQuoteId($cartId, $customerId, $storeId); + $payment = $order->getPayment(); + $paymentAdditionalInformation = $payment->getAdditionalInformation(); + + return [ + 'secure_form_url' => $paymentAdditionalInformation['secure_form_url'] + ]; + } + + /** + * Retrieve an order from its corresponding quote id + * + * @param int $quoteId + * @param int $customerId + * @param int $storeId + * @return Order + * @throws GraphQlNoSuchEntityException + */ + private function getOrderFromQuoteId(int $quoteId, int $customerId, int $storeId): Order + { + $orderCollection = $this->orderCollectionFactory->create($customerId ?? null); + $orderCollection->addFilter(Order::QUOTE_ID, $quoteId); + $orderCollection->addFilter(Order::STATUS, Order::STATE_PENDING_PAYMENT); + $orderCollection->addFilter(Order::STORE_ID, $storeId); + + if ($orderCollection->getTotalCount() !== 1) { + throw new GraphQlNoSuchEntityException(__('Could not find payment information for cart.')); + } + /** @var Order $order */ + $order = $orderCollection->getFirstItem(); + + return $order; + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php index 409145ca9a96..c0256e580e4f 100644 --- a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php +++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProToken.php @@ -11,10 +11,12 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Url\Validator as UrlValidator; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Framework\Exception\LocalizedException; +use Magento\PaypalGraphQl\Model\Resolver\Store\Url; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\Validation\ValidationException; /** * Resolver for generating PayflowProToken @@ -27,28 +29,28 @@ class PayflowProToken implements ResolverInterface private $getCartForUser; /** - * @var UrlValidator + * @var SecureToken */ - private $urlValidator; + private $secureTokenService; /** - * @var SecureToken + * @var Url */ - private $secureTokenService; + private $urlService; /** * @param GetCartForUser $getCartForUser - * @param UrlValidator $urlValidator * @param SecureToken $secureTokenService + * @param Url $urlService */ public function __construct( GetCartForUser $getCartForUser, - UrlValidator $urlValidator, - SecureToken $secureTokenService + SecureToken $secureTokenService, + Url $urlService ) { $this->getCartForUser = $getCartForUser; - $this->urlValidator = $urlValidator; $this->secureTokenService = $secureTokenService; + $this->urlService = $urlService; } /** @@ -65,11 +67,16 @@ public function resolve( $urls = $args['input']['urls'] ?? null ; $customerId = $context->getUserId(); - $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + $storeId = (int)$store->getId(); + $cart = $this->getCartForUser->execute($cartId, $customerId, $storeId); - if (!empty($args['input']['urls'])) { - $this->validateUrls($args['input']['urls']); + if (!empty($urls)) { + $urls = $this->validateAndConvertPathsToUrls($urls, $store); } try { @@ -88,20 +95,23 @@ public function resolve( } /** - * Validate redirect Urls + * Validate and convert to redirect urls from given paths * - * @param array $urls - * @return boolean + * @param string $paths + * @param StoreInterface $store + * @return array * @throws GraphQlInputException */ - private function validateUrls(array $urls): bool + private function validateAndConvertPathsToUrls(array $paths, StoreInterface $store): array { - foreach ($urls as $url) { - if (!$this->urlValidator->isValid($url)) { - $errorMessage = $this->urlValidator->getMessages()['invalidUrl'] ?? "Invalid Url."; - throw new GraphQlInputException(__($errorMessage)); + $urls = []; + foreach ($paths as $key => $path) { + try { + $urls[$key] = $this->urlService->getUrlFromPath($path, $store); + } catch (ValidationException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); } } - return true; + return $urls; } } diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php index 89db082e715c..25ad6c7d3d68 100644 --- a/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php +++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/PaypalExpressToken.php @@ -12,11 +12,13 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Url\Validator as UrlValidator; use Magento\Checkout\Helper\Data as CheckoutHelper; use Magento\PaypalGraphQl\Model\Provider\Checkout as CheckoutProvider; use Magento\PaypalGraphQl\Model\Provider\Config as ConfigProvider; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\PaypalGraphQl\Model\Resolver\Store\Url; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\Validation\ValidationException; /** * Resolver for generating Paypal token @@ -39,34 +41,34 @@ class PaypalExpressToken implements ResolverInterface private $checkoutProvider; /** - * @var UrlValidator + * @var CheckoutHelper */ - private $urlValidator; + private $checkoutHelper; /** - * @var CheckoutHelper + * @var Url */ - private $checkoutHelper; + private $urlService; /** * @param GetCartForUser $getCartForUser * @param CheckoutProvider $checkoutProvider * @param ConfigProvider $configProvider - * @param UrlValidator $urlValidator * @param CheckoutHelper $checkoutHelper + * @param Url $urlService */ public function __construct( GetCartForUser $getCartForUser, CheckoutProvider $checkoutProvider, ConfigProvider $configProvider, - UrlValidator $urlValidator, - CheckoutHelper $checkoutHelper + CheckoutHelper $checkoutHelper, + Url $urlService ) { $this->getCartForUser = $getCartForUser; $this->checkoutProvider = $checkoutProvider; $this->configProvider = $configProvider; - $this->urlValidator = $urlValidator; $this->checkoutHelper = $checkoutHelper; + $this->urlService = $urlService; } /** @@ -85,7 +87,10 @@ public function resolve( $usedExpressButton = isset($args['input']['express_button']) ? $args['input']['express_button'] : false; $customerId = $context->getUserId(); - $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + $storeId = (int)$store->getId(); $cart = $this->getCartForUser->execute($cartId, $customerId, $storeId); $config = $this->configProvider->getConfig($paymentCode); $checkout = $this->checkoutProvider->getCheckout($config, $cart); @@ -109,7 +114,7 @@ public function resolve( } if (!empty($args['input']['urls'])) { - $this->validateUrls($args['input']['urls']); + $args['input']['urls'] = $this->validateAndConvertPathsToUrls($args['input']['urls'], $store); } $checkout->prepareGiropayUrls( $args['input']['urls']['success_url'] ?? '', @@ -137,20 +142,23 @@ public function resolve( } /** - * Validate redirect Urls + * Validate and convert to redirect urls from given paths * - * @param array $urls - * @return boolean + * @param string $paths + * @param StoreInterface $store + * @return array * @throws GraphQlInputException */ - private function validateUrls(array $urls): bool + private function validateAndConvertPathsToUrls(array $paths, StoreInterface $store): array { - foreach ($urls as $url) { - if (!$this->urlValidator->isValid($url)) { - $errorMessage = $this->urlValidator->getMessages()['invalidUrl'] ?? "Invalid Url."; - throw new GraphQlInputException(__($errorMessage)); + $urls = []; + foreach ($paths as $key => $path) { + try { + $urls[$key] = $this->urlService->getUrlFromPath($path, $store); + } catch (ValidationException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); } } - return true; + return $urls; } } diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/Store/Url.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/Store/Url.php new file mode 100644 index 000000000000..bd506dd8c763 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/Store/Url.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Store; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\UrlInterface; +use Magento\Framework\Url\Validator as UrlValidator; +use Magento\Framework\Validation\ValidationException; + +/** + * Service class for scoped urls and paths + */ +class Url +{ + /** @var UrlValidator */ + private $urlValidator; + + /** @var UrlInterface */ + private $urlInterface; + + /** + * @param UrlValidator $urlValidator + * @param UrlInterface $urlInterface + */ + public function __construct( + UrlValidator $urlValidator, + UrlInterface $urlInterface + ) { + $this->urlValidator = $urlValidator; + $this->urlInterface = $urlInterface; + } + + /** + * Validate path + * + * @param string $path + * @return bool + */ + public function isPath(string $path): bool + { + $result = true; + + if (empty($path)) { + $result = false; + } elseif ($path[0] == '/' + || $this->containsProtocolDelimiter($path) + || $this->containsDirectoryTraversal($path) + || $this->containsParametersDelimiter($path) + || $this->startsWithPortDelimiter($path) + || $this->isUrl($path)) { + $result = false; + } + return $result; + } + + /** + * Validate url format + * + * @param array $url + * @return boolean + */ + private function isUrl(string $url): bool + { + $result = false; + if ($this->urlValidator->isValid($url)) { + $result = true; + } + return $result; + } + + /** + * Get full url with base path from a path + * + * @param string $path + * @param StoreInterface $store + * @return string + * @throws ValidationException + */ + public function getUrlFromPath(string $path, StoreInterface $store): string + { + //if it's a url then don't proceed with further validation + if (!$this->isPath($path)) { + throw new ValidationException(__('Invalid Url.')); + } + + $params = ["_secure" => $store->isCurrentlySecure()]; + $this->urlInterface->setScope($store); + + $baseUrl = $this->urlInterface->getBaseUrl($params); + $resultUrl = $this->urlInterface->getUrl($path, $params); + + // validate the resulting url + if (substr($resultUrl, 0, strlen($baseUrl)) != $baseUrl + || $path == $resultUrl + || $resultUrl == $baseUrl + || !$this->isUrl($resultUrl) + ) { + throw new ValidationException(__('Invalid Url.')); + } + + return $resultUrl; + } + + /** + * Validate if url contains protocol delimiter + * + * @param array $url + * @return boolean + */ + private function containsProtocolDelimiter(string $url): bool + { + if (strpos($url, '://') !== false) { + return true; + } + return false; + } + + /** + * Validate if url contains directory traversal + * + * @param array $url + * @return boolean + */ + private function containsDirectoryTraversal(string $url): bool + { + if (strpos($url, '..') !== false) { + return true; + } + return false; + } + + /** + * Validate if url contains parameters delimiter + * + * @param array $url + * @return boolean + */ + private function containsParametersDelimiter(string $url): bool + { + if (strpos($url, '?') !== false) { + return true; + } + return false; + } + + /** + * Validate if path starts with port delimiter + * + * @param array $path + * @return boolean + */ + private function startsWithPortDelimiter(string $path): bool + { + if (preg_match('/^\:[0-9]+/', $path)) { + return true; + } + return false; + } +} diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml index 8fa71bb75831..cd5d6e2062bb 100644 --- a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml @@ -9,6 +9,12 @@ <type name="Magento\QuoteGraphQl\Model\Resolver\SetPaymentMethodOnCart"> <plugin name="paypal_express_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Resolver\SetPaymentMethodOnCart"/> </type> + <type name="Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart"> + <plugin name="hosted_pro_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\HostedPro\SetPaymentMethodOnCart"/> + </type> + <type name="Magento\Paypal\Model\Payflowlink"> + <plugin name="payflow_link_update_redirect_urls" type="Magento\PaypalGraphQl\Model\Plugin\Payflowlink"/> + </type> <type name="Magento\PaypalGraphQl\Model\Plugin\Resolver\SetPaymentMethodOnCart"> <arguments> @@ -41,7 +47,9 @@ <arguments> <argument name="dataProviders" xsi:type="array"> <item name="payflow_link" xsi:type="object">Magento\PaypalGraphQl\Model\PayflowLinkAdditionalDataProvider</item> + <item name="payflow_advanced" xsi:type="object">Magento\PaypalGraphQl\Model\PayflowLinkAdditionalDataProvider</item> <item name="payflowpro" xsi:type="object">\Magento\PaypalGraphQl\Model\PayflowProAdditionalDataProvider</item> + <item name="hosted_pro" xsi:type="object">\Magento\PaypalGraphQl\Model\HostedProAdditionalDataProvider</item> </argument> </arguments> </type> diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml index 165727fe63e4..41154e5ae06e 100644 --- a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml +++ b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml @@ -9,4 +9,7 @@ <event name="payment_method_assign_data_payflow_link"> <observer name="payflow_link_data_assigner" instance="Magento\PaypalGraphQl\Observer\PayflowLinkSetAdditionalData"/> </event> + <event name="payment_method_assign_data_payflow_advanced"> + <observer name="payflow_advanced_data_assigner" instance="Magento\PaypalGraphQl\Observer\PayflowLinkSetAdditionalData"/> + </event> </config> diff --git a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls index 33cbb7366873..2bacfb18054d 100644 --- a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls +++ b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls @@ -2,102 +2,117 @@ # See COPYING.txt for license details. type Query { - getPayflowLinkToken(input: PayflowLinkTokenInput!): PayflowLinkToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowLinkToken") @doc(description: "Retrieve PayPal payment credentials for Payflow transaction.") + getPayflowLinkToken(input: PayflowLinkTokenInput!): PayflowLinkToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowLinkToken") @doc(description: "Retrieve payment credentials for transaction. Use this query for Payflow Link and Payments Advanced payment methods.") + getHostedProUrl(input: HostedProUrlInput!): HostedProUrl @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\HostedProUrl") @doc(description: "Retrieve secure PayPal url for Payments Pro Hosted Solution transaction.") } type Mutation { - createPaypalExpressToken(input: PaypalExpressTokenInput!): PaypalExpressToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PaypalExpressToken") @doc(description:"Initiates a PayPal checkout transaction and receives a token.") - createPayflowProToken(input: PayflowProTokenInput!): PayflowProToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProToken") @doc(description: "Initiates a PayFlowPro transaction and receives a token") - handlePayflowProResponse(input: PayflowProResponseInput!): PayflowProResponseOutput @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProResponse") @doc(description: "Handles PayFlowPro response and saves payment in Quote") + createPaypalExpressToken(input: PaypalExpressTokenInput!): PaypalExpressToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PaypalExpressToken") @doc(description:"Initiates an Express Checkout transaction and receives a token. Use this mutation for Express Checkout and Payments Standard payment methods.") + createPayflowProToken(input: PayflowProTokenInput!): PayflowProToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProToken") @doc(description: "Initiates a transaction and receives a token. Use this mutation for Payflow Pro and Payments Pro payment methods") + handlePayflowProResponse(input: PayflowProResponseInput!): PayflowProResponseOutput @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProResponse") @doc(description: "Handles payment response and saves payment in Quote. Use this mutations for Payflow Pro and Payments Pro payment methods.") } -input PaypalExpressTokenInput @doc(description:"Defines the attributes required to receive a payment token from PayPal") { +input PaypalExpressTokenInput @doc(description: "Defines the attributes required to receive a payment token for Express Checkout and Payments Standard payment methods.") { cart_id: String! @doc(description:"The unique ID that identifies the customer's cart") code: String! @doc(description:"Payment method code") - urls: PaypalExpressUrlsInput! @doc(description:"A set of URLs that PayPal uses to respond to a token request") + urls: PaypalExpressUrlsInput! @doc(description:"A set of relative URLs that PayPal uses in response to various actions during the authorization process") use_paypal_credit: Boolean @doc(description: "Indicates whether the buyer clicked the PayPal credit button. The default value is false") express_button: Boolean @doc(description: "Indicates whether the buyer selected the quick checkout button. The default value is false") } -type PaypalExpressToken @doc(description: "Contains the token returned by PayPal and a set of URLs that allow the buyer to authorize payment and adjust checkout details") { +type PaypalExpressToken @doc(description: "Contains the token returned by PayPal and a set of URLs that allow the buyer to authorize payment and adjust checkout details. Applies to Express Checkout and Payments Standard payment methods.") { token: String @doc(description:"The token returned by PayPal") paypal_urls: PaypalExpressUrlList @doc(description:"A set of URLs that allow the buyer to authorize payment and adjust checkout details") } -type PayflowLinkToken { +type PayflowLinkToken @doc(description:"Contains information used to generate PayPal iframe for transaction. Applies to Payflow Link and Payments Advanced payment methods.") { secure_token: String @doc(description:"Secure token generated by PayPal") secure_token_id: String @doc(description:"Secure token ID generated by PayPal") mode: PayflowLinkMode @doc(description:"Mode for Payflow transaction") paypal_url: String @doc(description:"PayPal URL used for requesting Payflow form") } +type HostedProUrl @doc(desription:"Contains secure URL used for Payments Pro Hosted Solution payment method.") { + secure_form_url: String @doc(description:"Secure Url generated by PayPal") +} + +input HostedProUrlInput @doc(description:"The required input to request the secure URL for Payments Pro Hosted Solution payment."){ + cart_id: String! @doc(description:"The unique ID that identifies the customer's cart") +} + input PaymentMethodInput { - paypal_express: PaypalExpressInput @doc(description:"Required input for PayPal Express Checkout payments") - payflow_express: PayflowExpressInput @doc(description:"Required input for PayPal Payflow Express Checkout payments") - payflow_link: PayflowLinkAdditionalDataInput @doc(description:"Required input for PayPal Payflow Link payments") - payflowpro: PayflowProInput @doc(description: "Required input type for Paypal payflow pro payments") + paypal_express: PaypalExpressInput @doc(description:"Required input for Express Checkout and Payments Standard payments") + payflow_express: PayflowExpressInput @doc(description:"Required input for Payflow Express Checkout payments") + payflow_link: PayflowLinkInput @doc(description:"Required input for PayPal Payflow Link and Payments Advanced payments") + payflowpro: PayflowProInput @doc(description: "Required input type for PayPal Payflow Pro and Payment Pro payments") + hosted_pro: HostedProInput @doc(description:"Required input for PayPal Hosted pro payments") +} + +input HostedProInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payments Pro Hosted Solution payment method.") { + return_url: String! @doc(description:"The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.") + cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.") } -input PaypalExpressInput @doc(description:"Required input for PayPal Express Checkout payments") { +input PaypalExpressInput @doc(description:"Required input for Express Checkout and Payments Standard payments") { payer_id: String! @doc(description:"The unique ID of the PayPal user") token: String! @doc(description:"The token returned by the createPaypalExpressToken mutation") } -input PayflowExpressInput @doc(description:"Required input for PayPal Payflow Express Checkout payments") { +input PayflowExpressInput @doc(description:"Required input for Payflow Express Checkout payments") { payer_id: String! @doc(description:"The unique ID of the PayPal user") token: String! @doc(description:"The token returned by the createPaypalExpressToken mutation") } -input PaypalExpressUrlsInput @doc(description:"A set of URLs that PayPal uses to respond to a token request") { - return_url: String! @doc(description:"The URL of the final review page on your website where the buyer confirms the order and payment") - cancel_url: String! @doc(description:"The URL of the original page on your website where the buyer initially chose PayPal as a payment type") - success_url: String @doc(description:"The URL to redirect upon success. Not applicable to most PayPal solutions") - pending_url: String @doc(description:"The URL to redirect for a pending transactions. Not applicable to most PayPal solutions") +input PaypalExpressUrlsInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Express Checkout and Payments Standard payment methods.") { + return_url: String! @doc(description:"The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.") + cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.") + success_url: String @doc(description:"The relative URL of the order confirmation page that PayPal will redirect to when the payment is successful and additional confirmation is not needed. Not applicable to most PayPal solutions. If the full URL to this page is https://www.example.com/paypal/action/success.html, the relative URL is paypal/action/success.html.") + pending_url: String @doc(description:"The relative URL of the page that PayPal will redirect to when the payment has been put on hold for additional review. This condition mostly applies to ACH transactions, and is not applicable to most PayPal solutions. If the full URL to this page is https://www.example.com/paypal/action/success_pending.html, the relative URL is paypal/action/success_pending.html. ") } -type PaypalExpressUrlList @doc(description:"A set of URLs that allow the buyer to authorize payment and adjust checkout details") { +type PaypalExpressUrlList @doc(description:"A set of URLs that allow the buyer to authorize payment and adjust checkout details for Express Checkout and Payments Standard transactions.") { start: String @doc(description:"The URL to the PayPal login page") edit: String @doc(description:"The PayPal URL that allows the buyer to edit their checkout details") } -input PayflowLinkAdditionalDataInput { - return_url: String! @doc(description:"The URL PayPal will redirect back to upon payment success") - cancel_url: String! @doc(description:"The URL PayPal will redirect back to upon payment cancellation") - error_url: String! @doc(description:"The URL PayPal will redirect back to upon payment error") +input PayflowLinkInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payflow Link and Payments Advanced payment methods.") { + return_url: String! @doc(description:"The relative URL of the order confirmation page that PayPal will redirect to when the payment is successful and additional confirmation is not needed. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.") + cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.") + error_url: String! @doc(description:"The relative URL of the transaction error page that PayPal will redirect to upon payment error. If the full URL to this page is https://www.example.com/paypal/action/error.html, the relative URL is paypal/action/error.html.") } -input PayflowLinkTokenInput { +input PayflowLinkTokenInput @doc(description:"Input required to fetch payment token information for Payflow Link and Payments Advanced payment methods.") { cart_id: String! @doc(description:"The unique ID that identifies the customer's cart") } -enum PayflowLinkMode @doc(description:"Mode for Payflow Link payment: TEST or LIVE") { +enum PayflowLinkMode @doc(description:"Mode for payment: TEST or LIVE. Applies to Payflow Link and Payments Advanced payment methods.") { TEST LIVE } -input PayflowProTokenInput { +input PayflowProTokenInput @doc(description:"Input required to fetch payment token information for Payflow Pro and Payments Pro payment methods."){ cart_id: String! @doc(description:"The unique ID that identifies the customer's cart") - urls: PayflowProUrlInput! @doc(description:"URL that PayPal uses for callback.") + urls: PayflowProUrlInput! @doc(description:"A set of relative URLs that PayPal uses for callback.") } -input PayflowProInput { +input PayflowProInput @doc(description:"Required input for Payflow Pro and Payments Pro payment methods.") { cc_details: CreditCardDetailsInput! @doc(description: "Required input for credit card related information") } -input CreditCardDetailsInput { +input CreditCardDetailsInput @doc(description:"Required fields for Payflow Pro and Payments Pro credit card payments") { cc_type: String! @doc(description: "Credit card type") cc_exp_year: Int! @doc(description: "Credit card expiration year") cc_exp_month: Int! @doc(description: "Credit card expiration month") cc_last_4: Int! @doc(description: "Last 4 digits of the credit card") } -input PayflowProUrlInput { - return_url: String! @doc(description:"The URL of the final review page on your website where the buyer confirms the order and payment") - cancel_url: String! @doc(description:"The URL of the original page on your website where the buyer initially chose PayPal as a payment type") - error_url: String! @doc(description:"The URL of the page on your website where any error in the transaction is handled") +input PayflowProUrlInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payflow Pro and Payment Pro payment methods.") { + return_url: String! @doc(description:"The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is https://www.example.com/paypal/action/return.html, the relative URL is paypal/action/return.html.") + cancel_url: String! @doc(description:"The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is https://www.example.com/paypal/action/cancel.html, the relative URL is paypal/action/cancel.html.") + error_url: String! @doc(description:"The relative URL of the transaction error page that PayPal will redirect to upon payment error. If the full URL to this page is https://www.example.com/paypal/action/error.html, the relative URL is paypal/action/error.html.") } -type PayflowProToken { +type PayflowProToken @doc(description: "Contains the secure information used to authorize transaction. Applies to Payflow Pro and Payments Pro payment methods.") { secure_token: String! secure_token_id: String! response_message: String! @@ -105,7 +120,7 @@ type PayflowProToken { result_code: Int! } -input PayflowProResponseInput { +input PayflowProResponseInput @doc(description:"Input required to complete payment. Applies to Payflow Pro and Payments Pro payment methods.") { cart_id: String! paypal_payload: String! } diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index 8c56aafa04be..1261a90b5843 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -6,6 +6,7 @@ namespace Magento\Persistent\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Model\Quote; /** * Observer of expired session @@ -68,6 +69,11 @@ class CheckExpirePersistentQuoteObserver implements ObserverInterface */ private $checkoutPagePath = 'checkout'; + /** + * @var Quote + */ + private $quote; + /** * @param \Magento\Persistent\Helper\Session $persistentSession * @param \Magento\Persistent\Helper\Data $persistentData @@ -100,6 +106,8 @@ public function __construct( * * @param \Magento\Framework\Event\Observer $observer * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -107,16 +115,21 @@ public function execute(\Magento\Framework\Event\Observer $observer) return; } + //clear persistent when persistent data is disabled + if ($this->isPersistentQuoteOutdated()) { + $this->_eventManager->dispatch('persistent_session_expired'); + $this->quoteManager->expire(); + $this->_checkoutSession->clearQuote(); + return; + } + if ($this->_persistentData->isEnabled() && !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && !$this->isRequestFromCheckoutPage($this->request) && // persistent session does not expire on onepage checkout page - ( - $this->_checkoutSession->getQuote()->getIsPersistent() || - $this->_checkoutSession->getQuote()->getCustomerIsGuest() - ) + $this->isNeedToExpireSession() ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); @@ -124,6 +137,50 @@ public function execute(\Magento\Framework\Event\Observer $observer) } } + /** + * Checks if current quote marked as persistent and Persistence Functionality is disabled. + * + * @return bool + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function isPersistentQuoteOutdated(): bool + { + if ((!$this->_persistentData->isEnabled() || !$this->_persistentData->isShoppingCartPersist()) + && !$this->_customerSession->isLoggedIn() + && $this->_checkoutSession->getQuoteId()) { + return (bool)$this->getQuote()->getIsPersistent(); + } + return false; + } + + /** + * Condition checker + * + * @return bool + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function isNeedToExpireSession(): bool + { + return $this->getQuote()->getIsPersistent() || $this->getQuote()->getCustomerIsGuest(); + } + + /** + * Getter for Quote with micro optimization + * + * @return Quote + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getQuote(): Quote + { + if ($this->quote === null) { + $this->quote = $this->_checkoutSession->getQuote(); + } + return $this->quote; + } + /** * Check current request is coming from onepage checkout page. * diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml index 2e57c8f8e1ee..b081f5c3eb0c 100644 --- a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml +++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml @@ -20,4 +20,4 @@ <remove keyForRemoval="verifyMessage"/> <dontSee selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" userInput="Welcome, {{customerFullName}}! Not you?" stepKey="dontSeeWelcomeMessageNotYou"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index b096dd2317a3..2c5b9dad48eb 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -116,7 +116,7 @@ public function testExecuteWhenPersistentIsNotEnabled() ->method('canProcess') ->with($this->observerMock) ->willReturn(true); - $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(false); + $this->persistentHelperMock->expects($this->exactly(2))->method('isEnabled')->willReturn(false); $this->eventManagerMock->expects($this->never())->method('dispatch'); $this->model->execute($this->observerMock); } @@ -144,8 +144,13 @@ public function testExecuteWhenPersistentIsEnabled( ->method('canProcess') ->with($this->observerMock) ->willReturn(true); - $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(true); - $this->sessionMock->expects($this->once())->method('isPersistent')->willReturn(false); + $this->persistentHelperMock->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $this->persistentHelperMock->expects($this->atLeastOnce()) + ->method('isShoppingCartPersist') + ->willReturn(true); + $this->sessionMock->expects($this->atLeastOnce())->method('isPersistent')->willReturn(false); $this->checkoutSessionMock ->method('getQuote') ->willReturn($this->quoteMock); diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index cd0f3b3d630a..acaf2afeb6c2 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'catalogGallery', 'loadPlayer' ], function ($) { diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js b/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js index 75a2c1d75da1..ede0d2019309 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js @@ -7,7 +7,10 @@ @version 0.0.1 @requires jQuery & jQuery UI */ -define(['jquery', 'jquery/ui'], function ($) { +define([ + 'jquery', + 'jquery-ui-modules/widget' +], function ($) { 'use strict'; var videoRegister = { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 6346acfcb9af..7053e365114d 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -11,7 +11,6 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -use Magento\Framework\Stdlib\ArrayManager; use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestBuilder; @@ -87,7 +86,11 @@ public function execute(Quote $cart, array $cartItemData): void */ private function extractSku(array $cartItemData): string { - if (!isset($cartItemData['data']['sku']) || empty($cartItemData['data']['sku'])) { + // Need to keep this for configurable product and backward compatibility. + if (!empty($cartItemData['parent_sku'])) { + return (string)$cartItemData['parent_sku']; + } + if (empty($cartItemData['data']['sku'])) { throw new GraphQlInputException(__('Missed "sku" in cart item data')); } return (string)$cartItemData['data']['sku']; diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php index f2c03d0dc22a..8864609bb884 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php @@ -1,13 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Reports\Controller\Adminhtml\Report\Product; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +/** + * Class \Magento\Reports\Controller\Adminhtml\Report\Product\Downloads + */ class Downloads extends \Magento\Reports\Controller\Adminhtml\Report\Product implements HttpGetActionInterface { /** @@ -15,7 +19,7 @@ class Downloads extends \Magento\Reports\Controller\Adminhtml\Report\Product imp * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Reports::report_products'; + const ADMIN_RESOURCE = 'Magento_Reports::downloads'; /** * Downloads action diff --git a/app/code/Magento/Reports/view/frontend/web/js/recently-viewed.js b/app/code/Magento/Reports/view/frontend/web/js/recently-viewed.js index 26df126a6323..7c914a326969 100644 --- a/app/code/Magento/Reports/view/frontend/web/js/recently-viewed.js +++ b/app/code/Magento/Reports/view/frontend/web/js/recently-viewed.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Review/view/frontend/web/js/validate-review.js b/app/code/Magento/Review/view/frontend/web/js/validate-review.js index e3f57eaf8cd3..795dd5958f06 100644 --- a/app/code/Magento/Review/view/frontend/web/js/validate-review.js +++ b/app/code/Magento/Review/view/frontend/web/js/validate-review.js @@ -5,7 +5,6 @@ define([ 'jquery', - 'jquery/ui', 'jquery/validate', 'mage/translate' ], function ($) { diff --git a/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php b/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php index b3d761b378d9..83006fe65061 100644 --- a/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php +++ b/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php @@ -9,6 +9,7 @@ /** * Abstract Rule entity resource collection model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -83,11 +84,21 @@ public function addWebsiteFilter($websiteId) if ($website instanceof \Magento\Store\Model\Website) { $websiteIds[$index] = $website->getId(); } + $websiteIds[$index] = (int) $websiteIds[$index]; } + + $websiteSelect = $this->getConnection()->select(); + $websiteSelect->from( + $this->getTable($entityInfo['associations_table']), + [$entityInfo['rule_id_field']] + )->distinct( + true + )->where( + $this->getConnection()->quoteInto($entityInfo['entity_id_field'] . ' IN (?)', $websiteIds) + ); $this->getSelect()->join( - ['website' => $this->getTable($entityInfo['associations_table'])], - $this->getConnection()->quoteInto('website.' . $entityInfo['entity_id_field'] . ' IN (?)', $websiteIds) - . ' AND main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'], + ['website' => $websiteSelect], + 'main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'], [] ); } @@ -127,11 +138,11 @@ public function addIsActiveFilter($isActive = 1) } /** - * Retrieve correspondent entity information (associations table name, columns names) - * of rule's associated entity by specified entity type + * Retrieve correspondent entity information of rule's associated entity by specified entity type * - * @param string $entityType + * (associations table name, columns names) * + * @param string $entityType * @throws \Magento\Framework\Exception\LocalizedException * @return array */ diff --git a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php b/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php deleted file mode 100644 index c4e7a591212c..000000000000 --- a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php +++ /dev/null @@ -1,200 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Rule\Test\Unit\Model\ResourceModel\Rule\Collection; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -class AbstractCollectionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $abstractCollection; - - /** - * @var ObjectManagerHelper - */ - protected $objectManagerHelper; - - /** - * @var \Magento\Framework\Data\Collection\EntityFactoryInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_entityFactoryMock; - - /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_loggerMock; - - /** - * @var \Magento\Framework\Data\Collection\Db\FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_fetchStrategyMock; - - /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_managerMock; - - /** - * @var \Magento\Framework\Model\ResourceModel\Db\AbstractDb|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_db; - - /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $connectionMock; - - /** - * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject - */ - private $selectMock; - - protected function setUp() - { - $this->_entityFactoryMock = $this->createMock(\Magento\Framework\Data\Collection\EntityFactoryInterface::class); - $this->_loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->_fetchStrategyMock = $this->createMock( - \Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class - ); - $this->_managerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->_db = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, - [], - '', - false, - false, - true, - ['__sleep', '__wakeup', 'getTable'] - ); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->abstractCollection = $this->getMockForAbstractClass( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - [ - 'entityFactory' => $this->_entityFactoryMock, - 'logger' => $this->_loggerMock, - 'fetchStrategy' => $this->_fetchStrategyMock, - 'eventManager' => $this->_managerMock, - null, - $this->_db - ], - '', - false, - false, - true, - ['__sleep', '__wakeup', '_getAssociatedEntityInfo', 'getConnection', 'getSelect', 'getTable'] - ); - } - - /** - * @return array - */ - public function addWebsitesToResultDataProvider() - { - return [ - [null, true], - [true, true], - [false, false] - ]; - } - - /** - * @dataProvider addWebsitesToResultDataProvider - */ - public function testAddWebsitesToResult($flag, $expectedResult) - { - $this->abstractCollection->addWebsitesToResult($flag); - $this->assertEquals($expectedResult, $this->abstractCollection->getFlag('add_websites_to_result')); - } - - protected function _prepareAddFilterStubs() - { - $entityInfo = []; - $entityInfo['entity_id_field'] = 'entity_id'; - $entityInfo['rule_id_field'] = 'rule_id'; - $entityInfo['associations_table'] = 'assoc_table'; - - $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $select = $this->createMock(\Magento\Framework\DB\Select::class); - $collectionSelect = $this->createMock(\Magento\Framework\DB\Select::class); - - $connection->expects($this->any()) - ->method('select') - ->will($this->returnValue($select)); - - $select->expects($this->any()) - ->method('from') - ->will($this->returnSelf()); - - $select->expects($this->any()) - ->method('where') - ->will($this->returnSelf()); - - $this->abstractCollection->expects($this->any()) - ->method('getConnection') - ->will($this->returnValue($connection)); - - $this->_db->expects($this->any()) - ->method('getTable') - ->will($this->returnArgument(0)); - - $this->abstractCollection->expects($this->any()) - ->method('getSelect') - ->will($this->returnValue($collectionSelect)); - - $this->abstractCollection->expects($this->any()) - ->method('_getAssociatedEntityInfo') - ->will($this->returnValue($entityInfo)); - } - - public function testAddWebsiteFilter() - { - $this->_prepareAddFilterStubs(); - $website = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getId', '__sleep', '__wakeup']); - - $website->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - - $this->assertInstanceOf( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - $this->abstractCollection->addWebsiteFilter($website) - ); - } - - public function testAddWebsiteFilterArray() - { - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->connectionMock->expects($this->atLeastOnce()) - ->method('quoteInto') - ->with($this->equalTo('website. IN (?)'), $this->equalTo(['2', '3'])) - ->willReturn(true); - - $this->abstractCollection->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock); - $this->abstractCollection->expects($this->atLeastOnce())->method('getConnection') - ->willReturn($this->connectionMock); - - $this->assertInstanceOf( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - $this->abstractCollection->addWebsiteFilter(['2', '3']) - ); - } - - public function testAddFieldToFilter() - { - $this->_prepareAddFilterStubs(); - $result = $this->abstractCollection->addFieldToFilter('website_ids', []); - $this->assertNotNull($result); - } -} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index 50d29c195968..1210391f70dd 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -111,4 +111,20 @@ public function getShippingLabel() } return $label; } + + /** + * Get update totals url. + * + * @return string + */ + public function getUpdateTotalsUrl(): string + { + return $this->getUrl( + 'sales/*/updateQty', + [ + 'order_id' => $this->getSource()->getOrderId(), + 'invoice_id' => $this->getRequest()->getParam('invoice_id', null), + ] + ); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php index 65163f9ed5d8..389c29bedf4c 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php @@ -56,7 +56,12 @@ protected function _prepareLayout() $this->addChild( 'update_button', \Magento\Backend\Block\Widget\Button::class, - ['label' => __('Update Qty\'s'), 'class' => 'update-button', 'onclick' => $onclick] + ['label' => __('Update Qty\'s'), 'class' => 'update-button secondary', 'onclick' => $onclick] + ); + $this->addChild( + 'update_totals_button', + \Magento\Backend\Block\Widget\Button::class, + ['label' => __('Update Totals'), 'class' => 'update-totals-button secondary', 'onclick' => $onclick] ); if ($this->getCreditmemo()->canRefund()) { @@ -176,6 +181,16 @@ public function getUpdateButtonHtml() return $this->getChildHtml('update_button'); } + /** + * Get update totals button html + * + * @return string + */ + public function getUpdateTotalsButtonHtml(): string + { + return $this->getChildHtml('update_totals_button'); + } + /** * Get update url * diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index ed9e38822245..1ae5d7479952 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -85,11 +85,9 @@ public function sendCopyTo() $copyTo = $this->identityContainer->getEmailCopyTo(); if (!empty($copyTo) && $this->identityContainer->getCopyMethod() == 'copy') { + $this->configureEmailTemplate(); foreach ($copyTo as $email) { - $this->configureEmailTemplate(); - $this->transportBuilder->addTo($email); - $transport = $this->transportBuilder->getTransport(); $transport->sendMessage(); } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml index 58c7752626c8..6eeee6064974 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml @@ -37,4 +37,25 @@ </arguments> <see selector="{{AdminCreditMemoItemsSection.skuColumn}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> </actionGroup> -</actionGroups> \ No newline at end of file + <actionGroup name="StartToCreateCreditMemoActionGroup"> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="navigateToOrderPage"/> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemo"/> + <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForPageTitle"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + </actionGroup> + <actionGroup name="SubmitCreditMemoActionGroup"> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="waitButtonEnabled"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemo"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You created the credit memo." stepKey="seeCreditMemoCreateSuccess"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}$grabOrderId" stepKey="seeViewOrderPageCreditMemo"/> + </actionGroup> + <actionGroup name="UpdateCreditMemoTotalsActionGroup"> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="waitUpdateTotalsButtonEnabled"/> + <click selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="clickUpdateTotals"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml index ee8cf05e3d7c..27612cc079b1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -22,5 +22,6 @@ <element name="creditMemoItem" type="text" selector="#sales_order_view_tabs_order_creditmemos"/> <element name="viewMemo" type="text" selector="div#sales_order_view_tabs_order_creditmemos_content a.action-menu-item"/> <element name="refundOffline" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-offline']"/> + <element name="updateTotals" type="button" selector=".update-totals-button" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml new file mode 100644 index 000000000000..8cd2b8ee60ed --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckingCreditMemoTotalsTest"> + <annotations> + <features value="CreditMemo"/> + <stories value="Create credit memo"/> + <title value="Checking Credit Memo Update Totals button"/> + <description value="Checking Credit Memo Update Totals button"/> + <severity value="MAJOR"/> + <testCaseId value="MC-18159"/> + <useCaseId value="MC-17003"/> + <group value="sales"/> + </annotations> + <before> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <!--Create customer--> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <!--Login to admin page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <!--Create invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <!--Submit invoice--> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Create Credit Memo--> + <actionGroup ref="StartToCreateCreditMemoActionGroup" stepKey="startToCreateCreditMemo"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + <fillField selector="{{AdminCreditMemoTotalSection.refundShipping}}" userInput="0" stepKey="setRefundShipping"/> + <actionGroup ref="UpdateCreditMemoTotalsActionGroup" stepKey="updateCreditMemoTotals"/> + <actionGroup ref="SubmitCreditMemoActionGroup" stepKey="submitCreditMemo"/> + + <!--Go to Credit Memo tab--> + <click selector="{{AdminOrderDetailsOrderViewSection.creditMemos}}" stepKey="clickCreditMemosTab"/> + <waitForPageLoad stepKey="waitForCreditMemosGridToLoad"/> + + <!--Check refunded total --> + <see selector="{{AdminOrderCreditMemosTabSection.gridRow('1')}}" userInput="$123" stepKey="seeCreditMemoInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index 31d3b281532d..31aefd8d2ca5 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -100,6 +100,7 @@ <span class="title"><?= $block->escapeHtml(__('Refund Totals')) ?></span> </div> <?= $block->getChildHtml('creditmemo_totals') ?> + <div class="totals-actions"><?= $block->getUpdateTotalsButtonHtml() ?></div> <div class="order-totals-actions"> <div class="field choice admin__field admin__field-option field-append-comments"> <input id="notify_customer" @@ -139,8 +140,8 @@ require(['jquery'], function(jQuery){ //<![CDATA[ var submitButtons = jQuery('.submit-button'); -var updateButtons = jQuery('.update-button'); -var fields = jQuery('.qty-input'); +var updateButtons = jQuery('.update-button,.update-totals-button'); +var fields = jQuery('.qty-input,.order-subtotal-table input[type="text"]'); function enableButtons(buttons) { buttons.removeClass('disabled').prop('disabled', false); diff --git a/app/code/Magento/Sales/view/frontend/web/js/gift-message.js b/app/code/Magento/Sales/view/frontend/web/js/gift-message.js index a5c898194c6d..48ba2dd89af8 100644 --- a/app/code/Magento/Sales/view/frontend/web/js/gift-message.js +++ b/app/code/Magento/Sales/view/frontend/web/js/gift-message.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Sales/view/frontend/web/js/orders-returns.js b/app/code/Magento/Sales/view/frontend/web/js/orders-returns.js index ee3bf3cda91d..fd54c7e16222 100644 --- a/app/code/Magento/Sales/view/frontend/web/js/orders-returns.js +++ b/app/code/Magento/Sales/view/frontend/web/js/orders-returns.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index 3f3e64738d46..64e6eceb1eba 100644 --- a/app/code/Magento/Search/view/frontend/web/js/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -11,7 +11,8 @@ define([ 'underscore', 'mage/template', 'matchMedia', - 'jquery/ui', + 'jquery-ui-modules/widget', + 'jquery-ui-modules/core', 'mage/translate' ], function ($, _, mageTemplate, mediaCheck) { 'use strict'; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php b/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php index 8aed785641ef..66fbce08a8b3 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php @@ -38,9 +38,12 @@ public function __construct( } /** + * Update version of private content on each store switch. + * * @param StoreInterface $fromStore store where we came from * @param StoreInterface $targetStore store where to go to * @param string $redirectUrl original url requested for redirect after switching + * * @return string redirect url * @throws CannotSwitchStoreException */ @@ -54,7 +57,7 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s ->setHttpOnly(false); $this->cookieManager->setPublicCookie( \Magento\Framework\App\PageCache\Version::COOKIE_NAME, - 'should_be_updated', + \uniqid('updated-', true), $publicCookieMetadata ); } catch (\Exception $e) { diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AdminEditPropertiesTabForSwatchProductAtributeActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AdminEditPropertiesTabForSwatchProductAtributeActionGroup.xml new file mode 100644 index 000000000000..05ab5a53468a --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AdminEditPropertiesTabForSwatchProductAtributeActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <!--You are on ProductAttributePage--> + <!--Add new swatch options and fill fields for product attribute --> + <actionGroup name="AdminAddSwatchOptionAndFillFieldsActionGroup"> + <arguments> + <argument name="swatchOption" defaultValue="visualSwatchOption1"/> + </arguments> + <click selector="{{AdminNewAttributePanel.addVisualSwatchOption}}" stepKey="clickAddSwatch"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.lastVisualSwatchOptionAdminValue}}" stepKey="waitForOption1Row"/> + <fillField selector="{{AdminNewAttributePanel.lastVisualSwatchOptionAdminValue}}" userInput="{{swatchOption.admin_label}}" stepKey="fillAdminLabel"/> + <fillField selector="{{AdminNewAttributePanel.lastVisualSwatchOptionDefaultStoreValue}}" userInput="{{swatchOption.default_label}}" stepKey="fillDefaultStoreLabel"/> + </actionGroup> + + <!--You are on ProductAttributePage--> + <!--Select value for option "Update Product Preview Image"--> + <actionGroup name="AdminUpdateProductPreviewImageActionGroup"> + <arguments> + <argument name="value" type="string" defaultValue="Yes"/> + </arguments> + <selectOption selector="{{AttributePropertiesSection.UpdateProductPreviewImage}}" userInput="{{value}}" stepKey="setUpdateProductPreviewImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml new file mode 100644 index 000000000000..a67f9d8999b5 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Click a swatch option on product page--> + <actionGroup name="StorefrontSelectSwatchOptionOnProductPage"> + <arguments> + <argument name="optionName" type="string"/> + </arguments> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(optionName)}}" stepKey="clickSwatchOption"/> + </actionGroup> + + <!--Click a swatch option on product page and check active image--> + <actionGroup name="StorefrontSelectSwatchOptionOnProductPageAndCheckImage" extends="StorefrontSelectSwatchOptionOnProductPage"> + <arguments> + <argument name="fileName" type="string" defaultValue="magento-logo"/> + </arguments> + <seeElement selector="{{StorefrontProductMediaSection.productImageActive(fileName)}}" stepKey="seeActiveImageDefault"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml index adefce918272..0c2dea5f4123 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -12,6 +12,8 @@ <element name="addVisualSwatchOption" type="button" selector="button#add_new_swatch_visual_option_button"/> <element name="addTextSwatchOption" type="button" selector="button#add_new_swatch_text_option_button"/> <element name="visualSwatchOptionAdminValue" type="input" selector="[data-role='swatch-visual-options-container'] input[name='optionvisual[value][option_{{row}}][0]']" parameterized="true"/> + <element name="lastVisualSwatchOptionAdminValue" type="input" selector="[data-role='swatch-visual-options-container'] tr:last-child [name*='][0]']"/> <element name="visualSwatchOptionDefaultStoreValue" type="input" selector="[data-role='swatch-visual-options-container'] input[name='optionvisual[value][option_{{row}}][1]']" parameterized="true"/> + <element name="lastVisualSwatchOptionDefaultStoreValue" type="input" selector="[data-role='swatch-visual-options-container'] tr:last-child [name*='][1]']"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml new file mode 100644 index 000000000000..1717b424e459 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSeeProductImagesMatchingProductSwatchesTest"> + <annotations> + <features value="Swatches"/> + <stories value="Swatches in product details page"/> + <title value="Customer can see product images matching product swatches"/> + <description value="Customer can see product images matching product swatches"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-3086"/> + <group value="swatches"/> + </annotations> + + <before> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="VisualSwatchProductAttribute"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductAttributeGridFilter"/> + <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllChildrenProducts"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Begin creating a new product attribute --> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + + <actionGroup ref="AdminFillProductAttributePropertiesActionGroup" stepKey="fillProductAttributeProperties"> + <argument name="attributeName" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + <argument name="attributeType" value="{{VisualSwatchProductAttribute.frontend_input}}"/> + </actionGroup> + + <!--Select value for option "Update Product Preview Image"--> + <actionGroup ref="AdminUpdateProductPreviewImageActionGroup" stepKey="setUpdateProductPreviewImage"/> + + <actionGroup ref="AdminAddSwatchOptionAndFillFieldsActionGroup" stepKey="addFirstSwatchOptionAndFillFields"/> + + <actionGroup ref="AdminAddSwatchOptionAndFillFieldsActionGroup" stepKey="addSecondSwatchOptionAndFillFields"> + <argument name="swatchOption" value="visualSwatchOption2"/> + </actionGroup> + + <!-- Set scope to global --> + <actionGroup ref="AdminSwitchScopeForProductAttributeActionGroup" stepKey="switchScopeForProductAttribute"/> + + <!-- Save the new attribute --> + <actionGroup ref="ClickSaveButtonActionGroup" stepKey="clickSaveAttribute"> + <argument name="message" value="You saved the product attribute."/> + </actionGroup> + + <!-- Edit configurable product --> + <actionGroup ref="goToProductPageViaID" stepKey="openProductEditPage"> + <argument name="productId" value="$$createSimpleProduct.id$$"/> + </actionGroup> + + <!-- Add images to configurable product --> + <actionGroup ref="addProductImage" stepKey="addFirstImageForProductConfigurable"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <actionGroup ref="addProductImage" stepKey="addSecondImageForProductConfigurable"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Create configurations based off the visual swatch we created earlier --> + <actionGroup ref="StartCreateConfigurationsForAttribute" stepKey="createConfigurations"> + <argument name="attributeCode" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Add images to configurable product attribute options--> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="TestImageAdobe"/> + <argument name="frontend_label" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + <argument name="label" value="{{visualSwatchOption1.default_label}}"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="ImageUpload3"/> + <argument name="frontend_label" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + <argument name="label" value="{{visualSwatchOption2.default_label}}"/> + </actionGroup> + + <actionGroup ref="GenerateAndSaveConfiguredProductAfterSettingOptions" stepKey="saveProductForm"/> + + <!-- Go to the category page --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + + <actionGroup ref="StorefrontAssertActiveProductImage" stepKey="StorefrontAssertActiveProductImage"/> + + <!--Click a swatch and expect to see the image from the swatch from the configurable product --> + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageAndCheckImage" stepKey="clickSwatchOption"> + <argument name="optionName" value="{{visualSwatchOption1.default_label}}"/> + <argument name="fileName" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertFotoramaImageAvailablity" stepKey="seeFirstImageBaseProductInSwatchOption"/> + <actionGroup ref="StorefrontAssertFotoramaImageAvailablity" stepKey="seeSecondImageBaseProductInSwatchOption"> + <argument name="fileName" value="{{TestImageNew.filename}}"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageAndCheckImage" stepKey="clickOnSwatchOption2"> + <argument name="optionName" value="{{visualSwatchOption2.default_label}}"/> + <argument name="fileName" value="{{ImageUpload3.filename}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertFotoramaImageAvailablity" stepKey="seeFirstImageBaseProductInSwatchOption2"/> + <actionGroup ref="StorefrontAssertFotoramaImageAvailablity" stepKey="seeSecondImageBaseProductInSwatchOption2"> + <argument name="fileName" value="{{TestImageNew.filename}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml index 93c19c8068f7..b22429eafc8e 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml @@ -75,7 +75,7 @@ </div> <script> - require(["jquery", "jquery/ui", "Magento_Swatches/js/swatch-renderer"], function ($) { + require(["jquery", "Magento_Swatches/js/swatch-renderer"], function ($) { $('.swatch-layered.<?= $block->escapeJs($swatchData['attribute_code']) ?>') .find('[option-type="1"], [option-type="2"], [option-type="0"], [option-type="3"]') .SwatchRendererTooltip(); diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 6e028ec53c12..d6302cff83bf 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -10,7 +10,7 @@ define([ 'mage/smart-keyboard-handler', 'mage/translate', 'priceUtils', - 'jquery/ui', + 'jquery-ui-modules/widget', 'jquery/jquery.parsequery', 'mage/validation/validation' ], function ($, _, mageTemplate, keyboardHandler, $t, priceUtils) { diff --git a/app/code/Magento/Theme/view/adminhtml/requirejs-config.js b/app/code/Magento/Theme/view/adminhtml/requirejs-config.js index 81269b87d583..67eac2f648e0 100644 --- a/app/code/Magento/Theme/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Theme/view/adminhtml/requirejs-config.js @@ -28,26 +28,60 @@ var config = { }, 'map': { '*': { - 'translateInline': 'mage/translate-inline', - 'form': 'mage/backend/form', - 'button': 'mage/backend/button', - 'accordion': 'mage/accordion', - 'actionLink': 'mage/backend/action-link', - 'validation': 'mage/backend/validation', - 'notification': 'mage/backend/notification', - 'loader': 'mage/loader_old', - 'loaderAjax': 'mage/loader_old', - 'floatingHeader': 'mage/backend/floating-header', - 'suggest': 'mage/backend/suggest', - 'mediabrowser': 'jquery/jstree/jquery.jstree', - 'tabs': 'mage/backend/tabs', - 'treeSuggest': 'mage/backend/tree-suggest', - 'calendar': 'mage/calendar', - 'dropdown': 'mage/dropdown_old', - 'collapsible': 'mage/collapsible', - 'menu': 'mage/backend/menu', - 'jstree': 'jquery/jstree/jquery.jstree', - 'details': 'jquery/jquery.details' + 'translateInline': 'mage/translate-inline', + 'form': 'mage/backend/form', + 'button': 'mage/backend/button', + 'accordion': 'mage/accordion', + 'actionLink': 'mage/backend/action-link', + 'validation': 'mage/backend/validation', + 'notification': 'mage/backend/notification', + 'loader': 'mage/loader_old', + 'loaderAjax': 'mage/loader_old', + 'floatingHeader': 'mage/backend/floating-header', + 'suggest': 'mage/backend/suggest', + 'mediabrowser': 'jquery/jstree/jquery.jstree', + 'tabs': 'mage/backend/tabs', + 'treeSuggest': 'mage/backend/tree-suggest', + 'calendar': 'mage/calendar', + 'dropdown': 'mage/dropdown_old', + 'collapsible': 'mage/collapsible', + 'menu': 'mage/backend/menu', + 'jstree': 'jquery/jstree/jquery.jstree', + 'details': 'jquery/jquery.details', + 'jquery-ui-modules/widget': 'jquery/ui', + 'jquery-ui-modules/core': 'jquery/ui', + 'jquery-ui-modules/accordion': 'jquery/ui', + 'jquery-ui-modules/autocomplete': 'jquery/ui', + 'jquery-ui-modules/button': 'jquery/ui', + 'jquery-ui-modules/datepicker': 'jquery/ui', + 'jquery-ui-modules/dialog': 'jquery/ui', + 'jquery-ui-modules/draggable': 'jquery/ui', + 'jquery-ui-modules/droppable': 'jquery/ui', + 'jquery-ui-modules/effect-blind': 'jquery/ui', + 'jquery-ui-modules/effect-bounce': 'jquery/ui', + 'jquery-ui-modules/effect-clip': 'jquery/ui', + 'jquery-ui-modules/effect-drop': 'jquery/ui', + 'jquery-ui-modules/effect-explode': 'jquery/ui', + 'jquery-ui-modules/effect-fade': 'jquery/ui', + 'jquery-ui-modules/effect-fold': 'jquery/ui', + 'jquery-ui-modules/effect-highlight': 'jquery/ui', + 'jquery-ui-modules/effect-scale': 'jquery/ui', + 'jquery-ui-modules/effect-pulsate': 'jquery/ui', + 'jquery-ui-modules/effect-shake': 'jquery/ui', + 'jquery-ui-modules/effect-slide': 'jquery/ui', + 'jquery-ui-modules/effect-transfer': 'jquery/ui', + 'jquery-ui-modules/effect': 'jquery/ui', + 'jquery-ui-modules/menu': 'jquery/ui', + 'jquery-ui-modules/mouse': 'jquery/ui', + 'jquery-ui-modules/position': 'jquery/ui', + 'jquery-ui-modules/progressbar': 'jquery/ui', + 'jquery-ui-modules/resizable': 'jquery/ui', + 'jquery-ui-modules/selectable': 'jquery/ui', + 'jquery-ui-modules/slider': 'jquery/ui', + 'jquery-ui-modules/sortable': 'jquery/ui', + 'jquery-ui-modules/spinner': 'jquery/ui', + 'jquery-ui-modules/tabs': 'jquery/ui', + 'jquery-ui-modules/tooltip': 'jquery/ui' } }, 'deps': [ diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index 52e9270952a9..2822d6db008a 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -43,7 +43,8 @@ var config = { 'text': 'mage/requirejs/text', 'domReady': 'requirejs/domReady', 'spectrum': 'jquery/spectrum/spectrum', - 'tinycolor': 'jquery/spectrum/tinycolor' + 'tinycolor': 'jquery/spectrum/tinycolor', + 'jquery-ui-modules': 'jquery/ui-modules' }, 'deps': [ 'jquery/jquery-migrate' diff --git a/app/code/Magento/Theme/view/frontend/requirejs-config.js b/app/code/Magento/Theme/view/frontend/requirejs-config.js index aacec30edf18..d1cf76b83ebb 100644 --- a/app/code/Magento/Theme/view/frontend/requirejs-config.js +++ b/app/code/Magento/Theme/view/frontend/requirejs-config.js @@ -29,12 +29,10 @@ var config = { 'validation': 'mage/validation/validation', 'welcome': 'Magento_Theme/js/view/welcome', 'breadcrumbs': 'Magento_Theme/js/view/breadcrumbs', - 'criticalCssLoader': 'Magento_Theme/js/view/critical-css-loader' + 'criticalCssLoader': 'Magento_Theme/js/view/critical-css-loader', + 'jquery/ui': 'jquery/compat' } }, - paths: { - 'jquery/ui': 'jquery/jquery-ui' - }, deps: [ 'jquery/jquery.mobile.custom', 'mage/common', diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml index e0a3169f4a8e..55798169cdf7 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml @@ -13,8 +13,8 @@ <script> require([ - "jquery", - "jquery/ui" + 'jquery', + 'jquery-ui-modules/datepicker' ], function($){ //<![CDATA[ diff --git a/app/code/Magento/Theme/view/frontend/web/js/row-builder.js b/app/code/Magento/Theme/view/frontend/web/js/row-builder.js index 7785ced2e4bd..18e4c20d4780 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/row-builder.js +++ b/app/code/Magento/Theme/view/frontend/web/js/row-builder.js @@ -11,7 +11,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, mageTemplate) { 'use strict'; diff --git a/app/code/Magento/Theme/view/frontend/web/js/truncate.js b/app/code/Magento/Theme/view/frontend/web/js/truncate.js index 9dd35379595e..139ca5edd0fe 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/truncate.js +++ b/app/code/Magento/Theme/view/frontend/web/js/truncate.js @@ -10,7 +10,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Theme/view/frontend/web/js/view/breadcrumbs.js b/app/code/Magento/Theme/view/frontend/web/js/view/breadcrumbs.js index e9c46354c014..19d8514a4ea8 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/view/breadcrumbs.js +++ b/app/code/Magento/Theme/view/frontend/web/js/view/breadcrumbs.js @@ -8,7 +8,7 @@ define([ 'mage/template', 'Magento_Theme/js/model/breadcrumb-list', 'text!Magento_Theme/templates/breadcrumbs.html', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, mageTemplate, breadcrumbList, tpl) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js index 708bf56e07a5..1dda3254f461 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js @@ -8,7 +8,7 @@ define([ 'jquery', 'underscore', '../template/renderer', - 'jquery/ui' + 'jquery-ui-modules/slider' ], function (ko, $, _, renderer) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/resizable.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/resizable.js index 130a351fac57..afc81ce61bb9 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/resizable.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/resizable.js @@ -9,7 +9,7 @@ define([ 'uiRegistry', 'underscore', '../template/renderer', - 'jquery/ui' + 'jquery-ui-modules/resizable' ], function (ko, $, async, registry, _, renderer) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 6f0dd01f3329..d2523ab43612 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -13,7 +13,6 @@ define([ 'moment', 'tinycolor', 'jquery/validate', - 'jquery/ui', 'mage/translate' ], function ($, _, utils, moment, tinycolor) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/modal/alert.js b/app/code/Magento/Ui/view/base/web/js/modal/alert.js index b63294e3d875..f36fe54a37a9 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/alert.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/alert.js @@ -9,7 +9,7 @@ define([ 'jquery', 'underscore', - 'jquery/ui', + 'jquery-ui-modules/widget', 'Magento_Ui/js/modal/confirm', 'mage/translate' ], function ($, _) { diff --git a/app/code/Magento/Ui/view/base/web/js/modal/confirm.js b/app/code/Magento/Ui/view/base/web/js/modal/confirm.js index eceab940d114..e661363d2eae 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/confirm.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/confirm.js @@ -10,7 +10,7 @@ define([ 'jquery', 'underscore', 'mage/translate', - 'jquery/ui', + 'jquery-ui-modules/widget', 'Magento_Ui/js/modal/modal' ], function ($, _, $t) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal.js b/app/code/Magento/Ui/view/base/web/js/modal/modal.js index c81274337f41..a8a76206bcd2 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal.js @@ -14,7 +14,7 @@ define([ 'text!ui/template/modal/modal-slide.html', 'text!ui/template/modal/modal-custom.html', 'Magento_Ui/js/lib/key-codes', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], function ($, _, template, popupTpl, slideTpl, customTpl, keyCodes) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/modal/prompt.js b/app/code/Magento/Ui/view/base/web/js/modal/prompt.js index 4a00d2b6c0ab..13b4d55ea278 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/prompt.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/prompt.js @@ -11,7 +11,7 @@ define([ 'underscore', 'mage/template', 'text!ui/template/modal/modal-prompt-content.html', - 'jquery/ui', + 'jquery-ui-modules/widget', 'Magento_Ui/js/modal/modal', 'mage/translate' ], function ($, _, template, promptContentTmpl) { diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 294cd7f0ab77..32bef54e732a 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml @@ -23,7 +23,7 @@ $allowedQty = $viewModel->setItem($item)->getMinMaxQty(); <label class="label" for="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" - name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>" <?= $product->isSaleable() ? '' : 'disabled="disabled"' ?>> + name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ $block->getAddToCartQty($item) * 1 ?>" <?= $product->isSaleable() ? '' : 'disabled="disabled"' ?>> </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index b38c5c2cda3a..033e2e43a3c2 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/search.js b/app/code/Magento/Wishlist/view/frontend/web/js/search.js index 64b0fda7ddae..278737d7bb2d 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/search.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/search.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/wishlist.js index 2bb804928a56..b92bd2b21544 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/wishlist.js @@ -10,7 +10,7 @@ define([ 'jquery', 'mage/template', 'Magento_Ui/js/modal/alert', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/validation/validation', 'mage/dataPost' ], function ($, mageTemplate, alert) { diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_total.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_total.less index f2369ad8f35e..6e663b15c89c 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_total.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_total.less @@ -22,6 +22,10 @@ } } +.totals-actions { + text-align: right; +} + .order-totals-actions { margin-top: @indent__s; .actions { diff --git a/app/etc/di.xml b/app/etc/di.xml index afe8f36b7de8..1a74fd9d7f84 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1771,4 +1771,10 @@ <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPut"/> <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillRead"/> <preference for="Magento\Framework\MessageQueue\CallbackInvokerInterface" type="Magento\Framework\MessageQueue\CallbackInvoker"/> + <preference for="Magento\Framework\Mail\EmailMessageInterface" + type="Magento\Framework\Mail\EmailMessage" /> + <preference for="Magento\Framework\Mail\MimeMessageInterface" + type="Magento\Framework\Mail\MimeMessage" /> + <preference for="Magento\Framework\Mail\MimePartInterface" + type="Magento\Framework\Mail\MimePart" /> </config> diff --git a/composer.json b/composer.json index 70ac25adcf2f..293cb06ef403 100644 --- a/composer.json +++ b/composer.json @@ -47,9 +47,9 @@ "phpseclib/mcrypt_compat": "1.0.8", "phpseclib/phpseclib": "2.0.*", "ramsey/uuid": "~3.8.0", - "symfony/console": "~4.1.0", - "symfony/event-dispatcher": "~4.1.0", - "symfony/process": "~4.1.0", + "symfony/console": "~4.1.0|~4.2.0|~4.3.0", + "symfony/event-dispatcher": "~4.1.0|~4.2.0|~4.3.0", + "symfony/process": "~4.1.0|~4.2.0|~4.3.0", "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", "webonyx/graphql-php": "^0.12.6", @@ -68,6 +68,7 @@ "zendframework/zend-json": "^2.6.1", "zendframework/zend-log": "^2.9.1", "zendframework/zend-mail": "^2.9.0", + "zendframework/zend-mime": "^2.5.0", "zendframework/zend-modulemanager": "^2.7", "zendframework/zend-mvc": "~2.7.0", "zendframework/zend-serializer": "^2.7.2", @@ -87,7 +88,7 @@ "friendsofphp/php-cs-fixer": "~2.14.0", "lusitanian/oauth": "~0.8.10", "magento/magento-coding-standard": "~3.0.0", - "magento/magento2-functional-testing-framework": "2.4.1", + "magento/magento2-functional-testing-framework": "2.4.3", "pdepend/pdepend": "2.5.2", "phpmd/phpmd": "@stable", "phpunit/phpunit": "~6.5.0", diff --git a/composer.lock b/composer.lock index 16fb3426b95d..591e51c62e83 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a6681a5855eda99e087af8ea30babf3c", + "content-hash": "f6c85e01a374b22a185f11a0c51e08fb", "packages": [ { "name": "braintree/braintree_php", @@ -201,25 +201,25 @@ }, { "name": "composer/ca-bundle", - "version": "1.1.4", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d" + "reference": "33810d865dd06a674130fceb729b2f279dc79e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/558f321c52faeb4828c03e7dc0cfe39a09e09a2d", - "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/33810d865dd06a674130fceb729b2f279dc79e8c", + "reference": "33810d865dd06a674130fceb729b2f279dc79e8c", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", "psr/log": "^1.0", "symfony/process": "^2.5 || ^3.0 || ^4.0" }, @@ -253,7 +253,7 @@ "ssl", "tls" ], - "time": "2019-01-28T09:30:10+00:00" + "time": "2019-07-31T08:13:16+00:00" }, { "name": "composer/composer", @@ -2132,29 +2132,36 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.1.12", + "version": "v4.3.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97" + "reference": "212b020949331b6531250584531363844b34a94e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51be1b61dfe04d64a260223f2b81475fa8066b97", - "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/212b020949331b6531250584531363844b34a94e", + "reference": "212b020949331b6531250584531363844b34a94e", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "^7.1.3", + "symfony/event-dispatcher-contracts": "^1.1" }, "conflict": { "symfony/dependency-injection": "<3.4" }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "1.1" + }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0", + "symfony/http-foundation": "^3.4|^4.0", + "symfony/service-contracts": "^1.1", "symfony/stopwatch": "~3.4|~4.0" }, "suggest": { @@ -2164,7 +2171,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -2191,7 +2198,65 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2019-01-16T18:35:49+00:00" + "time": "2019-06-27T06:42:14+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v1.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c61766f4440ca687de1084a5c00b08e167a2575c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c", + "reference": "c61766f4440ca687de1084a5c00b08e167a2575c", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/event-dispatcher": "", + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-06-20T06:46:26+00:00" }, { "name": "symfony/filesystem", @@ -2337,7 +2402,7 @@ }, { "name": "Gert de Pagter", - "email": "backendtea@gmail.com" + "email": "BackEndTea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -2411,16 +2476,16 @@ }, { "name": "symfony/process", - "version": "v4.1.12", + "version": "v4.3.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "72d838aafaa7c790330fe362b9cecec362c64629" + "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/72d838aafaa7c790330fe362b9cecec362c64629", - "reference": "72d838aafaa7c790330fe362b9cecec362c64629", + "url": "https://api.github.com/repos/symfony/process/zipball/856d35814cf287480465bb7a6c413bb7f5f5e69c", + "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c", "shasum": "" }, "require": { @@ -2429,7 +2494,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -2456,7 +2521,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2019-01-16T19:07:26+00:00" + "time": "2019-05-30T16:10:05+00:00" }, { "name": "tedivm/jshrink", @@ -6749,23 +6814,22 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.4.1", + "version": "2.4.3", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "9b5de03fe069d4a36c911112c30b824ff4e80c3a" + "reference": "9e9a20fd4c77833ef41ac07eb076a7f2434ce61c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/9b5de03fe069d4a36c911112c30b824ff4e80c3a", - "reference": "9b5de03fe069d4a36c911112c30b824ff4e80c3a", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/9e9a20fd4c77833ef41ac07eb076a7f2434ce61c", + "reference": "9e9a20fd4c77833ef41ac07eb076a7f2434ce61c", "shasum": "" }, "require": { "allure-framework/allure-codeception": "~1.3.0", "codeception/codeception": "~2.3.4 || ~2.4.0 ", "consolidation/robo": "^1.0.0", - "epfremme/swagger-php": "^2.0", "ext-curl": "*", "flow/jsonpath": ">0.2", "fzaninotto/faker": "^1.6", @@ -6789,6 +6853,9 @@ "squizlabs/php_codesniffer": "~3.2", "symfony/stopwatch": "~3.4.6" }, + "suggest": { + "epfremme/swagger-php": "^2.0" + }, "bin": [ "bin/mftf" ], @@ -6818,7 +6885,7 @@ "magento", "testing" ], - "time": "2019-06-10T17:57:40+00:00" + "time": "2019-08-02T14:26:18+00:00" }, { "name": "mikey179/vfsstream", @@ -7404,16 +7471,16 @@ }, { "name": "phpmd/phpmd", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "7425e155cf22cdd2b4dd3458a7da4cf6c0201562" + "reference": "a05a999c644f4bc9a204846017db7bb7809fbe4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/7425e155cf22cdd2b4dd3458a7da4cf6c0201562", - "reference": "7425e155cf22cdd2b4dd3458a7da4cf6c0201562", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/a05a999c644f4bc9a204846017db7bb7809fbe4c", + "reference": "a05a999c644f4bc9a204846017db7bb7809fbe4c", "shasum": "" }, "require": { @@ -7422,13 +7489,15 @@ "php": ">=5.3.9" }, "require-dev": { - "phpunit/phpunit": "^4.0", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.4", + "phpunit/phpunit": "^4.8.36 || ^5.7.27", "squizlabs/php_codesniffer": "^2.0" }, "bin": [ "src/bin/phpmd" ], - "type": "project", + "type": "library", "autoload": { "psr-0": { "PHPMD\\": "src/main/php" @@ -7445,20 +7514,20 @@ "email": "github@manuel-pichler.de", "homepage": "https://github.com/manuelpichler" }, - { - "name": "Other contributors", - "role": "Contributors", - "homepage": "https://github.com/phpmd/phpmd/graphs/contributors" - }, { "name": "Marc Würth", "role": "Project Maintainer", "email": "ravage@bluewin.ch", "homepage": "https://github.com/ravage84" + }, + { + "name": "Other contributors", + "role": "Contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors" } ], "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", - "homepage": "http://phpmd.org/", + "homepage": "https://phpmd.org/", "keywords": [ "mess detection", "mess detector", @@ -7466,7 +7535,7 @@ "phpmd", "pmd" ], - "time": "2019-07-05T23:07:02+00:00" + "time": "2019-07-30T21:13:32+00:00" }, { "name": "phpoption/phpoption", diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php index c839be26897b..e805bc940704 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php @@ -82,6 +82,78 @@ public function testMediaGalleryTypesAreCorrect() $this->assertEquals(['thumbnail', 'swatch_image'], $mediaGallery[1]['types']); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_with_multiple_images.php + */ + public function testMediaGallery() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + media_gallery { + label + url + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['media_gallery']); + $mediaGallery = $response['products']['items'][0]['media_gallery']; + $this->assertCount(2, $mediaGallery); + $this->assertEquals('Image Alt Text', $mediaGallery[0]['label']); + self::assertTrue($this->checkImageExists($mediaGallery[0]['url'])); + $this->assertEquals('Thumbnail Image', $mediaGallery[1]['label']); + self::assertTrue($this->checkImageExists($mediaGallery[1]['url'])); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_media_gallery_entries.php + */ + public function testMediaGalleryForProductVideos() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + media_gallery { + label + url + ... on ProductVideo { + video_content { + media_type + video_provider + video_url + video_title + video_description + video_metadata + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['media_gallery']); + $mediaGallery = $response['products']['items'][0]['media_gallery']; + $this->assertCount(1, $mediaGallery); + $this->assertEquals('Video Label', $mediaGallery[0]['label']); + self::assertTrue($this->checkImageExists($mediaGallery[0]['url'])); + $this->assertNotEmpty($mediaGallery[0]['video_content']); + $video_content = $mediaGallery[0]['video_content']; + $this->assertEquals('external-video', $video_content['media_type']); + $this->assertEquals('youtube', $video_content['video_provider']); + $this->assertEquals('http://www.youtube.com/v/tH_2PFNmWoga', $video_content['video_url']); + $this->assertEquals('Video title', $video_content['video_title']); + $this->assertEquals('Video description', $video_content['video_description']); + $this->assertEquals('Video Metadata', $video_content['video_metadata']); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php index f71ec9ad8521..36f5d7028fb0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -50,8 +50,6 @@ public function testAddProductIfQuantityIsNotAvailable() * @magentoApiDataFixture Magento/Catalog/_files/products.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php * @magentoConfigFixture default_store cataloginventory/item_options/max_sale_qty 5 - * @expectedException \Exception - * @expectedExceptionMessage The most you may purchase is 5. */ public function testAddMoreProductsThatAllowed() { @@ -59,6 +57,10 @@ public function testAddMoreProductsThatAllowed() $quantity = 7; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $this->expectExceptionMessageRegExp( + '/The most you may purchase is 5|The requested qty exceeds the maximum qty allowed in shopping cart/' + ); + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); $this->graphQlMutation($query); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php index 9166ad84be90..0e334999599b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -41,15 +41,15 @@ public function testAddConfigurableProductToCart() $quantity = 2; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); - $sku = $product['sku']; + $parentSku = $product['sku']; + $sku = 'simple_20'; $attributeId = (int) $product['configurable_options'][0]['attribute_id']; $optionId = $product['configurable_options'][0]['values'][1]['value_index']; $query = $this->getQuery( $maskedQuoteId, + $parentSku, $sku, - $attributeId, - $optionId, $quantity ); @@ -57,7 +57,7 @@ public function testAddConfigurableProductToCart() $cartItem = current($response['addConfigurableProductsToCart']['cart']['items']); self::assertEquals($quantity, $cartItem['quantity']); - self::assertEquals($sku, $cartItem['product']['sku']); + self::assertEquals($parentSku, $cartItem['product']['sku']); self::assertArrayHasKey('configurable_options', $cartItem); $option = current($cartItem['configurable_options']); @@ -67,32 +67,6 @@ public function testAddConfigurableProductToCart() self::assertArrayHasKey('value_label', $option); } - /** - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php - * @expectedException \Exception - * @expectedExceptionMessage You need to choose options for your item - */ - public function testAddProductWithInvalidOptions() - { - $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); - $product = current($searchResponse['products']['items']); - - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); - $sku = $product['sku']; - $attributeId = (int) $product['configurable_options'][0]['attribute_id']; - - $query = $this->getQuery( - $maskedQuoteId, - $sku, - $attributeId, - 9999, - 1 - ); - - $this->graphQlMutation($query); - } - /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php @@ -105,15 +79,13 @@ public function testAddProductIfQuantityIsNotAvailable() $product = current($searchResponse['products']['items']); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); - $sku = $product['sku']; - $attributeId = (int) $product['configurable_options'][0]['attribute_id']; - $optionId = $product['configurable_options'][0]['values'][1]['value_index']; + $parentSku = $product['sku']; + $sku = 'simple_20'; $query = $this->getQuery( $maskedQuoteId, + $parentSku, $sku, - $attributeId, - $optionId, 2000 ); @@ -122,13 +94,12 @@ public function testAddProductIfQuantityIsNotAvailable() /** * @param string $maskedQuoteId + * @param string $parentSku * @param string $sku - * @param int $optionId - * @param int $value * @param int $quantity * @return string */ - private function getQuery(string $maskedQuoteId, string $sku, int $optionId, int $value, int $quantity): string + private function getQuery(string $maskedQuoteId, string $parentSku, string $sku, int $quantity): string { return <<<QUERY mutation { @@ -136,11 +107,7 @@ private function getQuery(string $maskedQuoteId, string $sku, int $optionId, int input:{ cart_id:"{$maskedQuoteId}" cart_items:{ - configurable_attributes:[{ - id:{$optionId} - value:{$value} - } - ] + parent_sku: "{$parentSku}" data:{ sku:"{$sku}" quantity:{$quantity} diff --git a/dev/tests/functional/composer.json b/dev/tests/functional/composer.json index 8f5204c9a9af..233fd58c1dce 100644 --- a/dev/tests/functional/composer.json +++ b/dev/tests/functional/composer.json @@ -4,7 +4,7 @@ }, "require": { "php": "~7.1.3||~7.2.0||~7.3.0", - "magento/mtf": "dev-MC-17912", + "magento/mtf": "1.0.0-rc64", "allure-framework/allure-phpunit": "~1.2.0", "doctrine/annotations": "1.4.*", "phpunit/phpunit": "~6.5.0", @@ -19,11 +19,5 @@ "Magento\\": ["lib/Magento/", "testsuites/Magento", "generated/Magento/", "tests/app/Magento/"], "Test\\": "generated/Test/" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/magento-techdivision/mtf.git" - } - ] + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar.php index 7f9c6717c7ea..18153c9e1ff0 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar.php @@ -115,6 +115,9 @@ public function openMiniCart() if (!$this->browser->find($this->cartContent)->isVisible()) { $this->browser->find($this->cartLink)->click(); } + // Need this because there are a lot of JS processes that update shopping cart items + // and we cant control them all + sleep(5); } /** diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml index 06fe76c5efd0..72a76dacc329 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Cms\Test\TestCase\CreateCmsPageEntityMultipleStoreViewsTest" summary="Page cache for different CMS pages on multiple store views" ticketId="MAGETWO-52467"> <variation name="CreateCmsPageEntityMultipleStoreViewsTestVariation1"> - <data name="issue" xsi:type="string">MC-13801: Test "Page cache for different CMS pages on multiple store views" fails on Jenkins</data> <data name="cmsPages/0/is_active" xsi:type="string">Yes</data> <data name="cmsPages/0/title" xsi:type="string">NewCmsPage</data> <data name="cmsPages/0/store_id/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php index d98c5696c81f..28bb00757dac 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php @@ -27,6 +27,34 @@ class Totals extends \Magento\Sales\Test\Block\Adminhtml\Order\Totals */ protected $capture = '[name="invoice[capture_case]"]'; + /** + * Refund Shipping css selector. + * + * @var string + */ + private $refundShippingSelector = '#shipping_amount'; + + /** + * Adjustment Refund css selector. + * + * @var string + */ + private $adjustmentRefundSelector = '#adjustment_positive'; + + /** + * Adjustment Fee css selector. + * + * @var string + */ + private $adjustmentFeeSelector = '#adjustment_negative'; + + /** + * Update Totals button css selector. + * + * @var string + */ + private $updateTotalsSelector = '.update-totals-button'; + /** * Submit invoice. * @@ -57,4 +85,44 @@ public function setCaptureOption($option) { $this->_rootElement->find($this->capture, Locator::SELECTOR_CSS, 'select')->setValue($option); } + + /** + * Get Refund Shipping input element. + * + * @return \Magento\Mtf\Client\ElementInterface + */ + public function getRefundShippingElement() + { + return $this->_rootElement->find($this->refundShippingSelector, Locator::SELECTOR_CSS); + } + + /** + * Get Adjustment Refund input element. + * + * @return \Magento\Mtf\Client\ElementInterface + */ + public function getAdjustmentRefundElement() + { + return $this->_rootElement->find($this->adjustmentRefundSelector, Locator::SELECTOR_CSS); + } + + /** + * Get Adjustment Fee input element. + * + * @return \Magento\Mtf\Client\ElementInterface + */ + public function getAdjustmentFeeElement() + { + return $this->_rootElement->find($this->adjustmentFeeSelector, Locator::SELECTOR_CSS); + } + + /** + * Click update totals button. + * + * @return void + */ + public function clickUpdateTotals() + { + $this->_rootElement->find($this->updateTotalsSelector)->click(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php index 45298c5898c2..25b576a06dd3 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php @@ -95,8 +95,12 @@ public function run() if ($this->compare($items, $refundData)) { $this->orderCreditMemoNew->getFormBlock()->updateQty(); } - + $hasChangeTotals = $this->isTotalsDataChanged($refundData); $this->orderCreditMemoNew->getFormBlock()->fillFormData($refundData); + if ($hasChangeTotals) { + $this->orderCreditMemoNew->getTotalsBlock()->clickUpdateTotals(); + } + $this->orderCreditMemoNew->getFormBlock()->submit(); } @@ -116,4 +120,30 @@ protected function getCreditMemoIds() $this->salesOrderView->getOrderForm()->openTab('creditmemos'); return $this->salesOrderView->getOrderForm()->getTab('creditmemos')->getGridBlock()->getIds(); } + + /** + * Is totals data changed. + * + * @param array $data + * @return bool + */ + private function isTotalsDataChanged(array $data): bool + { + $compareData = [ + 'shipping_amount' => + $this->orderCreditMemoNew->getTotalsBlock()->getRefundShippingElement()->getValue(), + 'adjustment_positive' => + $this->orderCreditMemoNew->getTotalsBlock()->getAdjustmentRefundElement()->getValue(), + 'adjustment_negative' => + $this->orderCreditMemoNew->getTotalsBlock()->getAdjustmentFeeElement()->getValue(), + ]; + + foreach ($compareData as $fieldName => $fieldValue) { + if (isset($data['form_data'][$fieldName]) && $fieldValue != $data['form_data'][$fieldName]) { + return true; + } + } + + return false; + } } diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyComparedProducts.xml b/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyComparedProducts.xml index 9da96ab4c8fc..efcf1c43d5d0 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyComparedProducts.xml +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyComparedProducts.xml @@ -12,13 +12,13 @@ </page_size> <show_attributes> <selector> - .control [name="parameters[show_attributes][1]"] + .control [name^="parameters[show_attributes]["] </selector> <input>multiselect</input> </show_attributes> <show_buttons> <selector> - .control [name="parameters[show_buttons][2]"] + .control [name^="parameters[show_buttons]["] </selector> <input>multiselect</input> </show_buttons> diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyViewedProducts.xml b/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyViewedProducts.xml index 9da96ab4c8fc..efcf1c43d5d0 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyViewedProducts.xml +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/Widget/Instance/Edit/Tab/ParametersType/RecentlyViewedProducts.xml @@ -12,13 +12,13 @@ </page_size> <show_attributes> <selector> - .control [name="parameters[show_attributes][1]"] + .control [name^="parameters[show_attributes]["] </selector> <input>multiselect</input> </show_attributes> <show_buttons> <selector> - .control [name="parameters[show_buttons][2]"] + .control [name^="parameters[show_buttons]["] </selector> <input>multiselect</input> </show_buttons> diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/WidgetForm.xml b/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/WidgetForm.xml index 374b966861f6..f660de92a025 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/WidgetForm.xml +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/Block/Adminhtml/WidgetForm.xml @@ -32,13 +32,13 @@ <page_size /> <show_attributes> <selector> - .control [name="parameters[show_attributes][1]"] + .control [name^="parameters[show_attributes]["] </selector> <input>multiselect</input> </show_attributes> <show_buttons> <selector> - .control [name="parameters[show_buttons][2]"] + .control [name^="parameters[show_buttons]["] </selector> <input>multiselect</input> </show_buttons> diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php index 4f2b0fd67840..3c4fcdd1fc58 100644 --- a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php @@ -47,7 +47,7 @@ protected function setUp() $this->session = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() - ->setMethods(['getQuote', 'setLastOrderStatus', 'unsLastBillingAgreementReferenceId']) + ->setMethods(['getQuote', 'setLastOrderStatus', 'unsLastBillingAgreementReferenceId', 'getQuoteId']) ->getMock(); $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class) @@ -76,6 +76,9 @@ protected function tearDown() /** * Tests a negative scenario for a place order flow when exception throws after placing an order. * + * + * @magentoAppArea frontend + * @magentoAppIsolation enabled * @magentoDataFixture Magento/Braintree/Fixtures/paypal_quote.php */ public function testExecuteWithFailedOrder() @@ -85,6 +88,8 @@ public function testExecuteWithFailedOrder() $this->session->method('getQuote') ->willReturn($quote); + $this->session->method('getQuoteId') + ->willReturn($quote->getId()); $this->adapter->method('sale') ->willReturn($this->getTransactionStub('authorized')); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php index 04baef55863f..569cf2357675 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php @@ -47,8 +47,8 @@ protected function setUp() $this->imageUploader = $this->objectManager->create( \Magento\Catalog\Model\ImageUploader::class, [ - 'baseTmpPath' => $this->mediaDirectory->getRelativePath('tmp'), - 'basePath' => __DIR__, + 'baseTmpPath' => 'catalog/tmp/category', + 'basePath' => 'catalog/category', 'allowedExtensions' => ['jpg', 'jpeg', 'gif', 'png'], 'allowedMimeTypes' => ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'] ] @@ -79,6 +79,24 @@ public function testSaveFileToTmpDir(): void $this->assertTrue(is_file($this->mediaDirectory->getAbsolutePath($filePath))); } + /** + * Test that method rename files when move it with the same name into base directory. + * + * @return void + * @magentoDataFixture Magento/Catalog/_files/catalog_category_image.php + * @magentoDataFixture Magento/Catalog/_files/catalog_tmp_category_image.php + */ + public function testMoveFileFromTmp(): void + { + $expectedFilePath = $this->imageUploader->getBasePath() . DIRECTORY_SEPARATOR . 'magento_small_image_1.jpg'; + + $this->assertFileNotExists($this->mediaDirectory->getAbsolutePath($expectedFilePath)); + + $this->imageUploader->moveFileFromTmp('magento_small_image.jpg'); + + $this->assertFileExists($this->mediaDirectory->getAbsolutePath($expectedFilePath)); + } + /** * @expectedException \Magento\Framework\Exception\LocalizedException * @expectedExceptionMessage File validation failed. diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php index eb9e5664e0d9..677135092526 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php @@ -5,29 +5,44 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Model\Indexer\Product\Flat\State; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + /** * Full reindex Test */ class FullTest extends \Magento\TestFramework\Indexer\TestCase { /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\State + * @var State */ protected $_state; /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var Processor */ protected $_processor; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ protected function setUp() { - $this->_state = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Indexer\Product\Flat\State::class - ); - $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Indexer\Product\Flat\Processor::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->_state = $this->objectManager->get(State::class); + $this->_processor = $this->objectManager->get(Processor::class); } /** @@ -41,12 +56,8 @@ public function testReindexAll() $this->assertTrue($this->_state->isFlatEnabled()); $this->_processor->reindexAll(); - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\CategoryFactory::class - ); - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Block\Product\ListProduct::class - ); + $categoryFactory = $this->objectManager->get(CategoryFactory::class); + $listProduct = $this->objectManager->get(ListProduct::class); $category = $categoryFactory->create()->load(2); $layer = $listProduct->getLayer(); @@ -61,4 +72,50 @@ public function testReindexAll() $this->assertEquals('Short description', $product->getShortDescription()); } } + + /** + * @magentoAppArea frontend + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/product_simple_multistore.php + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 + * @magentoConfigFixture fixturestore_store catalog/frontend/flat_catalog_product 1 + */ + public function testReindexAllMultipleStores() + { + $this->assertTrue($this->_state->isFlatEnabled()); + $this->_processor->reindexAll(); + + /** @var ProductCollectionFactory $productCollectionFactory */ + $productCollectionFactory = $this->objectManager->create(ProductCollectionFactory::class); + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $store = $storeManager->getStore('fixturestore'); + $currentStore = $storeManager->getStore(); + + $expectedData = [ + $storeManager->getDefaultStoreView()->getId() => 'Simple Product One', + $store->getId() => 'StoreTitle', + ]; + + foreach ($expectedData as $storeId => $productName) { + $storeManager->setCurrentStore($storeId); + $productCollection = $productCollectionFactory->create(); + + $this->assertTrue( + $productCollection->isEnabledFlat(), + 'Flat should be enabled for product collection.' + ); + + $productCollection->addIdFilter(1)->addAttributeToSelect(ProductInterface::NAME); + + $this->assertEquals( + $productName, + $productCollection->getFirstItem()->getName(), + 'Wrong product name specified per store.' + ); + } + + $storeManager->setCurrentStore($currentStore); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php new file mode 100644 index 000000000000..0764d466898b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Filesystem\DirectoryList; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ +$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); +$fileName = 'magento_small_image.jpg'; +$filePath = 'catalog/category/' . $fileName; +$mediaDirectory->create('catalog/category'); + +copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($filePath)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image_rollback.php new file mode 100644 index 000000000000..977572bbb51f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image_rollback.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Filesystem\DirectoryList; + +/** @var \Magento\Framework\Filesystem\Directory\WriteInterface $mediaDirectory */ +$mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Filesystem::class +)->getDirectoryWrite( + DirectoryList::MEDIA +); + +$mediaDirectory->delete('catalog/category'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php new file mode 100644 index 000000000000..2562acdda2dc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Filesystem\DirectoryList; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ +$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); +$fileName = 'magento_small_image.jpg'; +$tmpFilePath = 'catalog/tmp/category/' . $fileName; +$mediaDirectory->create('catalog/tmp/category'); + +copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($tmpFilePath)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image_rollback.php new file mode 100644 index 000000000000..a85d349620f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image_rollback.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Filesystem\DirectoryList; + +/** @var \Magento\Framework\Filesystem\Directory\WriteInterface $mediaDirectory */ +$mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Filesystem::class +)->getDirectoryWrite( + DirectoryList::MEDIA +); + +$mediaDirectory->delete('catalog/tmp/category'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wholesale_tier_price.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wholesale_tier_price.php new file mode 100644 index 000000000000..5c5db4aee744 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wholesale_tier_price.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +/** @var $tpExtensionAttributes */ +$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var $productExtensionAttributes */ +$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); + +$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class)->get('admin'); +$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()); +$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( + ['website_ids' => $adminWebsite->getId()] +); + +$tierPrice = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => 2, + 'qty' => 1, + 'value' => 5, + ], + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$adminWebsite->getId()]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setTierPrices([$tierPrice]) + ->setDescription('Description with <b>html tag</b>') + ->setExtensionAttributes($productExtensionAttributesWebsiteIds) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wholesale_tier_price_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wholesale_tier_price_rollback.php new file mode 100644 index 000000000000..eecab1088d6f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wholesale_tier_price_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index b80f00b16f0b..566dfbadedd2 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -769,7 +769,7 @@ public function testConfirmationEmailWithSpecialCharacters(): void $message = $this->transportBuilderMock->getSentMessage(); $rawMessage = $message->getRawMessage(); - $this->assertContains('To: ' . $email, $rawMessage); + $this->assertContains('To: John Smith <' . $email . '>', $rawMessage); $content = $message->getBody()->getParts()[0]->getRawContent(); $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_with_different_customer_groups.php b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_with_different_customer_groups.php new file mode 100644 index 000000000000..01e451387c3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_with_different_customer_groups.php @@ -0,0 +1,31 @@ +<?php +/** + * Fixture for Customer List method. + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +use Magento\Customer\Model\Customer; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +require 'customer.php'; + +$customer = Bootstrap::getObjectManager()->create( + Customer::class +); +$customer->setWebsiteId(1) + ->setEmail('customer_two@example.com') + ->setPassword('password') + ->setGroupId(2) + ->setStoreId(Store::DEFAULT_STORE_ID) + ->setIsActive(1) + ->setFirstname('Firstname') + ->setLastname('Lastname') + ->setDefaultBilling(1) + ->setDefaultShipping(1); + +$customer->isObjectNew(true); +$customer->save(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_with_different_customer_groups_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_with_different_customer_groups_rollback.php new file mode 100644 index 000000000000..4054dea0f2a8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_with_different_customer_groups_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Fixture for Customer List method. + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +require 'customer_rollback.php'; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); +try { + $customer = $customerRepository->get('customer_two@example.com'); + $customerRepository->delete($customer); +} catch (NoSuchEntityException $e) { + /** Tests which are wrapped with MySQL transaction clear all data by transaction rollback. */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php new file mode 100644 index 000000000000..10a54b4e1b87 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Mail/EmailMessageTest.php @@ -0,0 +1,268 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Magento\Framework\Exception\MailException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class EmailMessageTest + */ +class EmailMessageTest extends TestCase +{ + private const ATTACHMENT_FILE_NAME = 'di.xml'; + private const XML_TYPE = 'text/xml'; + /** + * @var ObjectManagerInterface + */ + private $di; + + /** + * @var MimePartInterfaceFactory + */ + private $mimePartFactory; + + /** + * @var MimeMessageInterfaceFactory + */ + private $mimeMessageFactory; + + /** + * @var AddressConverter + */ + private $messageConverter; + + /** + * @var EmailMessageInterfaceFactory + */ + private $messageFactory; + + /** + * @var AddressFactory + */ + private $addressFactory; + + /** + * @var array + */ + private $addressList = [ + 'to' => [ + ['email' => 'to@adobe.com', 'name' => 'Addressee'] + ], + 'replyTo' => ['email' => 'replyTo@adobe.com', 'name' => 'Reply To Address'], + 'from' => 'from@adobe.com', + 'sender' => ['email' => 'sender@adobe.com', 'name' => 'Sender'], + 'cc' => [ + 'cc1@adobe.com' => 'CC 1 Address', + 'cc2@adobe.com' => 'CC 2 Address', + 'cc3@adobe.com' => 'CC 3 Address', + ], + 'bcc' => ['bcc1@adobe.com', 'bcc2@adobe.com'], + ]; + + /** + * @var string + */ + private $subject = 'Test subject'; + + /** + * @var string + */ + private $description = 'Test description'; + + /** + * + * @return void + */ + protected function setUp() + { + $this->di = Bootstrap::getObjectManager(); + $this->mimePartFactory = $this->di->get(MimePartInterfaceFactory::class); + $this->mimeMessageFactory = $this->di->get(MimeMessageInterfaceFactory::class); + $this->messageConverter = $this->di->get(AddressConverter::class); + $this->messageFactory = $this->di->get(EmailMessageInterfaceFactory::class); + } + + /** + * @return array + */ + public function getEmailMessageDataProvider(): array + { + return [ + [ + 'Content Test', + MimeInterface::TYPE_TEXT + ], [ + + '<h1>Html message</h1>', + MimeInterface::TYPE_HTML + ] + ]; + } + + /** + * Tests Email Message with Addresses + * + * @dataProvider getEmailMessageDataProvider + * @param $content + * @param $type + * @return void + * @throws MailException + */ + public function testEmailMessage($content, $type): void + { + $mimePart = $this->mimePartFactory->create( + [ + 'content' => $content, + 'description' => $this->description, + 'type' => $type + ] + ); + + $mimeMessage = $this->mimeMessageFactory->create( + [ + 'parts' => [$mimePart] + ] + ); + + $this->addressFactory = $this->di->get(AddressFactory::class); + /** @var Address $addressTo */ + $to = [ + $this->addressFactory->create( + [ + 'email' => $this->addressList['to'][0]['email'], + 'name' => $this->addressList['to'][0]['name'] + ] + ) + ]; + + $from = [$this->messageConverter->convert($this->addressList['from'])]; + $cc = $this->messageConverter->convertMany($this->addressList['cc']); + $replyTo = [ + $this->messageConverter->convert( + $this->addressList['replyTo']['email'], + $this->addressList['replyTo']['name'] + ) + ]; + $bcc = $this->messageConverter->convertMany($this->addressList['bcc']); + $sender = $this->messageConverter->convert( + $this->addressList['sender']['email'], + $this->addressList['sender']['name'] + ); + $data = [ + 'body' => $mimeMessage, + 'subject' => $this->subject, + 'from' => $from, + 'to' => $to, + 'cc' => $cc, + 'replyTo' => $replyTo, + 'bcc' => $bcc, + 'sender' => $sender + ]; + $message = $this->messageFactory->create($data); + + $this->assertContains($content, $message->toString()); + $this->assertContains('Content-Type: ' . $type, $message->toString()); + $senderString = 'Sender: ' . $sender->getName() . ' <' . $sender->getEmail() . '>'; + $this->assertContains($senderString, $message->toString()); + $this->assertContains('From: ' . $from[0]->getEmail(), $message->toString()); + $replyToString = 'Reply-To: ' . $replyTo[0]->getName() . ' <' . $replyTo[0]->getEmail() . '>'; + $this->assertContains($replyToString, $message->toString()); + $toString = 'To: ' . $to[0]->getName() . ' <' . $to[0]->getEmail() . '>'; + $this->assertContains($toString, $message->toString()); + $ccString = 'Cc: ' . $cc[0]->getName() . ' <' . $cc[0]->getEmail() . '>'; + $this->assertContains($ccString, $message->toString()); + $this->assertContains('Bcc: ' . $bcc[0]->getEmail(), $message->toString()); + $this->assertContains('Content-Description: ' . $this->description, $message->toString()); + $this->assertContains('Subject: ' . $this->subject, $message->toString()); + $this->assertContains($content, $message->toString()); + //tests address factory + $this->assertInstanceOf(Address::class, $message->getTo()[0]); + //tests address converter convert method + $this->assertInstanceOf(Address::class, $message->getFrom()[0]); + //tests address converter convertMany method + $this->assertInstanceOf(Address::class, $message->getCc()[0]); + } + + /** + * Test Email Message with Xml Attachment + * + * @return void + */ + public function testEmailMessageWithAttachment(): void + { + $mimePartMain = $this->mimePartFactory->create( + [ + 'content' => 'Test', + 'description' => $this->description, + 'type' => MimeInterface::TYPE_TEXT + ] + ); + $mimePartAttachment = $this->mimePartFactory->create( + [ + 'content' => $this->getXmlContent(), + 'disposition' => MimeInterface::DISPOSITION_ATTACHMENT, + 'fileName' => self::ATTACHMENT_FILE_NAME, + 'encoding' => MimeInterface::ENCODING_8BIT, + 'type' => self::XML_TYPE + ] + ); + + $mimeMessage = $this->mimeMessageFactory->create( + [ + 'parts' => [$mimePartMain, $mimePartAttachment] + ] + ); + + $this->addressFactory = $this->di->get(AddressFactory::class); + /** @var Address $addressTo */ + $addressTo = $this->addressFactory + ->create( + [ + 'email' => $this->addressList['to'][0]['email'], + 'name' => $this->addressList['to'][0]['name'] + ] + ); + + $data = [ + 'body' => $mimeMessage, + 'subject' => $this->subject, + 'to' => [$addressTo], + ]; + $message = $this->messageFactory->create($data); + + $this->assertContains($this->getXmlContent(), $message->toString()); + $this->assertContains('Content-Type: ' . self::XML_TYPE, $message->toString()); + $contentDisposition = 'Content-Disposition: ' . MimeInterface::DISPOSITION_ATTACHMENT + . '; filename="' . self::ATTACHMENT_FILE_NAME . '"'; + $this->assertContains($contentDisposition, $message->toString()); + } + + /** + * Provides xml content + * + * @return string + */ + private function getXmlContent(): string + { + return '<?xml version="1.0"?> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\Console\CommandList"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="furman_test_command_testbed" xsi:type="object">Furman\Test\Command\Testbed</item> + </argument> + </arguments> + </type> +</config> +'; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/test.min.js b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/test.min.js index c01e96760fee..dafa7cd26fcc 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/test.min.js +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/test.min.js @@ -1,4 +1,4 @@ -define(["jquery","matchMedia","jquery/ui","jquery/jquery.mobile.custom","mage/translate"],function($,mediaCheck){'use strict';$.widget('mage.menu',$.ui.menu,{options:{responsive:false,expanded:false,delay:300},_create:function(){var self=this;this._super();$(window).on('resize',function(){self.element.find('.submenu-reverse').removeClass('submenu-reverse');});},_init:function(){this._super();this.delay=this.options.delay;if(this.options.expanded===true){this.isExpanded();} +define(["jquery","matchMedia","jquery-ui-modules/menu","jquery/jquery.mobile.custom","mage/translate"],function($,mediaCheck){'use strict';$.widget('mage.menu',$.ui.menu,{options:{responsive:false,expanded:false,delay:300},_create:function(){var self=this;this._super();$(window).on('resize',function(){self.element.find('.submenu-reverse').removeClass('submenu-reverse');});},_init:function(){this._super();this.delay=this.options.delay;if(this.options.expanded===true){this.isExpanded();} if(this.options.responsive===true){mediaCheck({media:'(max-width: 640px)',entry:$.proxy(function(){this._toggleMobileMode();},this),exit:$.proxy(function(){this._toggleDesktopMode();},this)});} this._assignControls()._listen();},_assignControls:function(){this.controls={toggleBtn:$('[data-action="toggle-nav"]'),swipeArea:$('.nav-sections')};return this;},_listen:function(){var controls=this.controls;var toggle=this.toggle;this._on(controls.toggleBtn,{'click':toggle});this._on(controls.swipeArea,{'swipeleft':toggle});},toggle:function(){if($('html').hasClass('nav-open')){$('html').removeClass('nav-open');setTimeout(function(){$('html').removeClass('nav-before-open');},300);}else{$('html').addClass('nav-before-open');setTimeout(function(){$('html').addClass('nav-open');},42);}},isExpanded:function(){var subMenus=this.element.find(this.options.menus),expandedMenus=subMenus.find('ul');expandedMenus.addClass('expanded');},_activate:function(event){window.location.href=this.active.find('> a').attr('href');this.collapseAll(event);},_keydown:function(event){var match,prev,character,skip,regex,preventDefault=true;function escape(value){return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");} if(this.active.closest('ul').attr('aria-expanded')!='true'){switch(event.keyCode){case $.ui.keyCode.PAGE_UP:this.previousPage(event);break;case $.ui.keyCode.PAGE_DOWN:this.nextPage(event);break;case $.ui.keyCode.HOME:this._move("first","first",event);break;case $.ui.keyCode.END:this._move("last","last",event);break;case $.ui.keyCode.UP:this.previous(event);break;case $.ui.keyCode.DOWN:if(this.active&&!this.active.is(".ui-state-disabled")){this.expand(event);} diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/js/test.js b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/js/test.js index d2445d02161e..6c516ef4daa5 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/js/test.js +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/js/test.js @@ -5,7 +5,7 @@ define([ "jquery", "matchMedia", - "jquery/ui", + "jquery-ui-modules/menu", "jquery/jquery.mobile.custom", "mage/translate" ], function ($, mediaCheck) { diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index 2eda32e894d3..05866023a123 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -6,7 +6,7 @@ namespace Magento\MessageQueue\Model\Cron; use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; -use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; +use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\Config\File\ConfigFilePool; @@ -36,9 +36,9 @@ class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase private $consumerConfig; /** - * @var PidConsumerManager + * @var LockManagerInterface */ - private $pid; + private $lockManager; /** * @var FileReader @@ -83,7 +83,7 @@ protected function setUp() $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->shellMock = $this->getMockBuilder(ShellInterface::class) ->getMockForAbstractClass(); - $this->pid = $this->objectManager->get(PidConsumerManager::class); + $this->lockManager = $this->objectManager->get(LockManagerInterface::class); $this->consumerConfig = $this->objectManager->get(ConsumerConfigInterface::class); $this->reader = $this->objectManager->get(FileReader::class); $this->filesystem = $this->objectManager->get(Filesystem::class); @@ -97,16 +97,18 @@ protected function setUp() $this->shellMock->expects($this->any()) ->method('execute') - ->willReturnCallback(function ($command, $arguments) { - $command = vsprintf($command, $arguments); - $params = \Magento\TestFramework\Helper\Bootstrap::getInstance()->getAppInitParams(); - $params['MAGE_DIRS']['base']['path'] = BP; - $params = 'INTEGRATION_TEST_PARAMS="' . urldecode(http_build_query($params)) . '"'; - $command = str_replace('bin/magento', 'dev/tests/integration/bin/magento', $command); - $command = $params . ' ' . $command; - - return exec("{$command} >/dev/null &"); - }); + ->willReturnCallback( + function ($command, $arguments) { + $command = vsprintf($command, $arguments); + $params = \Magento\TestFramework\Helper\Bootstrap::getInstance()->getAppInitParams(); + $params['MAGE_DIRS']['base']['path'] = BP; + $params = 'INTEGRATION_TEST_PARAMS="' . urldecode(http_build_query($params)) . '"'; + $command = str_replace('bin/magento', 'dev/tests/integration/bin/magento', $command); + $command = $params . ' ' . $command; + + return exec("{$command} >/dev/null &"); //phpcs:ignore + } + ); } /** @@ -116,23 +118,20 @@ protected function setUp() */ public function testSpecificConsumerAndRerun() { - $specificConsumer = 'quoteItemCleaner'; - $pidFilePath = $this->getPidFileName($specificConsumer); + $specificConsumer = 'exportProcessor'; $config = $this->config; $config['cron_consumers_runner'] = ['consumers' => [$specificConsumer], 'max_messages' => 0]; - $this->writeConfig($config); - $this->reRunConsumersAndCheckPidFiles($specificConsumer); - $pid = $this->pid->getPid($pidFilePath); - $this->reRunConsumersAndCheckPidFiles($specificConsumer); - $this->assertSame($pid, $this->pid->getPid($pidFilePath)); + $this->reRunConsumersAndCheckLocks($specificConsumer); + $this->reRunConsumersAndCheckLocks($specificConsumer); + $this->assertTrue($this->lockManager->isLocked(md5($specificConsumer))); //phpcs:ignore } /** * @param string $specificConsumer * @return void */ - private function reRunConsumersAndCheckPidFiles($specificConsumer) + private function reRunConsumersAndCheckLocks($specificConsumer) { $this->consumersRunner->run(); @@ -140,12 +139,11 @@ private function reRunConsumersAndCheckPidFiles($specificConsumer) foreach ($this->consumerConfig->getConsumers() as $consumer) { $consumerName = $consumer->getName(); - $pidFileFullPath = $this->getPidFileFullPath($consumerName); if ($consumerName === $specificConsumer) { - $this->assertTrue(file_exists($pidFileFullPath)); + $this->assertTrue($this->lockManager->isLocked(md5($consumerName))); //phpcs:ignore } else { - $this->assertFalse(file_exists($pidFileFullPath)); + $this->assertFalse($this->lockManager->isLocked(md5($consumerName))); //phpcs:ignore } } } @@ -167,8 +165,7 @@ public function testCronJobDisabled() sleep(20); foreach ($this->consumerConfig->getConsumers() as $consumer) { - $pidFileFullPath = $this->getPidFileFullPath($consumer->getName()); - $this->assertFalse(file_exists($pidFileFullPath)); + $this->assertFalse($this->lockManager->isLocked(md5($consumer->getName()))); //phpcs:ignore } } @@ -191,33 +188,14 @@ private function writeConfig(array $config) $writer->saveConfig([ConfigFilePool::APP_ENV => $config]); } - /** - * @param string $consumerName - * @return string - */ - private function getPidFileFullPath($consumerName) - { - $directoryList = $this->objectManager->get(DirectoryList::class); - return $directoryList->getPath(DirectoryList::VAR_DIR) . '/' . $this->getPidFileName($consumerName); - } - /** * @inheritdoc */ protected function tearDown() { foreach ($this->consumerConfig->getConsumers() as $consumer) { - $consumerName = $consumer->getName(); - $pidFileFullPath = $this->getPidFileFullPath($consumerName); - $pidFilePath = $this->getPidFileName($consumerName); - $pid = $this->pid->getPid($pidFilePath); - - if ($pid && $this->pid->isRun($pidFilePath)) { - posix_kill($pid, SIGKILL); - } - - if (file_exists($pidFileFullPath)) { - unlink($pidFileFullPath); + foreach ($this->getConsumerProcessIds($consumer->getName()) as $consumerProcessId) { + exec("kill {$consumerProcessId}"); //phpcs:ignore } } @@ -230,13 +208,15 @@ protected function tearDown() } /** - * @param string $consumerName The consumers name - * @return string The name to file with PID + * Get Consumer ProcessIds + * + * @param string $consumer + * @return string[] */ - private function getPidFileName($consumerName) + private function getConsumerProcessIds($consumer) { - $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); - - return $consumerName . '-' . $sanitizedHostname . ConsumersRunner::PID_FILE_EXT; + //phpcs:ignore + exec("ps ax | grep -v grep | grep 'queue:consumers:start {$consumer}' | awk '{print $1}'", $output); + return $output; } } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_order_hostedpro.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_order_hostedpro.php new file mode 100644 index 000000000000..479fadb94af9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_order_hostedpro.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Model\Order\Payment; +use Magento\Paypal\Model\Config; +use Magento\Sales\Model\Order; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +include __DIR__ . '/../../GraphQl/Quote/_files/guest/create_empty_cart.php'; +include __DIR__ . '/../../PaypalGraphQl/_files/add_simple_product_payflowLink.php'; +include __DIR__ . '/../../GraphQl/Quote/_files/guest/set_guest_email.php'; +include __DIR__ . '/../../GraphQl/Quote/_files/set_new_shipping_address.php'; +include __DIR__ . '/../../GraphQl/Quote/_files/set_new_billing_address.php'; +include __DIR__ . '/../../GraphQl/Quote/_files/set_flatrate_shipping_method.php'; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +$store = $objectManager->get(StoreManagerInterface::class)->getStore(); + +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + +/** @var \Magento\Sales\Model\Order\Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(Config::METHOD_HOSTEDPRO) + ->setBaseAmountAuthorized(30) + ->setAdditionalInformation('secure_form_url', 'https://hostedpro.paypal.com'); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(Order::class); +$order->setCustomerEmail('wpphs.co@co.com') + ->setIncrementId('100000017') + ->setQuoteId($quote->getId()) + ->setStoreId($store->getId()) + ->setState(Order::STATE_PENDING_PAYMENT) + ->setStatus(Order::STATE_PENDING_PAYMENT) + ->setCustomerIsGuest(true) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php index b194bdbde307..cfefd7d3e6d6 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php @@ -11,7 +11,6 @@ use Magento\PaypalGraphQl\PaypalExpressAbstractTest; use Magento\Framework\Serialize\SerializerInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteId; -use Magento\Framework\UrlInterface; /** * Test ExpressSetPaymentMethodTest graphql endpoint for customer @@ -71,19 +70,16 @@ public function testResolve(string $paymentMethod): void $cartId = $cart->getId(); $maskedCartId = $this->quoteIdToMaskedId->execute((int) $cartId); - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); - $query = <<<QUERY mutation { createPaypalExpressToken(input: { cart_id: "{$maskedCartId}", code: "{$paymentMethod}", urls: { - return_url: "{$baseUrl}paypal/express/return/", - cancel_url: "{$baseUrl}paypal/express/cancel/" - success_url: "{$baseUrl}checkout/onepage/success/", - pending_url: "{$baseUrl}checkout/onepage/pending/" + return_url: "paypal/express/return/", + cancel_url: "paypal/express/cancel/" + success_url: "checkout/onepage/success/", + pending_url: "checkout/onepage/pending/" } express_button: false }) diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php index 96d80dfc2805..d55820ccffc1 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php @@ -18,7 +18,6 @@ use Magento\Paypal\Model\Payflow\Service\Gateway; use Magento\Quote\Model\QuoteIdToMaskedQuoteId; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\UrlInterface; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -113,10 +112,6 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void { $paymentMethod = 'payflow_link'; $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); - $query = <<<QUERY mutation { @@ -126,8 +121,8 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void code: "$paymentMethod" payflow_link: { - cancel_url:"{$baseUrl}paypal/payflow/cancelPayment" - return_url:"{$baseUrl}paypal/payflow/returnUrl" + cancel_url:"paypal/payflow/cancelPayment" + return_url:"paypal/payflow/returnUrl" } } }) { diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php index b8efe7c0319b..899af918b04b 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php @@ -10,7 +10,6 @@ use Magento\PaypalGraphQl\PaypalPayflowProAbstractTest; use Magento\Framework\Serialize\SerializerInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteId; -use Magento\Framework\UrlInterface; use Magento\Framework\DataObject; /** @@ -72,9 +71,6 @@ public function testResolveCustomer(): void $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); - $query = <<<QUERY mutation { setPaymentMethodOnCart(input: { @@ -101,9 +97,9 @@ public function testResolveCustomer(): void input: { cart_id:"{$cartId}", urls: { - cancel_url: "{$baseUrl}paypal/transparent/cancel/" - error_url: "{$baseUrl}paypal/transparent/error/" - return_url: "{$baseUrl}paypal/transparent/response/" + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" } } ) { diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/GetHostedProSecureUrlTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/GetHostedProSecureUrlTest.php new file mode 100644 index 000000000000..b6e0ed4694a8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/GetHostedProSecureUrlTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Guest; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test getHostedProUrl graphql endpoint for Paypal Hosted Pro payment method + * + * @magentoAppArea graphql + */ +class GetHostedProSecureUrlTest extends TestCase +{ + /** @var GraphQlRequest */ + private $graphQlRequest; + + /** @var SerializerInterface */ + private $json; + + /** @var GetMaskedQuoteIdByReservedOrderId */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->json = $objectManager->get(SerializerInterface::class); + $this->graphQlRequest = $objectManager->create(GraphQlRequest::class); + } + + /** + * Test get hostedpro secure URL + * + * @magentoConfigFixture default_store payment/hosted_pro/active 1 + * @magentoConfigFixture default_store payment/hosted_pro/sandbox_flag 1 + * @magentoDataFixture Magento/Paypal/_files/quote_order_hostedpro.php + * @return void + */ + public function testResolveHostedProUrl(): void + { + $reservedQuoteId = 'test_quote'; + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedQuoteId); + + $payflowLinkTokenQuery + = <<<QUERY + { + getHostedProUrl(input: {cart_id:"$cartId"}) + { + secure_form_url + } +} +QUERY; + + $response = $this->graphQlRequest->send($payflowLinkTokenQuery); + $responseData = $this->json->unserialize($response->getContent()); + + $this->assertArrayNotHasKey('errors', $responseData); + $this->assertNotEmpty($responseData['data']['getHostedProUrl']); + $expectedSecureUrl = 'https://hostedpro.paypal.com'; + $actualSecureUrl = $responseData['data']['getHostedProUrl']['secure_form_url']; + $this->assertEquals($expectedSecureUrl, $actualSecureUrl); + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php index e5e1955bbf81..1b5f14c7df63 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php @@ -11,7 +11,7 @@ use Magento\PaypalGraphQl\PaypalExpressAbstractTest; use Magento\Framework\Serialize\SerializerInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteId; -use Magento\Framework\UrlInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** * Test ExpressSetPaymentMethodTest graphql endpoint for guest @@ -69,19 +69,16 @@ public function testResolveGuest(string $paymentMethod): void $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); - $query = <<<QUERY mutation { createPaypalExpressToken(input: { cart_id: "{$cartId}", code: "{$paymentMethod}", urls: { - return_url: "{$baseUrl}paypal/express/return/", - cancel_url: "{$baseUrl}paypal/express/cancel/" - success_url: "{$baseUrl}checkout/onepage/success/", - pending_url: "{$baseUrl}checkout/onepage/pending/" + return_url: "paypal/express/return/", + cancel_url: "paypal/express/cancel/" + success_url: "checkout/onepage/success/", + pending_url: "checkout/onepage/pending/" } express_button: false }) diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php index f91ffd4089d7..3c7bd4a8c0bd 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php @@ -15,7 +15,7 @@ use Magento\Quote\Model\QuoteIdToMaskedQuoteId; /** - * Test createPaypalExpressToken graphql endpoint for guest + * Test create PaypalExpressToken graphql endpoint for guest * * @magentoAppArea graphql */ @@ -140,46 +140,36 @@ public function testResolveWithPaypalError($paymentMethod): void } /** - * Test redirect Urls are validated + * Test create paypal token for Invalid Url for guest * + * @param string $paymentMethod * @return void + * @dataProvider getPaypalCodesProvider + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php */ - public function testResolveWithInvalidRedirectUrl(): void + public function testResolveWithInvalidRedirectUrl($paymentMethod): void { - $paymentMethod = 'paypal_express'; $this->enablePaymentMethod($paymentMethod); + if ($paymentMethod === 'payflow_express') { + $this->enablePaymentMethod('payflow_link'); + } $reservedQuoteId = 'test_quote'; $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); - $query = <<<QUERY -mutation { - createPaypalExpressToken(input: { - cart_id: "{$cartId}", - code: "{$paymentMethod}", - urls: { - return_url: "http://mangeto.test/paypal/express/return/", - cancel_url: "http://mangeto.test/paypal/express/cancel/" - success_url: "not/a/url", - pending_url: "http://mangeto.test/checkout/onepage/pending/" - } - }) - { - __typename - token - paypal_urls{ - start - edit - } - } -} -QUERY; - - $expectedExceptionMessage = "Invalid URL 'not/a/url'."; + $query = $this->getCreateTokenMutationWithInvalidUrl($cartId, $paymentMethod); + $expectedExceptionMessage = "Invalid Url."; $response = $this->graphQlRequest->send($query); $responseData = $this->json->unserialize($response->getContent()); + $this->assertArrayHasKey('createPaypalExpressToken', $responseData['data']); + $this->assertEmpty($responseData['data']['createPaypalExpressToken']); $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); @@ -198,4 +188,37 @@ public function getPaypalCodesProvider(): array ['payflow_express'], ]; } + + /** + * Get GraphQl query for creating Paypal token + * + * @param string $cartId + * @param string $paymentMethod + * @return string + */ + protected function getCreateTokenMutationWithInvalidUrl(string $cartId, string $paymentMethod): string + { + return <<<QUERY +mutation { + createPaypalExpressToken(input: { + cart_id: "{$cartId}", + code: "{$paymentMethod}", + urls: { + return_url: "paypal/express/return/", + cancel_url: "paypal/express/cancel/" + success_url: "http://mage.com/checkout/onepage/success/", + pending_url: "checkout/onepage/pending/" + } + }) + { + __typename + token + paypal_urls{ + start + edit + } + } +} +QUERY; + } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php index 1ef054fbdaac..0e1a74fa817d 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php @@ -10,7 +10,6 @@ use Magento\PaypalGraphQl\PaypalPayflowProAbstractTest; use Magento\Framework\Serialize\SerializerInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteId; -use Magento\Framework\UrlInterface; use Magento\Framework\DataObject; /** @@ -72,9 +71,6 @@ public function testResolveGuest(): void $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); - $query = <<<QUERY mutation { setPaymentMethodOnCart(input: { @@ -101,9 +97,9 @@ public function testResolveGuest(): void input: { cart_id:"{$cartId}", urls: { - cancel_url: "{$baseUrl}paypal/transparent/cancel/" - error_url: "{$baseUrl}paypal/transparent/error/" - return_url: "{$baseUrl}paypal/transparent/response/" + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" } } ) { diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php index 8bbb0dff2d69..df7f80ccd35a 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php @@ -107,9 +107,9 @@ public function testResolveWithInvalidRedirectUrl(): void input: { cart_id:"{$cartId}", urls: { - cancel_url: "http://domain/paypal/transparent/cancel/" - error_url: "not/a/url" - return_url: "http://domain/paypal/transparent/response/" + cancel_url: "paypal/transparent/cancel/" + error_url: "http://domain/paypal/transparent/cancel" + return_url: "paypal/transparent/response/" } } ) { @@ -122,10 +122,11 @@ public function testResolveWithInvalidRedirectUrl(): void } QUERY; - $expectedExceptionMessage = "Invalid URL 'not/a/url'."; + $expectedExceptionMessage = "Invalid Url."; $response = $this->graphQlRequest->send($query); $responseData = $this->json->unserialize($response->getContent()); + $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php new file mode 100644 index 000000000000..0920aa2eb36b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php @@ -0,0 +1,263 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Guest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\Paypal\Model\Api\Nvp; +use Magento\Paypal\Model\Config; +use Magento\Paypal\Model\Hostedpro; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Paypal\Model\Api\Type\Factory as ApiFactory; + +/** + * End to end place order test using hostedpro via graphql endpoint + * + * @magentoAppArea graphql + */ +class PlaceOrderWithHostedProTest extends TestCase +{ + private $paymentMethod = Config::METHOD_HOSTEDPRO; + + /** @var GraphQlRequest */ + private $graphQlRequest; + + /** @var SerializerInterface */ + private $json; + + /** @var GetMaskedQuoteIdByReservedOrderId */ + private $getMaskedQuoteIdByReservedOrderId; + + /** @var ObjectManager */ + private $objectManager; + + /** @var Nvp|MockObject */ + private $nvpMock; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $this->objectManager->create(GraphQlRequest::class); + $this->json = $this->objectManager->get(SerializerInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + + $this->nvpMock = $this->getMockBuilder(Nvp::class) + ->disableOriginalConstructor() + ->setMethods(['call']) + ->getMock(); + + $apiFactoryMock = $this->getMockBuilder(ApiFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $apiFactoryMock->method('create')->willReturn($this->nvpMock); + + $this->objectManager->addSharedInstance($apiFactoryMock, ApiFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->objectManager->removeSharedInstance(ApiFactory::class); + } + + /** + * Test successful place order with Hosted Pro + * + * @magentoConfigFixture default_store payment/hosted_pro/active 1 + * @magentoConfigFixture default_store paypal/wpp/sandbox_flag 1 + * @magentoConfigFixture default_store paypal/general/merchant_country GB + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @return void + */ + public function testPlaceOrderWithHostedPro(): void + { + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query + = <<<QUERY + mutation { + setPaymentMethodOnCart(input: { + cart_id: "$cartId" + payment_method: { + code: "$this->paymentMethod" + hosted_pro: { + cancel_url:"paypal/hostedpro/customcancel" + return_url:"paypal/hostedpro/customreturn" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "$cartId"}) { + order { + order_id + } + } +} +QUERY; + + $apiRequestData = require __DIR__ . '/../../../_files/hosted_pro_nvp_request.php'; + $apiResponseData = require __DIR__ . '/../../../_files/hosted_pro_nvp_response.php'; + + $this->nvpMock + ->expects($this->once()) + ->method('call') + ->with(Hostedpro::BM_BUTTON_METHOD, $apiRequestData) + ->willReturn($apiResponseData); + + $response = $this->graphQlRequest->send($query); + $responseData = $this->json->unserialize($response->getContent()); + + $this->assertArrayNotHasKey('errors', $responseData); + $this->assertArrayHasKey('data', $responseData); + $this->assertEquals( + $this->paymentMethod, + $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] + ); + $this->assertTrue( + isset($responseData['data']['placeOrder']['order']['order_id']) + ); + $this->assertEquals( + 'test_quote', + $responseData['data']['placeOrder']['order']['order_id'] + ); + } + + /** + * Test place order with Hosted Pro with a declined status + * + * @magentoConfigFixture default_store payment/hosted_pro/active 1 + * @magentoConfigFixture default_store paypal/wpp/sandbox_flag 1 + * @magentoConfigFixture default_store paypal/general/merchant_country GB + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @return void + */ + public function testOrderWithHostedProDeclined(): void + { + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query + = <<<QUERY + mutation { + setPaymentMethodOnCart(input: { + cart_id: "$cartId" + payment_method: { + code: "$this->paymentMethod" + hosted_pro: { + cancel_url:"paypal/hostedpro/customCancel" + return_url:"paypal/hostedpro/customReturnUrl" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "$cartId"}) { + order { + order_id + } + } +} +QUERY; + + $exceptionMessage = 'Declined response message from PayPal gateway'; + $exception = new LocalizedException(__($exceptionMessage)); + $expectedExceptionMessage = 'Unable to place order: ' . $exceptionMessage; + + $this->nvpMock->method('call')->willThrowException($exception); + + $response = $this->graphQlRequest->send($query); + $responseData = $this->json->unserialize($response->getContent()); + $this->assertArrayHasKey('errors', $responseData); + $actualError = $responseData['errors'][0]; + $this->assertEquals($expectedExceptionMessage, $actualError['message']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + } + + /** + * Test setPaymentMethodOnCart with invalid url inputs + * + * @magentoConfigFixture default_store payment/hosted_pro/active 1 + * @magentoConfigFixture default_store paypal/wpp/sandbox_flag 1 + * @magentoConfigFixture default_store paypal/general/merchant_country GB + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @return void + */ + public function testSetPaymentMethodInvalidUrls() + { + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query + = <<<QUERY + mutation { + setPaymentMethodOnCart(input: { + cart_id: "$cartId" + payment_method: { + code: "$this->paymentMethod" + hosted_pro: { + cancel_url:"http://mysite.com/paypal/hostedpro/customCancel" + return_url:"http://mysite.com/paypal/hostedpro/customReturnUrl" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + + $expectedExceptionMessage = 'Invalid Url.'; + + $response = $this->graphQlRequest->send($query); + $responseData = $this->json->unserialize($response->getContent()); + $this->assertArrayHasKey('errors', $responseData); + $actualError = $responseData['errors'][0]; + $this->assertEquals($expectedExceptionMessage, $actualError['message']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php index 7ad0659839bc..c2ec5e6bddde 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php @@ -119,9 +119,9 @@ public function testResolvePlaceOrderWithPayflowLink(): void code: "$paymentMethod" payflow_link: { - cancel_url:"http://mage.test/paypal/payflow/cancel" - return_url:"http://mage.test/paypal/payflow/return" - error_url:"http://mage.test/paypal/payflow/error" + cancel_url:"paypal/payflow/cancel" + return_url:"paypal/payflow/return" + error_url:"paypal/payflow/error" } } }) { @@ -221,9 +221,9 @@ public function testResolveWithPayflowLinkDeclined(): void code: "$paymentMethod" payflow_link: { - cancel_url:"http://mage.test/paypal/payflow/cancelPayment" - return_url:"http://mage.test/paypal/payflow/returnUrl" - error_url:"http://mage.test/paypal/payflow/returnUrl" + cancel_url:"paypal/payflow/cancelPayment" + return_url:"paypal/payflow/returnUrl" + error_url:"paypal/payflow/returnUrl" } } }) { diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php new file mode 100644 index 000000000000..7ef5db4a2dda --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php @@ -0,0 +1,318 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Guest; + +use Magento\Framework\App\ProductMetadataInterface; +use Magento\Framework\DataObject; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\Paypal\Model\Payflow\Request; +use Magento\Paypal\Model\Payflow\RequestFactory; +use Magento\Paypal\Model\Payflow\Service\Gateway; +use Magento\Paypal\Model\Payflowlink; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * End to end place order test using PayPal payments advanced via GraphQl + * + * @magentoAppArea graphql + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PlaceOrderWithPaymentsAdvancedTest extends TestCase +{ + /** @var GraphQlRequest */ + private $graphQlRequest; + + /** @var GetMaskedQuoteIdByReservedOrderId */ + private $getMaskedQuoteIdByReservedOrderId; + + /** @var ObjectManager */ + private $objectManager; + + /** @var Gateway|MockObject */ + private $gateway; + + /** @var Request|MockObject */ + private $paymentRequest; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $this->objectManager->create(GraphQlRequest::class); + + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->gateway = $this->getMockBuilder(Gateway::class) + ->disableOriginalConstructor() + ->setMethods(['postRequest']) + ->getMock(); + + $requestFactory = $this->getMockBuilder(RequestFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentRequest = $this->getMockBuilder(Request::class) + ->disableOriginalConstructor() + ->setMethods(['__call','setData']) + ->getMock(); + $this->paymentRequest->method('__call') + ->willReturnCallback( + function ($method) { + if (strpos($method, 'set') === 0) { + return $this->paymentRequest; + } + return null; + } + ); + + $requestFactory->method('create')->willReturn($this->paymentRequest); + $this->objectManager->addSharedInstance($this->gateway, Gateway::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->objectManager->removeSharedInstance(Gateway::class); + } + + /** + * Test successful place Order with Payments Advanced + * + * @magentoConfigFixture default_store payment/payflow_advanced/active 1 + * @magentoConfigFixture default_store payment/payflow_advanced/sandbox_flag 1 + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @return void + */ + public function testResolvePlaceOrderWithPaymentsAdvanced(): void + { + $paymentMethod = 'payflow_advanced'; + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); + $button = 'Magento_Cart_' . $productMetadata->getEdition(); + + $payflowLinkResponse = new DataObject( + [ + 'result' => '0', + 'respmsg' => 'Approved', + 'pnref' => 'V19A3D27B61E', + 'result_code' => '0' + ] + ); + $this->gateway->expects($this->once()) + ->method('postRequest') + ->willReturn($payflowLinkResponse); + + $this->paymentRequest + ->method('setData') + ->willReturnMap( + [ + [ + 'user' => null, + 'vendor' => null, + 'partner' => null, + 'pwd' => null, + 'verbosity' => null, + 'BUTTONSOURCE' => $button, + 'tender' => 'C', + ], + $this->returnSelf() + ], + ['USER1', 1, $this->returnSelf()], + ['USER2', 'USER2SilentPostHash', $this->returnSelf()] + ); + + $responseData = $this->setPaymentMethodAndPlaceOrder($cartId, $paymentMethod); + + $this->assertArrayNotHasKey('errors', $responseData); + $this->assertArrayHasKey('data', $responseData); + $this->assertEquals( + $paymentMethod, + $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] + ); + $this->assertNotEmpty(isset($responseData['data']['placeOrder']['order']['order_id'])); + $this->assertEquals('test_quote', $responseData['data']['placeOrder']['order']['order_id']); + } + + /** + * Test place Order with Payments Advanced with Invalid Url. + * + * @magentoConfigFixture default_store payment/payflow_advanced/active 1 + * @magentoConfigFixture default_store payment/payflow_advanced/sandbox_flag 1 + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @return void + */ + public function testResolvePaymentsAdvancedWithInvalidUrl(): void + { + $paymentMethod = 'payflow_advanced'; + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $responseData = $this->setPaymentMethodWithInValidUrl($cartId, $paymentMethod); + $expectedExceptionMessage = "Invalid Url."; + $this->assertArrayHasKey('errors', $responseData); + $actualError = $responseData['errors'][0]; + $this->assertEquals($expectedExceptionMessage, $actualError['message']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + } + + /** + * Test place Order with PaymentAdvanced with a declined status + * + * @magentoConfigFixture default_store payment/payflow_advanced/active 1 + * @magentoConfigFixture default_store payment/payflow_advanced/sandbox_flag 1 + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testResolveWithPaymentAdvancedDeclined(): void + { + $paymentMethod = 'payflow_advanced'; + $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $resultCode = Payflowlink::RESPONSE_CODE_DECLINED_BY_FILTER; + $exception = new \Zend_Http_Client_Exception(__('Declined response message from PayPal gateway')); + //Exception message is transformed into more controlled message + $expectedExceptionMessage = + "Unable to place order: Payment Gateway is unreachable at the moment. Please use another payment option."; + + $this->paymentRequest->method('setData') + ->with( + [ + [ + 'invnum' => 'test_quote', + 'amt' => '40.00', + 'pnref' => 'TEST123PNREF', + 'USER2' => '1EncryptedSilentPostHash', + 'result' => $resultCode, + 'trxtype' => 'A', + + ] + ] + ) + ->willReturnSelf(); + + $this->gateway->method('postRequest')->willThrowException($exception); + + $responseData = $this->setPaymentMethodAndPlaceOrder($cartId, $paymentMethod); + $this->assertArrayHasKey('errors', $responseData); + $actualError = $responseData['errors'][0]; + $this->assertEquals($expectedExceptionMessage, $actualError['message']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + } + + /** + * Send setPaymentMethodOnCart and placeOrder mutations and return response content + * + * @param string $cartId + * @param string $paymentMethod + * @return array + */ + private function setPaymentMethodAndPlaceOrder(string $cartId, string $paymentMethod): array + { + $serializer = $this->objectManager->get(SerializerInterface::class); + $query + = <<<QUERY + mutation { + setPaymentMethodOnCart(input: { + cart_id: "$cartId" + payment_method: { + code: "$paymentMethod" + payflow_link: { + cancel_url:"paypal/payflowadvanced/customcancel" + return_url:"paypal/payflowadvanced/customreturn" + error_url:"paypal/payflowadvanced/customerror" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "$cartId"}) { + order { + order_id + } + } +} +QUERY; + + $response = $this->graphQlRequest->send($query); + $responseContent = $serializer->unserialize($response->getContent()); + + return $responseContent; + } + + /** + * Send setPaymentMethodOnCart and placeOrder mutations and return response content + * + * @param string $cartId + * @param string $paymentMethod + * @return array + */ + private function setPaymentMethodWithInValidUrl(string $cartId, string $paymentMethod): array + { + $serializer = $this->objectManager->get(SerializerInterface::class); + $query + = <<<QUERY + mutation { + setPaymentMethodOnCart(input: { + cart_id: "$cartId" + payment_method: { + code: "$paymentMethod" + payflow_link: { + cancel_url:"paypal/payflowadvanced/cancel" + return_url:"http://localhost/paypal/payflowadvanced/return" + error_url:"paypal/payflowadvanced/error" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + + $response = $this->graphQlRequest->send($query); + $responseContent = $serializer->unserialize($response->getContent()); + + return $responseContent; + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php index a4b61ed11d78..7c23ec08af65 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php @@ -72,9 +72,9 @@ public function testSetPayflowLinkAsPaymentMethod(): void payment_method: { code: "$paymentMethod" payflow_link: { - return_url:"http://magento.com/paypal/payflow/link/success" - cancel_url:"http://magento.com/paypal/payflow/link/cancel" - error_url:"http://magento.com/paypal/payflow/link/error" + return_url:"paypal/payflow/link/success" + cancel_url:"paypal/payflow/link/cancel" + error_url:"paypal/payflow/link/error" } } }) { @@ -105,15 +105,15 @@ public function testSetPayflowLinkAsPaymentMethod(): void $payment = $quote->getPayment(); $this->assertEquals( - "http://magento.com/paypal/payflow/link/cancel", + "paypal/payflow/link/cancel", $payment->getAdditionalInformation('cancel_url') ); $this->assertEquals( - "http://magento.com/paypal/payflow/link/success", + "paypal/payflow/link/success", $payment->getAdditionalInformation('return_url') ); $this->assertEquals( - "http://magento.com/paypal/payflow/link/error", + "paypal/payflow/link/error", $payment->getAdditionalInformation('error_url') ); } @@ -146,9 +146,9 @@ public function testInvalidUrl(): void payment_method: { code: "$paymentMethod" payflow_link: { - return_url:"http://magento.com/paypal/payflow/link/sucess" - cancel_url:"http://magento.com/paypal/payflow/link/cancel" - error_url:"/not/a/validUrl" + return_url:"http://magento.com/paypal/payflow/link/success" + cancel_url:"paypal/payflow/link/cancel" + error_url:"paypal/payflow/link/error" } } }) { @@ -165,7 +165,7 @@ public function testInvalidUrl(): void $responseData = $this->json->unserialize($response->getContent()); $this->assertArrayHasKey('errors', $responseData); - $expectedExceptionMessage = "Invalid URL '/not/a/validUrl'."; + $expectedExceptionMessage = "Invalid Url."; $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalExpressAbstractTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalExpressAbstractTest.php index 936bbcf6503d..9ae9d1f36d4a 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalExpressAbstractTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalExpressAbstractTest.php @@ -30,7 +30,6 @@ use Psr\Log\LoggerInterface; use Magento\Config\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\UrlInterface; /** * Abstract class with common logic for Paypal GraphQl tests @@ -181,8 +180,6 @@ private function getNvpMock(string $nvpClass) */ protected function getCreateTokenMutation(string $cartId, string $paymentMethod): string { - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); return <<<QUERY mutation { @@ -190,10 +187,10 @@ protected function getCreateTokenMutation(string $cartId, string $paymentMethod) cart_id: "{$cartId}", code: "{$paymentMethod}", urls: { - return_url: "{$baseUrl}paypal/express/return/", - cancel_url: "{$baseUrl}paypal/express/cancel/" - success_url: "{$baseUrl}checkout/onepage/success/", - pending_url: "{$baseUrl}checkout/onepage/pending/" + return_url: "paypal/express/return/", + cancel_url: "paypal/express/cancel/" + success_url: "checkout/onepage/success/", + pending_url: "checkout/onepage/pending/" } express_button: true }) diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalPayflowProAbstractTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalPayflowProAbstractTest.php index e5a94c1f1039..49088c5dbd6f 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalPayflowProAbstractTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/PaypalPayflowProAbstractTest.php @@ -18,7 +18,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Config\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\UrlInterface; use Magento\Payment\Model\Method\Online\GatewayInterface; @@ -143,8 +142,6 @@ private function getGatewayMock() */ protected function getCreatePayflowTokenMutation(string $cartId): string { - $url = $this->objectManager->get(UrlInterface::class); - $baseUrl = $url->getBaseUrl(); return <<<QUERY mutation { @@ -152,9 +149,9 @@ protected function getCreatePayflowTokenMutation(string $cartId): string input: { cart_id:"{$cartId}", urls: { - cancel_url: "{$baseUrl}paypal/transparent/cancel/" - error_url: "{$baseUrl}paypal/transparent/error/" - return_url: "{$baseUrl}paypal/transparent/response/" + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" } } ) { diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/customer_paypal_create_token_request.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/customer_paypal_create_token_request.php index 342074daec1d..f456e8cc46ae 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/customer_paypal_create_token_request.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/customer_paypal_create_token_request.php @@ -9,19 +9,24 @@ use Magento\TestFramework\ObjectManager; $url = ObjectManager::getInstance()->get(UrlInterface::class); -$baseUrl = $url->getBaseUrl(); +$cancelUrl = $url->getUrl('paypal/express/cancel/'); +$successUrl = $url->getUrl('checkout/onepage/success/'); +$returnUrl = $url->getUrl('paypal/express/return/'); +$pendingUrl = $url->getUrl('checkout/onepage/pending/'); + + return [ 'PAYMENTACTION' => 'Authorization', 'AMT' => '30.00', 'CURRENCYCODE' => 'USD', - 'RETURNURL' => $baseUrl . 'paypal/express/return/', - 'CANCELURL' => $baseUrl . 'paypal/express/cancel/', + 'RETURNURL' => $returnUrl, + 'CANCELURL' => $cancelUrl, 'INVNUM' => 'test_quote', 'SOLUTIONTYPE' => 'Mark', - 'GIROPAYCANCELURL' => $baseUrl . 'paypal/express/cancel/', - 'GIROPAYSUCCESSURL' => $baseUrl . 'checkout/onepage/success/', - 'BANKTXNPENDINGURL' => $baseUrl . 'checkout/onepage/pending/', + 'GIROPAYCANCELURL' => $cancelUrl, + 'GIROPAYSUCCESSURL' => $successUrl, + 'BANKTXNPENDINGURL' => $pendingUrl, 'SHIPPINGAMT' => '10.00', 'ITEMAMT' => '20.00', 'TAXAMT' => '0.00', diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/guest_paypal_create_token_request.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/guest_paypal_create_token_request.php index 37bb11e3f075..4b5ed74b5b67 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/guest_paypal_create_token_request.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/guest_paypal_create_token_request.php @@ -9,19 +9,23 @@ use Magento\TestFramework\ObjectManager; $url = ObjectManager::getInstance()->get(UrlInterface::class); -$baseUrl = $url->getBaseUrl(); +$cancelUrl = $url->getUrl('paypal/express/cancel/'); +$successUrl = $url->getUrl('checkout/onepage/success/'); +$returnUrl = $url->getUrl('paypal/express/return/'); +$pendingUrl = $url->getUrl('checkout/onepage/pending/'); + return [ 'PAYMENTACTION' => 'Authorization', 'AMT' => '30.00', 'CURRENCYCODE' => 'USD', - 'RETURNURL' => $baseUrl . 'paypal/express/return/', - 'CANCELURL' => $baseUrl . 'paypal/express/cancel/', + 'RETURNURL' => $returnUrl, + 'CANCELURL' => $cancelUrl, 'INVNUM' => 'test_quote', 'SOLUTIONTYPE' => 'Mark', - 'GIROPAYCANCELURL' => $baseUrl . 'paypal/express/cancel/', - 'GIROPAYSUCCESSURL' => $baseUrl . 'checkout/onepage/success/', - 'BANKTXNPENDINGURL' => $baseUrl . 'checkout/onepage/pending/', + 'GIROPAYCANCELURL' => $cancelUrl, + 'GIROPAYSUCCESSURL' => $successUrl, + 'BANKTXNPENDINGURL' => $pendingUrl, 'SHIPPINGAMT' => '10.00', 'ITEMAMT' => '20.00', 'TAXAMT' => '0.00', diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/hosted_pro_nvp_request.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/hosted_pro_nvp_request.php new file mode 100644 index 000000000000..e1bd00ba32db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/hosted_pro_nvp_request.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +//phpcs:ignorefile +use Magento\Framework\UrlInterface; +use Magento\TestFramework\ObjectManager; + +$url = ObjectManager::getInstance()->create(UrlInterface::class); +$cancelUrl = $url->getUrl('paypal/hostedpro/customcancel'); +$returnUrl = $url->getUrl('paypal/hostedpro/customreturn'); + +return [ + 'METHOD' => 'BMCreateButton', + 'BUTTONCODE' => 'TOKEN', + 'BUTTONTYPE' => 'PAYMENT', + 'L_BUTTONVAR0' => 'invoice=test_quote', + 'L_BUTTONVAR1' => 'address_override=true', + 'L_BUTTONVAR2' => 'currency_code=USD', + 'L_BUTTONVAR3' => 'buyer_email=guest@example.com', + 'L_BUTTONVAR4' => 'billing_first_name=John', + 'L_BUTTONVAR5' => 'billing_last_name=Smith', + 'L_BUTTONVAR6' => 'billing_city=CityM', + 'L_BUTTONVAR7' => 'billing_state=AL', + 'L_BUTTONVAR8' => 'billing_zip=75477', + 'L_BUTTONVAR9' => 'billing_country=US', + 'L_BUTTONVAR10' => 'billing_address1=Green str, 67', + 'L_BUTTONVAR11' => 'billing_address2=', + 'L_BUTTONVAR12' => 'first_name=John', + 'L_BUTTONVAR13' => 'last_name=Smith', + 'L_BUTTONVAR14' => 'city=CityM', + 'L_BUTTONVAR15' => 'state=AL', + 'L_BUTTONVAR16' => 'zip=75477', + 'L_BUTTONVAR17' => 'country=US', + 'L_BUTTONVAR18' => 'address1=Green str, 67', + 'L_BUTTONVAR19' => 'address2=', + 'L_BUTTONVAR20' => 'paymentaction=authorization', + 'L_BUTTONVAR21' => 'notify_url=http://localhost/index.php/paypal/ipn/', + 'L_BUTTONVAR22' => 'cancel_return=' . $cancelUrl, + 'L_BUTTONVAR23' => 'return=' . $returnUrl, + 'L_BUTTONVAR24' => 'lc=US', + 'L_BUTTONVAR25' => 'template=mobile-iframe', + 'L_BUTTONVAR26' => 'showBillingAddress=false', + 'L_BUTTONVAR27' => 'showShippingAddress=true', + 'L_BUTTONVAR28' => 'showBillingEmail=false', + 'L_BUTTONVAR29' => 'showBillingPhone=false', + 'L_BUTTONVAR30' => 'showCustomerName=false', + 'L_BUTTONVAR31' => 'showCardInfo=true', + 'L_BUTTONVAR32' => 'showHostedThankyouPage=false', + 'L_BUTTONVAR33' => 'subtotal=20.00', + 'L_BUTTONVAR34' => 'total=30.00', + 'L_BUTTONVAR35' => 'tax=0.00', + 'L_BUTTONVAR36' => 'shipping=10.00', + 'L_BUTTONVAR37' => 'discount=0.00', +]; diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/hosted_pro_nvp_response.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/hosted_pro_nvp_response.php new file mode 100644 index 000000000000..0d0cd7dabe2a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/hosted_pro_nvp_response.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +//phpcs:ignorefile +return [ + 'WEBSITECODE' => '<form action="https://securepayments.sandbox.paypal.com/webapps/HostedSoleSolutionApp/webflow/sparta/hostedSoleSolutionProcess" method="post"> +<input type="hidden" name="hosted_button_id" value="hosted_pro_hosted_button_id"> +<input type="image" src="https://www.sandbox.paypal.com/en_GB/i/btn/btn_paynow_LG.gif" border="0" name="submit" alt="PayPal – The safer, easier way to pay online!"> +<img alt="" border="0" src="https://www.sandbox.paypal.com/en_GB/i/scr/pixel.gif" width="1" height="1"> +</form>', + 'EMAILLINK' => 'https://securepayments.sandbox.paypal.com/webapps/HostedSoleSolutionApp/webflow/sparta/hostedSoleSolutionProcess?hosted_button_id=hosted_pro_hosted_button_id', + 'HOSTEDBUTTONID' => 'hosted_pro_hosted_button_id', + 'TIMESTAMP' => '2019-07-25T16:03:27Z', + 'CORRELATIONID' => '1098765432', + 'ACK' => 'Success', + 'VERSION' => '72.0', + 'BUILD' => '1234567', +]; diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php index e74409485c78..7a622ab15814 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php @@ -10,7 +10,7 @@ use Magento\TestFramework\ObjectManager; $url = ObjectManager::getInstance()->get(UrlInterface::class); -$baseUrl = $url->getBaseUrl(); +$notifyUrl = $url->getUrl('paypal/ipn/'); $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); $button = 'Magento_Cart_' . $productMetadata->getEdition(); @@ -22,7 +22,7 @@ 'AMT' => '30.00', 'CURRENCYCODE' => 'USD', 'BUTTONSOURCE' => $button, - 'NOTIFYURL' => $baseUrl . 'paypal/ipn/', + 'NOTIFYURL' => $notifyUrl, 'RETURNFMFDETAILS' => 1, 'SHIPPINGAMT' => '10.00', 'ITEMAMT' => '20.00', diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php index aed921489923..ad254e1db40b 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php @@ -6,13 +6,21 @@ namespace Magento\ProductAlert\Model; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\ProductAlert\Model\Email; +use Magento\Store\Model\Website; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; + /** + * Test for Magento\ProductAlert\Model\Email class. + * * @magentoAppIsolation enabled */ class EmailTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\ProductAlert\Model\Email + * @var Email */ protected $_emailModel; @@ -31,6 +39,24 @@ class EmailTest extends \PHPUnit\Framework\TestCase */ protected $_customerViewHelper; + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @inheritdoc + */ protected function setUp() { $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -38,9 +64,15 @@ protected function setUp() \Magento\Customer\Api\AccountManagementInterface::class ); $this->_customerViewHelper = $this->_objectManager->create(\Magento\Customer\Helper\View::class); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + + $this->_emailModel = $this->_objectManager->create(Email::class); } /** + * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @dataProvider customerFunctionDataProvider @@ -49,19 +81,12 @@ protected function setUp() */ public function testSend($isCustomerIdUsed) { - \Magento\TestFramework\Helper\Bootstrap::getInstance() - ->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - - $this->_emailModel = $this->_objectManager->create(\Magento\ProductAlert\Model\Email::class); - - /** @var \Magento\Store\Model\Website $website */ - $website = $this->_objectManager->create(\Magento\Store\Model\Website::class); + /** @var Website $website */ + $website = $this->_objectManager->create(Website::class); $website->load(1); $this->_emailModel->setWebsite($website); - /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ - $customerRepository = $this->_objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $customer = $customerRepository->getById(1); + $customer = $this->customerRepository->getById(1); if ($isCustomerIdUsed) { $this->_emailModel->setCustomerId(1); @@ -70,19 +95,14 @@ public function testSend($isCustomerIdUsed) } /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->_objectManager->create(\Magento\Catalog\Model\Product::class); - $product->load(1); + $product = $this->productRepository->getById(1); $this->_emailModel->addPriceProduct($product); $this->_emailModel->send(); - /** @var \Magento\TestFramework\Mail\Template\TransportBuilderMock $transportBuilder */ - $transportBuilder = $this->_objectManager->get( - \Magento\TestFramework\Mail\Template\TransportBuilderMock::class - ); $this->assertContains( 'John Smith,', - $transportBuilder->getSentMessage()->getRawMessage() + $this->transportBuilder->getSentMessage()->getRawMessage() ); } @@ -93,4 +113,47 @@ public function customerFunctionDataProvider() [false], ]; } + + /** + * Assert that product price shown correct in email for customers with different customer groups. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_wholesale_tier_price.php + * @magentoDataFixture Magento/Customer/_files/two_customers_with_different_customer_groups.php + * + * @return void + */ + public function testEmailForDifferentCustomers(): void + { + $customerGeneral = $this->customerRepository->get('customer@example.com'); + $customerWholesale = $this->customerRepository->get('customer_two@example.com'); + $product = $this->productRepository->get('simple'); + + /** @var Website $website */ + $website = $this->_objectManager->create(Website::class); + $website->load(1); + + $data = [ + $customerGeneral->getId() => '10', + $customerWholesale->getId() => '5', + ]; + + foreach ($data as $customerId => $expectedPrice) { + $this->_emailModel->clean(); + $this->_emailModel->setCustomerId($customerId); + $this->_emailModel->setWebsite($website); + $this->_emailModel->addStockProduct($product); + $this->_emailModel->setType('stock'); + $this->_emailModel->send(); + + $expectedPriceBox = '<span id="product-price-' . $product->getId() . '" data-price-amount="' + . $expectedPrice . '" data-price-type="finalPrice" ' + . 'class="price-wrapper "><span class="price">$' . $expectedPrice . '.00</span></span>'; + + $this->assertContains( + $expectedPriceBox, + $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent() + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Rule/CollectionTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Rule/CollectionTest.php index f619193d4921..9ea97b6e939c 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Rule/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Rule/CollectionTest.php @@ -5,12 +5,39 @@ */ namespace Magento\SalesRule\Model\ResourceModel\Rule; +use Magento\Config\Model\Config\Backend\Admin\Custom as AdminBackendConfig; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ -class CollectionTest extends \PHPUnit\Framework\TestCase +class CollectionTest extends TestCase { + /** + * @var Collection + */ + private $collection; + + /** + * @var string + */ + private $defaultTimezone; + + /** + * @inheritDoc + */ + protected function setUp() + { + $scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + $this->defaultTimezone = $scopeConfig->getValue(AdminBackendConfig::XML_PATH_GENERAL_LOCALE_TIMEZONE); + + $this->collection = Bootstrap::getObjectManager()->create(Collection::class); + } + /** * @magentoDataFixture Magento/SalesRule/_files/rules.php * @magentoDataFixture Magento/SalesRule/_files/coupons.php @@ -21,12 +48,8 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ public function testSetValidationFilter($couponCode, $expectedItems) { - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $items = array_values($collection->setValidationFilter(1, 0, $couponCode)->getItems()); - - $ids = []; + /** @var \Magento\SalesRule\Model\Rule[] $items */ + $items = array_values($this->collection->setValidationFilter(1, 0, $couponCode)->getItems()); $this->assertEquals( count($expectedItems), @@ -34,6 +57,7 @@ public function testSetValidationFilter($couponCode, $expectedItems) 'Invalid number of items in the result collection' ); + $ids = []; foreach ($items as $key => $item) { $this->assertEquals($expectedItems[$key], $item->getName()); $this->assertFalse( @@ -71,7 +95,7 @@ public function setValidationFilterDataProvider() */ public function testSetValidationFilterWithGroup() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\SalesRule\Model\Rule $rule */ $rule = $objectManager->get(\Magento\Framework\Registry::class) @@ -82,13 +106,8 @@ public function testSetValidationFilterWithGroup() $quote->load('test_order_item_with_items', 'reserved_order_id'); //gather only the existing rules that obey the validation filter - /** @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection $ruleCollection */ - $ruleCollection = $objectManager->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $appliedRulesArray = array_keys( - $ruleCollection->setValidationFilter( + $this->collection->setValidationFilter( $quote->getStore()->getWebsiteId(), 0, '', @@ -108,7 +127,7 @@ public function testSetValidationFilterWithGroup() */ public function testSetValidationFilterAnyCategory() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\SalesRule\Model\Rule $rule */ $rule = $objectManager->get(\Magento\Framework\Registry::class) @@ -119,13 +138,8 @@ public function testSetValidationFilterAnyCategory() $quote->load('test_order_item_with_items', 'reserved_order_id'); //gather only the existing rules that obey the validation filter - /** @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection $ruleCollection */ - $ruleCollection = $objectManager->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $appliedRulesArray = array_keys( - $ruleCollection->setValidationFilter( + $this->collection->setValidationFilter( $quote->getStore()->getWebsiteId(), 0, '', @@ -146,20 +160,15 @@ public function testSetValidationFilterAnyCategory() */ public function testSetValidationFilterOther() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Quote\Model\Quote $quote */ $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); //gather only the existing rules that obey the validation filter - /** @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection $ruleCollection */ - $ruleCollection = $objectManager->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $appliedRulesArray = array_keys( - $ruleCollection->setValidationFilter( + $this->collection->setValidationFilter( $quote->getStore()->getWebsiteId(), 0, '', @@ -181,11 +190,8 @@ public function testSetValidationFilterOther() public function testMultiRulesWithTimezone() { $this->setSpecificTimezone('Europe/Kiev'); - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addWebsiteGroupDateFilter(1, 0); - $items = array_values($collection->getItems()); + $this->collection->addWebsiteGroupDateFilter(1, 0); + $items = array_values($this->collection->getItems()); $this->assertNotEmpty($items); } @@ -200,11 +206,8 @@ public function testMultiRulesWithTimezone() public function testMultiRulesWithDifferentTimezone() { $this->setSpecificTimezone('Australia/Sydney'); - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addWebsiteGroupDateFilter(1, 0); - $items = array_values($collection->getItems()); + $this->collection->addWebsiteGroupDateFilter(1, 0); + $items = array_values($this->collection->getItems()); $this->assertNotEmpty($items); } @@ -224,7 +227,7 @@ protected function setSpecificTimezone($timezone) ] ] ]; - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config\Factory::class) + Bootstrap::getObjectManager()->get(\Magento\Config\Model\Config\Factory::class) ->create() ->addData($localeData) ->save(); @@ -239,11 +242,9 @@ protected function setSpecificTimezone($timezone) */ public function testAddAttributeInConditionFilterPositive() { - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addAttributeInConditionFilter('attribute_for_sales_rule_1'); - $item = $collection->getFirstItem(); + $this->collection->addAttributeInConditionFilter('attribute_for_sales_rule_1'); + /** @var \Magento\SalesRule\Model\Rule $item */ + $item = $this->collection->getFirstItem(); $this->assertEquals('50% Off on some attribute', $item->getName()); } @@ -256,16 +257,57 @@ public function testAddAttributeInConditionFilterPositive() */ public function testAddAttributeInConditionFilterNegative() { - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addAttributeInConditionFilter('attribute_for_sales_rule_2'); - $this->assertEquals(0, $collection->count()); + $this->collection->addAttributeInConditionFilter('attribute_for_sales_rule_2'); + $this->assertEquals(0, $this->collection->count()); + } + + /** + * @magentoAppIsolation disabled + * @magentoDataFixture Magento/SalesRule/_files/multi_websites_rules.php + * @dataProvider addWebsiteFilterDataProvider + * @param string[] $websiteCodes + * @param int $count + */ + public function testAddWebsiteFilter(array $websiteCodes, int $count) + { + $websiteRepository = Bootstrap::getObjectManager()->get(WebsiteRepositoryInterface::class); + $websiteIds = []; + foreach ($websiteCodes as $websiteCode) { + $websiteIds[] = (int) $websiteRepository->get($websiteCode)->getId(); + } + + $this->collection->addWebsiteFilter($websiteIds); + $this->assertEquals($count, $this->collection->getSize()); + $this->assertCount($count, $this->collection->getItems()); } - public function tearDown() + /** + * @return array + */ + public function addWebsiteFilterDataProvider(): array + { + return [ + [ + ['base'], + 4, + ], + [ + ['test'], + 2, + ], + [ + ['base', 'test'], + 5, + ], + ]; + } + + /** + * @inheritDoc + */ + protected function tearDown() { // restore default timezone - $this->setSpecificTimezone('America/Los_Angeles'); + $this->setSpecificTimezone($this->defaultTimezone); } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules.php new file mode 100644 index 000000000000..a43df3d67c07 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +include __DIR__ . '/../../Store/_files/website.php'; +include __DIR__ . '/rules.php'; + +/** @var \Magento\SalesRule\Model\Rule $rule2 */ +$rule2->setWebsiteIds($website->getId()) + ->save(); + +/** @var \Magento\SalesRule\Model\Rule $rule3 */ +$rule3->setWebsiteIds(implode(',', [1, $website->getId()])) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules_rollback.php new file mode 100644 index 000000000000..9e0e01b9fc51 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +include __DIR__ . '/rules_rollback.php'; +include __DIR__ . '/../../Store/_files/website_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules.php index f2366305473a..ebedc20814c8 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules.php @@ -4,9 +4,9 @@ * See COPYING.txt for license details. */ -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule1 */ +$rule1 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule1->setName( '#1' )->setIsActive( 1 @@ -27,9 +27,9 @@ )->setSortOrder(1) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule2 */ +$rule2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule2->setName( '#2' )->setIsActive( 1 @@ -50,9 +50,9 @@ )->setSortOrder(2) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule3 */ +$rule3 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule3->setName( '#3' )->setIsActive( 1 @@ -73,9 +73,9 @@ )->setSortOrder(3) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule4 */ +$rule4 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule4->setName( '#4' )->setIsActive( 1 @@ -96,9 +96,9 @@ )->setSortOrder(4) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule5 */ +$rule5 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule5->setName( '#5' )->setIsActive( 1 diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/breadcrumbs.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/breadcrumbs.test.js index 2d8f145bce1d..60fa44b309fc 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/breadcrumbs.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/breadcrumbs.test.js @@ -6,8 +6,7 @@ /* eslint-disable max-nested-callbacks */ define([ 'squire', - 'jquery', - 'jquery/ui' + 'jquery' ], function (Squire, $) { 'use strict'; @@ -43,8 +42,7 @@ define([ injector.require( [ 'Magento_Catalog/js/product/breadcrumbs', - 'Magento_Theme/js/view/breadcrumbs', - 'jquery/ui' + 'Magento_Theme/js/view/breadcrumbs' ], function (mixin, breadcrumb) { widget = mixin(breadcrumb); done(); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/view/product-ids-resolver.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/view/product-ids-resolver.test.js index be92b6814da3..953f759b819d 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/view/product-ids-resolver.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/view/product-ids-resolver.test.js @@ -7,8 +7,7 @@ define([ 'squire', 'jquery', - 'ko', - 'jquery/ui' + 'ko' ], function (Squire, $, ko) { 'use strict'; diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js index 24dc53563851..3d065ea8c018 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js @@ -8,8 +8,7 @@ define([ 'uiRegistry', 'Magento_Ui/js/form/client', 'jquery', - 'mageUtils', - 'jquery/ui' + 'mageUtils' ], function (_, registry, Constr, $, utils) { 'use strict'; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php index fe15c06bdea4..57d5f5a7a11d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php @@ -184,6 +184,14 @@ public function testJsFiles() function ($file) { $content = file_get_contents($file); $this->_testObsoletePropertySkipCalculate($content); + if (strpos($file, 'requirejs-config.js') === false + && ( + strpos($file, '/view/frontend/web/') !== false + || strpos($file, '/view/base/web/') !== false + ) + ) { + $this->_testJqueryUiLibraryIsNotUsedInJs($content); + } }, Files::init()->getJsFiles() ); @@ -703,14 +711,16 @@ private function _checkCompletePathOfClass( } $pathWithConstParts = explode('\\', $pathWithConst); $pathInUseNamespace = trim($matchClassString['classPath'], '\\'); - $pathInUseNamespaceTruncated = trim(trim( - preg_replace( - '/' . preg_quote($pathWithConstParts[0]) . '$/', - '', - $pathInUseNamespace - ), - '\\' - )); + $pathInUseNamespaceTruncated = trim( + trim( + preg_replace( + '/' . preg_quote($pathWithConstParts[0]) . '$/', + '', + $pathInUseNamespace + ), + '\\' + ) + ); if ($this->_checkClasspathProperDivisionNoConstantPath( $pathInUseNamespaceTruncated, $pathInUseNamespace, @@ -947,4 +957,21 @@ private function getBlacklistFiles($absolutePath = false) } return $ignored; } + + /** + * Assert that jquery/ui library is not used in JS content. + * + * @param string $fileContent + */ + private function _testJqueryUiLibraryIsNotUsedInJs($fileContent) + { + $this->_assertNotRegexp( + '/(["\'])jquery\/ui\1/', + $fileContent, + $this->_suggestReplacement( + sprintf("Dependency '%s' is redundant.", 'jquery/ui'), + 'Use separate jquery ui widget instead of all library.' + ) + ); + } } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php index 7cb65c27d7fe..5c342614f94f 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php @@ -7,6 +7,9 @@ */ namespace Magento\Test\Legacy; +/** + * Static test for phtml template files. + */ class PhtmlTemplateTest extends \PHPUnit\Framework\TestCase { public function testBlockVariableInsteadOfThis() @@ -19,7 +22,7 @@ public function testBlockVariableInsteadOfThis() * @param string $file */ function ($file) { - $this->assertNotRegExp( + self::assertNotRegExp( '/this->(?!helper)\S*/iS', file_get_contents($file), 'Access to members and methods of Block class through $this is ' . @@ -46,7 +49,7 @@ public function testObsoleteBlockMethods() * @param string $file */ function ($file) { - $this->assertNotRegexp( + self::assertNotRegexp( '/block->_[^_]+\S*\(/iS', file_get_contents($file), 'Access to protected and private members of Block class is ' . @@ -68,7 +71,7 @@ public function testObsoleteJavascriptAttributeType() * @param string $file */ function ($file) { - $this->assertNotRegexp( + self::assertNotRegexp( '/type="text\/javascript"/', file_get_contents($file), 'Please do not use "text/javascript" type attribute.' @@ -77,4 +80,29 @@ function ($file) { \Magento\Framework\App\Utility\Files::init()->getPhtmlFiles() ); } + + public function testJqueryUiLibraryIsNotUsedInTemplates() + { + $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); + $invoker( + /** + * 'jquery/ui' library is not obligatory to use in phtml files. + * It's better to use needed jquery ui widget instead. + * + * @param string $file + */ + function ($file) { + if (strpos($file, '/view/frontend/templates/') !== false + || strpos($file, '/view/base/templates/') !== false + ) { + self::assertNotRegexp( + '/(["\'])jquery\/ui\1/', + file_get_contents($file), + 'Please do not use "jquery/ui" library in templates. Use needed jquery ui widget instead.' + ); + } + }, + \Magento\Framework\App\Utility\Files::init()->getPhtmlFiles() + ); + } } diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index e59cd0983da1..4653203f4d57 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt @@ -216,3 +216,5 @@ Magento/Elasticsearch6/Model/Client Magento/Config/App/Config/Type Magento/InventoryReservationCli/Test/Integration Magento/InventoryAdminUi/Controller/Adminhtml +Magento/Newsletter/Model/Queue/TransportBuilder +Magento/Framework/Mail/Template/TransportBuilder \ No newline at end of file diff --git a/lib/internal/Magento/Framework/Api/ImageContentValidator.php b/lib/internal/Magento/Framework/Api/ImageContentValidator.php index cc5e830f6723..91df08152386 100644 --- a/lib/internal/Magento/Framework/Api/ImageContentValidator.php +++ b/lib/internal/Magento/Framework/Api/ImageContentValidator.php @@ -86,7 +86,7 @@ protected function isMimeTypeValid($mimeType) */ protected function isNameValid($name) { - // Cannot contain \ / : * ? " < > | + // Cannot contain \ / ? * : " ; < > ( ) | { } if (!preg_match('/^[^\\/?*:";<>()|{}\\\\]+$/', $name)) { return false; } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php b/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php index 3c03155fb163..70dca9ec1661 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php @@ -15,6 +15,9 @@ use Magento\Framework\Escaper; +/** + * Form editable multiselect element. + */ class Editablemultiselect extends \Magento\Framework\Data\Form\Element\Multiselect { /** @@ -77,8 +80,7 @@ public function getElementHtml() $html .= " <script type='text/javascript'> require([ - 'jquery', - 'jquery/ui' + 'jquery' ], function( $ ){ function isResolved(){ diff --git a/lib/internal/Magento/Framework/Encryption/Encryptor.php b/lib/internal/Magento/Framework/Encryption/Encryptor.php index 4bc1b2589362..3e501bdd601b 100644 --- a/lib/internal/Magento/Framework/Encryption/Encryptor.php +++ b/lib/internal/Magento/Framework/Encryption/Encryptor.php @@ -162,7 +162,7 @@ public function __construct( */ public function getLatestHashVersion(): int { - if (extension_loaded('sodium')) { + if (extension_loaded('sodium') && defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { return self::HASH_VERSION_ARGON2ID13; } @@ -314,8 +314,8 @@ public function validateHashVersion($hash, $validateCount = false) * Explode password hash * * @param string $hash - * @throws \RuntimeException When given hash cannot be processed. * @return array + * @throws \RuntimeException When given hash cannot be processed. */ private function explodePasswordHash($hash) { @@ -398,6 +398,7 @@ public function encryptWithFastestAvailableAlgorithm($data) ':' . $this->getCipherVersion() . ':' . base64_encode($crypt->encrypt($data)); } + /** * Look for key and crypt versions in encrypted data before decrypting * @@ -579,13 +580,15 @@ private function getArgonHash($data, $salt = ''): string $salt = str_pad($salt, SODIUM_CRYPTO_PWHASH_SALTBYTES, $salt); } - return bin2hex(sodium_crypto_pwhash( - SODIUM_CRYPTO_SIGN_SEEDBYTES, - $data, - $salt, - SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, - SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, - $this->hashVersionMap[self::HASH_VERSION_ARGON2ID13] - )); + return bin2hex( + sodium_crypto_pwhash( + SODIUM_CRYPTO_SIGN_SEEDBYTES, + $data, + $salt, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, + $this->hashVersionMap[self::HASH_VERSION_ARGON2ID13] + ) + ); } } diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index 0faf3bfa5e13..63c688b364e8 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -320,7 +320,7 @@ public function escapeJsQuote($data, $quote = '\'') public function escapeXssInUrl($data) { return htmlspecialchars( - $this->escapeScriptIdentifiers((string)$data), + $this->escapeScriptIdentifiers(html_entity_decode((string)$data)), $this->htmlSpecialCharsFlag | ENT_HTML5 | ENT_HTML401, 'UTF-8', false diff --git a/lib/internal/Magento/Framework/Lock/Backend/Database.php b/lib/internal/Magento/Framework/Lock/Backend/Database.php index efdba63e7a08..096e77a11768 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Database.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Database.php @@ -19,6 +19,14 @@ */ class Database implements \Magento\Framework\Lock\LockManagerInterface { + /** + * Max time for lock is 1 week + * + * MariaDB does not support negative timeout value to get infinite timeout, + * so we set 1 week for lock timeout + */ + private const MAX_LOCK_TIME = 604800; + /** * @var ResourceConnection */ @@ -87,7 +95,7 @@ public function lock(string $name, int $timeout = -1): bool $result = (bool)$this->resource->getConnection()->query( "SELECT GET_LOCK(?, ?);", - [(string)$name, (int)$timeout] + [$name, $timeout < 0 ? self::MAX_LOCK_TIME : $timeout] )->fetchColumn(); if ($result === true) { @@ -143,7 +151,7 @@ public function isLocked(string $name): bool return (bool)$this->resource->getConnection()->query( "SELECT IS_USED_LOCK(?);", - [(string)$name] + [$name] )->fetchColumn(); } diff --git a/lib/internal/Magento/Framework/Mail/Address.php b/lib/internal/Magento/Framework/Mail/Address.php new file mode 100644 index 000000000000..18e1a8c72f21 --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/Address.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +/** + * Class MailAddress + */ +class Address +{ + /** + * @var string|null + */ + private $name; + + /** + * @var string + */ + private $email; + + /** + * MailAddress constructor + * + * @param string|null $email + * @param string|null $name + */ + public function __construct( + ?string $email, + ?string $name + ) { + $this->email = $email; + $this->name = $name; + } + + /** + * Name getter + * + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Email getter + * + * @return string + */ + public function getEmail(): string + { + return $this->email; + } +} diff --git a/lib/internal/Magento/Framework/Mail/AddressConverter.php b/lib/internal/Magento/Framework/Mail/AddressConverter.php new file mode 100644 index 000000000000..d787482f1757 --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/AddressConverter.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Magento\Framework\Mail\Exception\InvalidArgumentException; + +/** + * Class AddressConverter + */ +class AddressConverter +{ + /** + * @var AddressFactory + */ + private $addressFactory; + + /** + * AddressConverter constructor + * + * @param AddressFactory $addressFactory + */ + public function __construct( + AddressFactory $addressFactory + ) { + $this->addressFactory = $addressFactory; + } + + /** + * Creates MailAddress from string values + * + * @param string $email + * @param string|null $name + * + * @return Address + */ + public function convert(string $email, ?string $name = null): Address + { + return $this->addressFactory->create( + [ + 'name' => $name, + 'email' => $email + ] + ); + } + + /** + * Converts array to list of MailAddresses + * + * @param array $addresses + * + * @return Address[] + * @throws InvalidArgumentException + */ + public function convertMany(array $addresses): array + { + $addressList = []; + foreach ($addresses as $key => $value) { + + if (is_int($key) || is_numeric($key)) { + $addressList[] = $this->convert($value); + continue; + } + + if (!is_string($key)) { + throw new InvalidArgumentException( + sprintf( + 'Invalid key type in provided addresses array ("%s")', + (is_object($key) ? get_class($key) : var_export($key, 1)) + ) + ); + } + $addressList[] = $this->convert($key, $value); + } + + return $addressList; + } +} diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php new file mode 100644 index 000000000000..aaef97507518 --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -0,0 +1,257 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Magento\Framework\Mail\Exception\InvalidArgumentException; +use Zend\Mail\Address as ZendAddress; +use Zend\Mail\AddressList; +use Zend\Mail\Message as ZendMessage; +use Zend\Mime\Message as ZendMimeMessage; + +/** + * Class EmailMessage + */ +class EmailMessage implements EmailMessageInterface +{ + /** + * @var ZendMessage + */ + private $message; + + /** + * @var MimeMessageInterfaceFactory + */ + private $mimeMessageFactory; + + /** + * @var AddressFactory + */ + private $addressFactory; + + /** + * EmailMessage constructor + * + * @param MimeMessageInterface $body + * @param array $to + * @param MimeMessageInterfaceFactory $mimeMessageFactory + * @param AddressFactory $addressFactory + * @param Address[]|null $from + * @param Address[]|null $cc + * @param Address[]|null $bcc + * @param Address[]|null $replyTo + * @param Address|null $sender + * @param string|null $subject + * @param string|null $encoding + * @throws InvalidArgumentException + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function __construct( + MimeMessageInterface $body, + array $to, + MimeMessageInterfaceFactory $mimeMessageFactory, + AddressFactory $addressFactory, + ?array $from = null, + ?array $cc = null, + ?array $bcc = null, + ?array $replyTo = null, + ?Address $sender = null, + ?string $subject = '', + ?string $encoding = '' + ) { + $this->message = new ZendMessage(); + $mimeMessage = new ZendMimeMessage(); + $mimeMessage->setParts($body->getParts()); + $this->message->setBody($mimeMessage); + if ($encoding) { + $this->message->setEncoding($encoding); + } + if ($subject) { + $this->message->setSubject($subject); + } + if ($sender) { + $this->message->setSender($sender->getEmail(), $sender->getName()); + } + if (count($to) < 1) { + throw new InvalidArgumentException('Email message must have at list one addressee'); + } + if ($to) { + $this->message->setTo($this->convertAddressArrayToAddressList($to)); + } + if ($replyTo) { + $this->message->setReplyTo($this->convertAddressArrayToAddressList($replyTo)); + } + if ($from) { + $this->message->setFrom($this->convertAddressArrayToAddressList($from)); + } + if ($cc) { + $this->message->setCc($this->convertAddressArrayToAddressList($cc)); + } + if ($bcc) { + $this->message->setBcc($this->convertAddressArrayToAddressList($bcc)); + } + $this->mimeMessageFactory = $mimeMessageFactory; + $this->addressFactory = $addressFactory; + } + + /** + * @inheritDoc + */ + public function getEncoding(): string + { + return $this->message->getEncoding(); + } + + /** + * @inheritDoc + */ + public function getHeaders(): array + { + return $this->message->getHeaders()->toArray(); + } + + /** + * @inheritDoc + */ + public function getFrom(): ?array + { + return $this->convertAddressListToAddressArray($this->message->getFrom()); + } + + /** + * @inheritDoc + */ + public function getTo(): array + { + return $this->convertAddressListToAddressArray($this->message->getTo()); + } + + /** + * @inheritDoc + */ + public function getCc(): ?array + { + return $this->convertAddressListToAddressArray($this->message->getCc()); + } + + /** + * @inheritDoc + */ + public function getBcc(): ?array + { + return $this->convertAddressListToAddressArray($this->message->getBcc()); + } + + /** + * @inheritDoc + */ + public function getReplyTo(): ?array + { + return $this->convertAddressListToAddressArray($this->message->getReplyTo()); + } + + /** + * @inheritDoc + */ + public function getSender(): ?Address + { + /** @var ZendAddress $zendSender */ + if (!$zendSender = $this->message->getSender()) { + return null; + } + + return $this->addressFactory->create( + [ + 'email' => $zendSender->getEmail(), + 'name' => $zendSender->getName() + ] + ); + } + + /** + * @inheritDoc + */ + public function getSubject(): ?string + { + return $this->message->getSubject(); + } + + /** + * @inheritDoc + */ + public function getBody(): MimeMessageInterface + { + return $this->mimeMessageFactory->create( + ['parts' => $this->message->getBody()->getParts()] + ); + } + + /** + * @inheritDoc + */ + public function getBodyText(): string + { + return $this->message->getBodyText(); + } + + /** + * @inheritdoc + */ + public function getRawMessage(): string + { + return $this->toString(); + } + + /** + * @inheritDoc + */ + public function toString(): string + { + return $this->message->toString(); + } + + /** + * Converts AddressList to array + * + * @param AddressList $addressList + * @return Address[] + */ + private function convertAddressListToAddressArray(AddressList $addressList): array + { + $arrayList = []; + foreach ($addressList as $address) { + $arrayList[] = + $this->addressFactory->create( + [ + 'email' => $address->getEmail(), + 'name' => $address->getName() + ] + ); + } + + return $arrayList; + } + + /** + * Converts MailAddress array to AddressList + * + * @param Address[] $arrayList + * @return AddressList + */ + private function convertAddressArrayToAddressList(array $arrayList): AddressList + { + $zendAddressList = new AddressList(); + foreach ($arrayList as $address) { + $zendAddressList->add($address->getEmail(), $address->getName()); + } + + return $zendAddressList; + } +} diff --git a/lib/internal/Magento/Framework/Mail/EmailMessageInterface.php b/lib/internal/Magento/Framework/Mail/EmailMessageInterface.php new file mode 100644 index 000000000000..95f83ff679cd --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/EmailMessageInterface.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Mail; + +/** + * Interface EmailMessageInterface + */ +interface EmailMessageInterface +{ + /** + * Get the message encoding + * + * @return string + */ + public function getEncoding(): string; + + /** + * Access headers collection + * + * @return array + */ + public function getHeaders(): array; + + /** + * Retrieve list of From senders + * + * @return Address[]|null + */ + public function getFrom(): ?array; + + /** + * Access the address list of the To header + * + * @return Address[] + */ + public function getTo(): array; + + /** + * Retrieve list of CC recipients + * + * @return Address[]|null + */ + public function getCc(): ?array; + + /** + * Retrieve list of Bcc recipients + * + * @return Address[]|null + */ + public function getBcc(): ?array; + + /** + * Access the address list of the Reply-To header + * + * @return Address[]|null + */ + public function getReplyTo(): ?array; + + /** + * Retrieve the sender address, if any + * + * @return Address|null + */ + public function getSender(): ?Address; + + /** + * Get the message subject header value + * + * @return null|string + */ + public function getSubject(): ?string; + + /** + * Return the currently set message body + * + * @return MimeMessageInterface + */ + public function getBody(): MimeMessageInterface; + + /** + * Get the string-serialized message body text + * + * @return string + */ + public function getBodyText(): string; + + /** + * Serialize to string + * + * @return string + */ + public function toString(): string; +} diff --git a/lib/internal/Magento/Framework/Mail/Exception/InvalidArgumentException.php b/lib/internal/Magento/Framework/Mail/Exception/InvalidArgumentException.php new file mode 100644 index 000000000000..b9442a77658d --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/Exception/InvalidArgumentException.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail\Exception; + +/** + * Class InvalidArgumentException + */ +class InvalidArgumentException extends \InvalidArgumentException +{ +} diff --git a/lib/internal/Magento/Framework/Mail/MimeInterface.php b/lib/internal/Magento/Framework/Mail/MimeInterface.php new file mode 100644 index 000000000000..026dd188d168 --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/MimeInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Mail; + +/** + * Interface MimeInterface used providing constants + * + * @see \Zend\Mime\Mime + */ +interface MimeInterface +{ + // @codingStandardsIgnoreStart + public const TYPE_OCTET_STREAM = 'application/octet-stream'; + public const TYPE_TEXT = 'text/plain'; + public const TYPE_HTML = 'text/html'; + public const ENCODING_7BIT = '7bit'; + public const ENCODING_8BIT = '8bit'; + public const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; + public const ENCODING_BASE64 = 'base64'; + public const DISPOSITION_ATTACHMENT = 'attachment'; + public const DISPOSITION_INLINE = 'inline'; + public const LINE_LENGTH = 72; + public const LINE_END = "\n"; + public const MULTIPART_ALTERNATIVE = 'multipart/alternative'; + public const MULTIPART_MIXED = 'multipart/mixed'; + public const MULTIPART_RELATED = 'multipart/related'; + public const CHARSET_REGEX = '#=\?(?P<charset>[\x21\x23-\x26\x2a\x2b\x2d\x5e\5f\60\x7b-\x7ea-zA-Z0-9]+)\?(?P<encoding>[\x21\x23-\x26\x2a\x2b\x2d\x5e\5f\60\x7b-\x7ea-zA-Z0-9]+)\?(?P<text>[\x21-\x3e\x40-\x7e]+)#'; + // @codingStandardsIgnoreEnd +} diff --git a/lib/internal/Magento/Framework/Mail/MimeMessage.php b/lib/internal/Magento/Framework/Mail/MimeMessage.php new file mode 100644 index 000000000000..4d783dafd1d7 --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/MimeMessage.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Zend\Mime\Message as ZendMimeMessage; + +/** + * Class MimeMessage + */ +class MimeMessage implements MimeMessageInterface +{ + /** + * @var ZendMimeMessage + */ + private $mimeMessage; + + /** + * MimeMessage constructor + * + * @param array $parts + */ + public function __construct(array $parts) + { + $this->mimeMessage = new ZendMimeMessage(); + $this->mimeMessage->setParts($parts); + } + + /** + * @inheritDoc + */ + public function getParts(): array + { + return $this->mimeMessage->getParts(); + } + + /** + * @inheritDoc + */ + public function isMultiPart(): bool + { + return $this->mimeMessage->isMultiPart(); + } + + /** + * @inheritDoc + */ + public function getMessage(string $endOfLine = MimeInterface::LINE_END): string + { + return $this->mimeMessage->generateMessage($endOfLine); + } + + /** + * @inheritDoc + */ + public function getPartHeadersAsArray(int $partNum): array + { + return $this->mimeMessage->getPartHeadersArray($partNum); + } + + /** + * @inheritDoc + */ + public function getPartHeaders(int $partNum, string $endOfLine = MimeInterface::LINE_END): string + { + return $this->mimeMessage->getPartHeaders($partNum, $endOfLine); + } + + /** + * @inheritDoc + */ + public function getPartContent(int $partNum, string $endOfLine = MimeInterface::LINE_END): string + { + return $this->mimeMessage->getPartContent($partNum, $endOfLine); + } +} diff --git a/lib/internal/Magento/Framework/Mail/MimeMessageInterface.php b/lib/internal/Magento/Framework/Mail/MimeMessageInterface.php new file mode 100644 index 000000000000..a272bcfd8980 --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/MimeMessageInterface.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Mail; + +/** + * Interface MimeMessageInterface + */ +interface MimeMessageInterface +{ + /** + * Returns the list of all MimeParts in the message + * + * @return MimePartInterface[] + */ + public function getParts(): array; + + /** + * Check if message needs to be sent as multipart MIME message or if it has only one part. + * + * @return bool + */ + public function isMultiPart(): bool; + + /** + * Generate MIME-compliant message from the current configuration + * + * @param string $endOfLine + * + * @return string + */ + public function getMessage(string $endOfLine = MimeInterface::LINE_END): string; + + /** + * Get the headers of a given part as an array + * + * @param int $partNum + * + * @return array + */ + public function getPartHeadersAsArray(int $partNum): array; + + /** + * Get the headers of a given part as a string + * + * @param int $partNum + * @param string $endOfLine + * + * @return string + */ + public function getPartHeaders(int $partNum, string $endOfLine = MimeInterface::LINE_END): string; + + /** + * Get the (encoded) content of a given part as a string + * + * @param int $partNum + * @param string $endOfLine + * + * @return string + */ + public function getPartContent(int $partNum, string $endOfLine = MimeInterface::LINE_END): string; +} diff --git a/lib/internal/Magento/Framework/Mail/MimePart.php b/lib/internal/Magento/Framework/Mail/MimePart.php new file mode 100644 index 000000000000..9b9bb6eadbec --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/MimePart.php @@ -0,0 +1,221 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Magento\Framework\Mail\Exception\InvalidArgumentException; +use Zend\Mime\Part as ZendMimePart; + +/** + * @inheritDoc + */ +class MimePart implements MimePartInterface +{ + public const CHARSET_UTF8 = 'utf-8'; + + /** + * @var ZendMimePart + */ + private $mimePart; + + /** + * MimePart constructor + * + * @param resource|string $content + * @param string|null $type + * @param string|null $fileName + * @param string|null $disposition + * @param string|null $encoding + * @param string|null $description + * @param array|null $filters + * @param string|null $charset + * @param string|null $boundary + * @param string|null $location + * @param string|null $language + * @param bool|null $isStream + * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws InvalidArgumentException + */ + public function __construct( + $content, + ?string $type = MimeInterface::TYPE_HTML, + ?string $fileName = null, + ?string $disposition = MimeInterface::DISPOSITION_INLINE, + ?string $encoding = MimeInterface::ENCODING_QUOTED_PRINTABLE, + ?string $description = null, + ?array $filters = [], + ?string $charset = self::CHARSET_UTF8, + ?string $boundary = null, + ?string $location = null, + ?string $language = null, + ?bool $isStream = null + ) { + try { + $this->mimePart = new ZendMimePart($content); + } catch (\Exception $e) { + throw new InvalidArgumentException($e->getMessage()); + } + $this->mimePart->setType($type); + $this->mimePart->setEncoding($encoding); + $this->mimePart->setFilters($filters); + if ($charset) { + $this->mimePart->setBoundary($boundary); + } + if ($charset) { + $this->mimePart->setCharset($charset); + } + if ($disposition) { + $this->mimePart->setDisposition($disposition); + } + if ($description) { + $this->mimePart->setDescription($description); + } + if ($fileName) { + $this->mimePart->setFileName($fileName); + } + if ($location) { + $this->mimePart->setLocation($location); + } + if ($language) { + $this->mimePart->setLanguage($language); + } + if ($isStream) { + $this->mimePart->setIsStream($isStream); + } + } + + /** + * @inheritDoc + */ + public function getType(): string + { + return $this->mimePart->getType(); + } + + /** + * @inheritDoc + */ + public function getEncoding(): string + { + return $this->mimePart->getEncoding(); + } + + /** + * @inheritDoc + */ + public function getDisposition(): string + { + return $this->mimePart->getDisposition(); + } + + /** + * @inheritDoc + */ + public function getDescription(): string + { + return $this->mimePart->getDescription(); + } + + /** + * @inheritDoc + */ + public function getFileName(): string + { + return $this->mimePart->getFileName(); + } + + /** + * @inheritDoc + */ + public function getCharset(): string + { + return $this->mimePart->getCharset(); + } + + /** + * @inheritDoc + */ + public function getBoundary(): string + { + return $this->mimePart->getBoundary(); + } + + /** + * @inheritDoc + */ + public function getLocation(): string + { + return $this->mimePart->getLocation(); + } + + /** + * @inheritDoc + */ + public function getLanguage(): string + { + return $this->mimePart->getLanguage(); + } + + /** + * @inheritDoc + */ + public function getFilters(): array + { + return $this->mimePart->getFilters(); + } + + /** + * @inheritDoc + */ + public function isStream(): bool + { + return $this->mimePart->isStream(); + } + + /** + * @inheritDoc + */ + public function getEncodedStream($endOfLine = MimeInterface::LINE_END) + { + return $this->mimePart->getEncodedStream($endOfLine); + } + + /** + * @inheritDoc + */ + public function getContent($endOfLine = MimeInterface::LINE_END) + { + return $this->mimePart->getContent($endOfLine); + } + + /** + * @inheritDoc + */ + public function getRawContent(): string + { + return $this->mimePart->getRawContent(); + } + + /** + * @inheritDoc + */ + public function getHeadersArray($endOfLine = MimeInterface::LINE_END): array + { + return $this->mimePart->getHeadersArray($endOfLine); + } + + /** + * @inheritDoc + */ + public function getHeaders($endOfLine = MimeInterface::LINE_END): string + { + return $this->mimePart->getHeaders($endOfLine); + } +} diff --git a/lib/internal/Magento/Framework/Mail/MimePartInterface.php b/lib/internal/Magento/Framework/Mail/MimePartInterface.php new file mode 100644 index 000000000000..8a658cdf975c --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/MimePartInterface.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Mail; + +/** + * Interface representing a MIME part. + */ +interface MimePartInterface +{ + /** + * Get type + * + * @return string + */ + public function getType(): string; + + /** + * Get encoding + * + * @return string + */ + public function getEncoding(): string; + + /** + * Get disposition + * + * @return string + */ + public function getDisposition(): string; + + /** + * Get description + * + * @return string + */ + public function getDescription(): string; + + /** + * Get filename + * + * @return string + */ + public function getFileName(): string; + + /** + * Get charset + * + * @return string + */ + public function getCharset(): string; + + /** + * Get boundary + * + * @return string + */ + public function getBoundary(): string; + + /** + * Get location + * + * @return string + */ + public function getLocation(): string; + + /** + * Get language + * + * @return string + */ + public function getLanguage(): string; + + /** + * Get Filters + * + * @return array + */ + public function getFilters(): array; + + /** + * Check if this part can be read as a stream + * + * @return bool + */ + public function isStream(): bool; + + /** + * If this was created with a stream, return a filtered stream for reading the content. Useful for file attachment + * + * @param string $endOfLine + * + * @return resource + */ + public function getEncodedStream($endOfLine = MimeInterface::LINE_END); + + /** + * Get the Content of the current Mime Part in the given encoding. + * + * @param string $endOfLine + * + * @return string|resource + */ + public function getContent($endOfLine = MimeInterface::LINE_END); + + /** + * Get the RAW unencoded content from this part + * + * @return string + */ + public function getRawContent(): string; + + /** + * Create and return the array of headers for this MIME part + * + * @param string $endOfLine + * + * @return array + */ + public function getHeadersArray($endOfLine = MimeInterface::LINE_END): array; + + /** + * Create and return the array of headers for this MIME part + * + * @param string $endOfLine + * + * @return string + */ + public function getHeaders($endOfLine = MimeInterface::LINE_END): string; +} diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index 7bef92af57da..4a8d6572faaf 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -5,13 +5,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Mail\Template; use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Mail\EmailMessageInterface; +use Magento\Framework\Mail\EmailMessageInterfaceFactory; +use Magento\Framework\Mail\AddressConverter; +use Magento\Framework\Mail\Exception\InvalidArgumentException; use Magento\Framework\Mail\MessageInterface; use Magento\Framework\Mail\MessageInterfaceFactory; +use Magento\Framework\Mail\MimeInterface; +use Magento\Framework\Mail\MimeMessageInterfaceFactory; +use Magento\Framework\Mail\MimePartInterfaceFactory; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Mail\TransportInterfaceFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; @@ -55,7 +66,7 @@ class TransportBuilder /** * Mail Transport * - * @var \Magento\Framework\Mail\TransportInterface + * @var TransportInterface */ protected $transport; @@ -69,43 +80,71 @@ class TransportBuilder /** * Object Manager * - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $objectManager; /** * Message * - * @var \Magento\Framework\Mail\Message + * @var EmailMessageInterface */ protected $message; /** * Sender resolver * - * @var \Magento\Framework\Mail\Template\SenderResolverInterface + * @var SenderResolverInterface */ protected $_senderResolver; /** - * @var \Magento\Framework\Mail\TransportInterfaceFactory + * @var TransportInterfaceFactory */ protected $mailTransportFactory; /** - * @var \Magento\Framework\Mail\MessageInterfaceFactory + * Param that used for storing all message data until it will be used + * + * @var array + */ + private $messageData = []; + + /** + * @var EmailMessageInterfaceFactory + */ + private $emailMessageInterfaceFactory; + + /** + * @var MimeMessageInterfaceFactory + */ + private $mimeMessageInterfaceFactory; + + /** + * @var MimePartInterfaceFactory + */ + private $mimePartInterfaceFactory; + + /** + * @var AddressConverter|null */ - private $messageFactory; + private $addressConverter; /** + * TransportBuilder constructor + * * @param FactoryInterface $templateFactory * @param MessageInterface $message * @param SenderResolverInterface $senderResolver * @param ObjectManagerInterface $objectManager * @param TransportInterfaceFactory $mailTransportFactory - * @param MessageInterfaceFactory $messageFactory - * + * @param MessageInterfaceFactory|null $messageFactory + * @param EmailMessageInterfaceFactory|null $emailMessageInterfaceFactory + * @param MimeMessageInterfaceFactory|null $mimeMessageInterfaceFactory + * @param MimePartInterfaceFactory|null $mimePartInterfaceFactory + * @param addressConverter|null $addressConverter * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( FactoryInterface $templateFactory, @@ -113,14 +152,24 @@ public function __construct( SenderResolverInterface $senderResolver, ObjectManagerInterface $objectManager, TransportInterfaceFactory $mailTransportFactory, - MessageInterfaceFactory $messageFactory = null + MessageInterfaceFactory $messageFactory = null, + EmailMessageInterfaceFactory $emailMessageInterfaceFactory = null, + MimeMessageInterfaceFactory $mimeMessageInterfaceFactory = null, + MimePartInterfaceFactory $mimePartInterfaceFactory = null, + AddressConverter $addressConverter = null ) { $this->templateFactory = $templateFactory; $this->objectManager = $objectManager; $this->_senderResolver = $senderResolver; $this->mailTransportFactory = $mailTransportFactory; - $this->messageFactory = $messageFactory ?: $this->objectManager->get(MessageInterfaceFactory::class); - $this->message = $this->messageFactory->create(); + $this->emailMessageInterfaceFactory = $emailMessageInterfaceFactory ?: $this->objectManager + ->get(EmailMessageInterfaceFactory::class); + $this->mimeMessageInterfaceFactory = $mimeMessageInterfaceFactory ?: $this->objectManager + ->get(MimeMessageInterfaceFactory::class); + $this->mimePartInterfaceFactory = $mimePartInterfaceFactory ?: $this->objectManager + ->get(MimePartInterfaceFactory::class); + $this->addressConverter = $addressConverter ?: $this->objectManager + ->get(AddressConverter::class); } /** @@ -128,11 +177,13 @@ public function __construct( * * @param array|string $address * @param string $name + * * @return $this */ public function addCc($address, $name = '') { - $this->message->addCc($address, $name); + $this->addAddressByType('cc', $address, $name); + return $this; } @@ -141,11 +192,14 @@ public function addCc($address, $name = '') * * @param array|string $address * @param string $name + * * @return $this + * @throws InvalidArgumentException */ public function addTo($address, $name = '') { - $this->message->addTo($address, $name); + $this->addAddressByType('to', $address, $name); + return $this; } @@ -153,11 +207,14 @@ public function addTo($address, $name = '') * Add bcc address * * @param array|string $address + * * @return $this + * @throws InvalidArgumentException */ public function addBcc($address) { - $this->message->addBcc($address); + $this->addAddressByType('bcc', $address); + return $this; } @@ -166,28 +223,32 @@ public function addBcc($address) * * @param string $email * @param string|null $name + * * @return $this + * @throws InvalidArgumentException */ public function setReplyTo($email, $name = null) { - $this->message->setReplyTo($email, $name); + $this->addAddressByType('replyTo', $email, $name); + return $this; } /** * Set mail from address * - * @deprecated This function sets the from address but does not provide - * a way of setting the correct from addresses based on the scope. - * @see setFromByScope() - * * @param string|array $from + * * @return $this - * @throws \Magento\Framework\Exception\MailException + * @throws InvalidArgumentException + * @see setFromByScope() + * + * @deprecated This function sets the from address but does not provide + * a way of setting the correct from addresses based on the scope. */ public function setFrom($from) { - return $this->setFromByScope($from, null); + return $this->setFromByScope($from); } /** @@ -195,13 +256,16 @@ public function setFrom($from) * * @param string|array $from * @param string|int $scopeId + * * @return $this - * @throws \Magento\Framework\Exception\MailException + * @throws InvalidArgumentException + * @throws MailException */ public function setFromByScope($from, $scopeId = null) { $result = $this->_senderResolver->resolve($from, $scopeId); - $this->message->setFromAddress($result['email'], $result['name']); + $this->addAddressByType('from', $result['email'], $result['name']); + return $this; } @@ -209,11 +273,13 @@ public function setFromByScope($from, $scopeId = null) * Set template identifier * * @param string $templateIdentifier + * * @return $this */ public function setTemplateIdentifier($templateIdentifier) { $this->templateIdentifier = $templateIdentifier; + return $this; } @@ -221,6 +287,7 @@ public function setTemplateIdentifier($templateIdentifier) * Set template model * * @param string $templateModel + * * @return $this */ public function setTemplateModel($templateModel) @@ -233,11 +300,13 @@ public function setTemplateModel($templateModel) * Set template vars * * @param array $templateVars + * * @return $this */ public function setTemplateVars($templateVars) { $this->templateVars = $templateVars; + return $this; } @@ -250,13 +319,14 @@ public function setTemplateVars($templateVars) public function setTemplateOptions($templateOptions) { $this->templateOptions = $templateOptions; + return $this; } /** * Get mail transport * - * @return \Magento\Framework\Mail\TransportInterface + * @return TransportInterface * @throws LocalizedException */ public function getTransport() @@ -278,7 +348,7 @@ public function getTransport() */ protected function reset() { - $this->message = $this->messageFactory->create(); + $this->messageData = []; $this->templateIdentifier = null; $this->templateVars = null; $this->templateOptions = null; @@ -288,7 +358,7 @@ protected function reset() /** * Get template * - * @return \Magento\Framework\Mail\TemplateInterface + * @return TemplateInterface */ protected function getTemplate() { @@ -306,14 +376,14 @@ protected function getTemplate() protected function prepareMessage() { $template = $this->getTemplate(); - $body = $template->processTemplate(); + $content = $template->processTemplate(); switch ($template->getType()) { case TemplateTypesInterface::TYPE_TEXT: - $this->message->setBodyText($body); + $part['type'] = MimeInterface::TYPE_TEXT; break; case TemplateTypesInterface::TYPE_HTML: - $this->message->setBodyHtml($body); + $part['type'] = MimeInterface::TYPE_HTML; break; default: @@ -321,7 +391,42 @@ protected function prepareMessage() new Phrase('Unknown template type') ); } - $this->message->setSubject(html_entity_decode($template->getSubject(), ENT_QUOTES)); + $mimePart = $this->mimePartInterfaceFactory->create(['content' => $content]); + $this->messageData['body'] = $this->mimeMessageInterfaceFactory->create( + ['parts' => [$mimePart]] + ); + + $this->messageData['subject'] = html_entity_decode( + (string)$template->getSubject(), + ENT_QUOTES + ); + $this->message = $this->emailMessageInterfaceFactory->create($this->messageData); + return $this; } + + /** + * Handles possible incoming types of email (string or array) + * + * @param string $addressType + * @param string|array $email + * @param string|null $name + * + * @return void + * @throws InvalidArgumentException + */ + private function addAddressByType(string $addressType, $email, ?string $name = null): void + { + if (is_string($email)) { + $this->messageData[$addressType][] = $this->addressConverter->convert($email, $name); + return; + } + $convertedAddressArray = $this->addressConverter->convertMany($email); + if (isset($this->messageData[$addressType])) { + $this->messageData[$addressType] = array_merge( + $this->messageData[$addressType], + $convertedAddressArray + ); + } + } } diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php index c9781281d353..74f40e76353f 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php @@ -3,50 +3,67 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Mail\Test\Unit\Template; use Magento\Framework\App\TemplateTypesInterface; -use Magento\Framework\Mail\MessageInterface; +use Magento\Framework\Mail\EmailMessageInterface; +use Magento\Framework\Mail\EmailMessageInterfaceFactory; +use Magento\Framework\Mail\Message; use Magento\Framework\Mail\MessageInterfaceFactory; +use Magento\Framework\Mail\MimePartInterface; +use Magento\Framework\Mail\MimePartInterfaceFactory; +use Magento\Framework\Mail\Template\FactoryInterface; +use Magento\Framework\Mail\Template\SenderResolverInterface; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TransportInterface; +use Magento\Framework\Mail\TransportInterfaceFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** + * Class TransportBuilderTest + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class TransportBuilderTest extends \PHPUnit\Framework\TestCase +class TransportBuilderTest extends TestCase { /** * @var string */ - protected $builderClassName = \Magento\Framework\Mail\Template\TransportBuilder::class; + protected $builderClassName = TransportBuilder::class; /** - * @var \Magento\Framework\Mail\Template\TransportBuilder + * @var TransportBuilder */ protected $builder; /** - * @var \Magento\Framework\Mail\Template\FactoryInterface | \PHPUnit_Framework_MockObject_MockObject + * @var FactoryInterface | \PHPUnit_Framework_MockObject_MockObject */ protected $templateFactoryMock; /** - * @var \Magento\Framework\Mail\Message | \PHPUnit_Framework_MockObject_MockObject + * @var Message | \PHPUnit_Framework_MockObject_MockObject */ protected $messageMock; /** - * @var \Magento\Framework\ObjectManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var ObjectManagerInterface | \PHPUnit_Framework_MockObject_MockObject */ protected $objectManagerMock; /** - * @var \Magento\Framework\Mail\Template\SenderResolverInterface | \PHPUnit_Framework_MockObject_MockObject + * @var SenderResolverInterface | \PHPUnit_Framework_MockObject_MockObject */ protected $senderResolverMock; /** - * @var \Magento\Framework\Mail\MessageInterfaceFactory| \PHPUnit_Framework_MockObject_MockObject + * @var MessageInterfaceFactory| \PHPUnit_Framework_MockObject_MockObject */ private $messageFactoryMock; @@ -55,26 +72,39 @@ class TransportBuilderTest extends \PHPUnit\Framework\TestCase */ protected $mailTransportFactoryMock; + /** + * @var MimePartInterfaceFactory|MockObject + */ + private $mimePartFactoryMock; + + /** + * @var EmailMessageInterfaceFactory|MockObject + */ + private $emailMessageInterfaceFactoryMock; + /** * @return void */ protected function setUp() { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->templateFactoryMock = $this->createMock(\Magento\Framework\Mail\Template\FactoryInterface::class); - $this->messageMock = $this->createMock(\Magento\Framework\Mail\Message::class); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->senderResolverMock = $this->createMock(\Magento\Framework\Mail\Template\SenderResolverInterface::class); + $objectManagerHelper = new ObjectManager($this); + $this->templateFactoryMock = $this->createMock(FactoryInterface::class); + $this->messageMock = $this->createMock(Message::class); + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $this->senderResolverMock = $this->createMock(SenderResolverInterface::class); $this->mailTransportFactoryMock = $this->getMockBuilder( - \Magento\Framework\Mail\TransportInterfaceFactory::class + TransportInterfaceFactory::class )->disableOriginalConstructor() ->setMethods(['create']) ->getMockForAbstractClass(); - $this->messageFactoryMock = $this->getMockBuilder(\Magento\Framework\Mail\MessageInterfaceFactory::class) + $this->messageFactoryMock = $this->getMockBuilder(MessageInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMockForAbstractClass(); - $this->messageFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($this->messageMock); + + $this->emailMessageInterfaceFactoryMock = $this->createMock(EmailMessageInterfaceFactory::class); + $this->mimePartFactoryMock = $this->createMock(MimePartInterfaceFactory::class); + $this->builder = $objectManagerHelper->getObject( $this->builderClassName, [ @@ -83,7 +113,9 @@ protected function setUp() 'objectManager' => $this->objectManagerMock, 'senderResolver' => $this->senderResolverMock, 'mailTransportFactory' => $this->mailTransportFactoryMock, - 'messageFactory' => $this->messageFactoryMock + 'messageFactory' => $this->messageFactoryMock, + 'emailMessageInterfaceFactory' => $this->emailMessageInterfaceFactoryMock, + 'mimePartInterfaceFactory' => $this->mimePartFactoryMock, ] ); } @@ -91,19 +123,32 @@ protected function setUp() /** * @dataProvider getTransportDataProvider * @param int $templateType - * @param string $messageType * @param string $bodyText * @param string $templateNamespace * @return void */ - public function testGetTransport($templateType, $messageType, $bodyText, $templateNamespace) + public function testGetTransport($templateType, $bodyText, $templateNamespace) { $this->builder->setTemplateModel($templateNamespace); $vars = ['reason' => 'Reason', 'customer' => 'Customer']; $options = ['area' => 'frontend', 'store' => 1]; - $template = $this->createMock(\Magento\Framework\Mail\TemplateInterface::class); + /** @var MimePartInterface|MockObject $mimePartMock */ + $mimePartMock = $this->createMock(MimePartInterface::class); + + $this->mimePartFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($mimePartMock); + + /** @var EmailMessageInterface|MockObject $emailMessage */ + $emailMessage = $this->createMock(EmailMessageInterface::class); + + $this->emailMessageInterfaceFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($emailMessage); + + $template = $this->createMock(TemplateInterface::class); $template->expects($this->once())->method('setVars')->with($this->equalTo($vars))->willReturnSelf(); $template->expects($this->once())->method('setOptions')->with($this->equalTo($options))->willReturnSelf(); $template->expects($this->once())->method('getSubject')->willReturn('Email Subject'); @@ -115,32 +160,16 @@ public function testGetTransport($templateType, $messageType, $bodyText, $templa ->with($this->equalTo('identifier'), $this->equalTo($templateNamespace)) ->willReturn($template); - $this->messageMock->expects($this->once()) - ->method('setSubject') - ->with($this->equalTo('Email Subject')) - ->willReturnSelf(); - - $this->messageMock->expects($this->exactly((int)($messageType == MessageInterface::TYPE_TEXT))) - ->method('setBodyText') - ->with($this->equalTo($bodyText)) - ->willReturnSelf(); - - $this->messageMock->expects($this->exactly((int)($messageType == MessageInterface::TYPE_HTML))) - ->method('setBodyHtml') - ->with($this->equalTo($bodyText)) - ->willReturnSelf(); - - $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); + $transport = $this->createMock(TransportInterface::class); $this->mailTransportFactoryMock->expects($this->at(0)) ->method('create') - ->with($this->equalTo(['message' => $this->messageMock])) ->willReturn($transport); - $this->messageFactoryMock->expects($this->once())->method('create')->willReturn($transport); - $this->builder->setTemplateIdentifier('identifier')->setTemplateVars($vars)->setTemplateOptions($options); - $this->assertInstanceOf(\Magento\Framework\Mail\TransportInterface::class, $this->builder->getTransport()); + + $result = $this->builder->getTransport(); + $this->assertInstanceOf(TransportInterface::class, $result); } /** @@ -156,11 +185,10 @@ public function testGetTransportWithException() $vars = ['reason' => 'Reason', 'customer' => 'Customer']; $options = ['area' => 'frontend', 'store' => 1]; - $template = $this->createMock(\Magento\Framework\Mail\TemplateInterface::class); + $template = $this->createMock(TemplateInterface::class); $template->expects($this->once())->method('setVars')->with($this->equalTo($vars))->willReturnSelf(); $template->expects($this->once())->method('setOptions')->with($this->equalTo($options))->willReturnSelf(); $template->expects($this->once())->method('getType')->willReturn('Unknown'); - $this->messageFactoryMock->expects($this->once())->method('create'); $this->templateFactoryMock->expects($this->once()) ->method('get') ->with($this->equalTo('identifier'), $this->equalTo('Test\Namespace\Template')) @@ -168,7 +196,7 @@ public function testGetTransportWithException() $this->builder->setTemplateIdentifier('identifier')->setTemplateVars($vars)->setTemplateOptions($options); - $this->assertInstanceOf(\Magento\Framework\Mail\TransportInterface::class, $this->builder->getTransport()); + $this->assertInstanceOf(TransportInterface::class, $this->builder->getTransport()); } /** @@ -179,13 +207,11 @@ public function getTransportDataProvider() return [ [ TemplateTypesInterface::TYPE_TEXT, - MessageInterface::TYPE_TEXT, 'Plain text', null ], [ TemplateTypesInterface::TYPE_HTML, - MessageInterface::TYPE_HTML, '<h1>Html message</h1>', 'Test\Namespace\Template' ] @@ -203,60 +229,7 @@ public function testSetFromByScope() ->method('resolve') ->with($sender, $scopeId) ->willReturn($sender); - $this->messageMock->expects($this->once()) - ->method('setFromAddress') - ->with($sender['email'], $sender['name']) - ->willReturnSelf(); $this->builder->setFromByScope($sender, $scopeId); } - - /** - * @return void - */ - public function testSetCc() - { - $this->messageMock->expects($this->once())->method('addCc')->with('cc@example.com')->willReturnSelf(); - - $this->builder->addCc('cc@example.com'); - } - - /** - * @return void - */ - public function testAddTo() - { - $this->messageMock->expects($this->once()) - ->method('addTo') - ->with('to@example.com', 'recipient') - ->willReturnSelf(); - - $this->builder->addTo('to@example.com', 'recipient'); - } - - /** - * @return void - */ - public function testAddBcc() - { - $this->messageMock->expects($this->once()) - ->method('addBcc') - ->with('bcc@example.com') - ->willReturnSelf(); - - $this->builder->addBcc('bcc@example.com'); - } - - /** - * @return void - */ - public function testSetReplyTo() - { - $this->messageMock->expects($this->once()) - ->method('setReplyTo') - ->with('replyTo@example.com', 'replyName') - ->willReturnSelf(); - - $this->builder->setReplyTo('replyTo@example.com', 'replyName'); - } } diff --git a/lib/internal/Magento/Framework/Mail/TransportInterfaceFactory.php b/lib/internal/Magento/Framework/Mail/TransportInterfaceFactory.php index db86275954cb..5448df4e16ca 100644 --- a/lib/internal/Magento/Framework/Mail/TransportInterfaceFactory.php +++ b/lib/internal/Magento/Framework/Mail/TransportInterfaceFactory.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Mail; +use Magento\Framework\ObjectManagerInterface; + /** * Factory class for \Magento\Framework\Mail\TransportInterface */ @@ -14,39 +17,39 @@ class TransportInterfaceFactory /** * Object Manager instance * - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ - protected $_objectManager = null; + private $objectManager; /** * Instance name to create * * @var string */ - protected $_instanceName = null; + private $instanceName; /** * Factory constructor * - * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param string $instanceName */ public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, - $instanceName = \Magento\Framework\Mail\TransportInterface::class + ObjectManagerInterface $objectManager, + $instanceName = TransportInterface::class ) { - $this->_objectManager = $objectManager; - $this->_instanceName = $instanceName; + $this->objectManager = $objectManager; + $this->instanceName = $instanceName; } /** * Create class instance with specified parameters * * @param array $data - * @return \Magento\Framework\Mail\TransportInterface + * @return TransportInterface */ - public function create(array $data = []) + public function create(array $data = []): TransportInterface { - return $this->_objectManager->create($this->_instanceName, $data); + return $this->objectManager->create($this->instanceName, $data); } } diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index 7b45765fdefe..a1e21e22a156 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -293,6 +293,10 @@ public function escapeUrlDataProvider(): array 'data' => "http://exam\r\nple.com/search?term=this+%26+that&view=list", 'expected' => "http://example.com/search?term=this+%26+that&view=list", ], + [ + 'data' => "http://example.com/", + 'expected' => "http://example.com/", + ], ]; } diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index ed7e9f1cd1a1..c42323a2ecc0 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -37,6 +37,8 @@ "zendframework/zend-stdlib": "^3.2.1", "zendframework/zend-uri": "^2.5.1", "zendframework/zend-validator": "^2.6.0", + "zendframework/zend-mail": "^2.9.0", + "zendframework/zend-mime": "^2.5.0", "guzzlehttp/guzzle": "^6.3.3" }, "archive": { diff --git a/lib/web/jquery/compat.js b/lib/web/jquery/compat.js new file mode 100644 index 000000000000..518f218599fd --- /dev/null +++ b/lib/web/jquery/compat.js @@ -0,0 +1,49 @@ +// Import every plugin under the sun. Bad for performance, +// but prevents the store from breaking in situations +// where a dependency was missed during the migration from +// a monolith build of jQueryUI to a modular one + +define([ + 'jquery-ui-modules/core', + 'jquery-ui-modules/accordion', + 'jquery-ui-modules/autocomplete', + 'jquery-ui-modules/button', + 'jquery-ui-modules/datepicker', + 'jquery-ui-modules/dialog', + 'jquery-ui-modules/draggable', + 'jquery-ui-modules/droppable', + 'jquery-ui-modules/effect-blind', + 'jquery-ui-modules/effect-bounce', + 'jquery-ui-modules/effect-clip', + 'jquery-ui-modules/effect-drop', + 'jquery-ui-modules/effect-explode', + 'jquery-ui-modules/effect-fade', + 'jquery-ui-modules/effect-fold', + 'jquery-ui-modules/effect-highlight', + 'jquery-ui-modules/effect-scale', + 'jquery-ui-modules/effect-pulsate', + 'jquery-ui-modules/effect-shake', + 'jquery-ui-modules/effect-slide', + 'jquery-ui-modules/effect-transfer', + 'jquery-ui-modules/effect', + 'jquery-ui-modules/menu', + 'jquery-ui-modules/mouse', + 'jquery-ui-modules/position', + 'jquery-ui-modules/progressbar', + 'jquery-ui-modules/resizable', + 'jquery-ui-modules/selectable', + 'jquery-ui-modules/slider', + 'jquery-ui-modules/sortable', + 'jquery-ui-modules/spinner', + 'jquery-ui-modules/tabs', + 'jquery-ui-modules/timepicker', + 'jquery-ui-modules/tooltip', + 'jquery-ui-modules/widget' +], function() { + console.warn( + 'Fallback to JQueryUI Compat activated. ' + + 'Your store is missing a dependency for a ' + + 'jQueryUI widget. Identifying and addressing the dependency ' + + 'will drastically improve the performance of your site.' + ) +}); \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index 8b73c9fbc45c..676f8aa1e805 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -18,7 +18,7 @@ // Register as an anonymous AMD module: define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', 'jquery/fileUploader/jquery.iframe-transport' ], factory); } else { diff --git a/lib/web/jquery/ui-modules/accordion.js b/lib/web/jquery/ui-modules/accordion.js new file mode 100644 index 000000000000..de3bf49026fe --- /dev/null +++ b/lib/web/jquery/ui-modules/accordion.js @@ -0,0 +1,570 @@ +/*! + * jQuery UI Accordion - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/accordion/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/widget' +], function ($, undefined) { + + var uid = 0, + hideProps = {}, + showProps = {}; + + hideProps.height = hideProps.paddingTop = hideProps.paddingBottom = + hideProps.borderTopWidth = hideProps.borderBottomWidth = "hide"; + showProps.height = showProps.paddingTop = showProps.paddingBottom = + showProps.borderTopWidth = showProps.borderBottomWidth = "show"; + + $.widget("ui.accordion", { + version: "1.10.4", + options: { + active: 0, + animate: {}, + collapsible: false, + event: "click", + header: "> li > :first-child,> :not(li):even", + heightStyle: "auto", + icons: { + activeHeader: "ui-icon-triangle-1-s", + header: "ui-icon-triangle-1-e" + }, + + // callbacks + activate: null, + beforeActivate: null + }, + + _create: function () { + var options = this.options; + this.prevShow = this.prevHide = $(); + this.element.addClass("ui-accordion ui-widget ui-helper-reset") + // ARIA + .attr("role", "tablist"); + + // don't allow collapsible: false and active: false / null + if (!options.collapsible && (options.active === false || options.active == null)) { + options.active = 0; + } + + this._processPanels(); + // handle negative values + if (options.active < 0) { + options.active += this.headers.length; + } + this._refresh(); + }, + + _getCreateEventData: function () { + return { + header: this.active, + panel: !this.active.length ? $() : this.active.next(), + content: !this.active.length ? $() : this.active.next() + }; + }, + + _createIcons: function () { + var icons = this.options.icons; + if (icons) { + $("<span>") + .addClass("ui-accordion-header-icon ui-icon " + icons.header) + .prependTo(this.headers); + this.active.children(".ui-accordion-header-icon") + .removeClass(icons.header) + .addClass(icons.activeHeader); + this.headers.addClass("ui-accordion-icons"); + } + }, + + _destroyIcons: function () { + this.headers + .removeClass("ui-accordion-icons") + .children(".ui-accordion-header-icon") + .remove(); + }, + + _destroy: function () { + var contents; + + // clean up main element + this.element + .removeClass("ui-accordion ui-widget ui-helper-reset") + .removeAttr("role"); + + // clean up headers + this.headers + .removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top") + .removeAttr("role") + .removeAttr("aria-expanded") + .removeAttr("aria-selected") + .removeAttr("aria-controls") + .removeAttr("tabIndex") + .each(function () { + if (/^ui-accordion/.test(this.id)) { + this.removeAttribute("id"); + } + }); + this._destroyIcons(); + + // clean up content panels + contents = this.headers.next() + .css("display", "") + .removeAttr("role") + .removeAttr("aria-hidden") + .removeAttr("aria-labelledby") + .removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled") + .each(function () { + if (/^ui-accordion/.test(this.id)) { + this.removeAttribute("id"); + } + }); + if (this.options.heightStyle !== "content") { + contents.css("height", ""); + } + }, + + _setOption: function (key, value) { + if (key === "active") { + // _activate() will handle invalid values and update this.options + this._activate(value); + return; + } + + if (key === "event") { + if (this.options.event) { + this._off(this.headers, this.options.event); + } + this._setupEvents(value); + } + + this._super(key, value); + + // setting collapsible: false while collapsed; open first panel + if (key === "collapsible" && !value && this.options.active === false) { + this._activate(0); + } + + if (key === "icons") { + this._destroyIcons(); + if (value) { + this._createIcons(); + } + } + + // #5332 - opacity doesn't cascade to positioned elements in IE + // so we need to add the disabled class to the headers and panels + if (key === "disabled") { + this.headers.add(this.headers.next()) + .toggleClass("ui-state-disabled", !!value); + } + }, + + _keydown: function (event) { + if (event.altKey || event.ctrlKey) { + return; + } + + var keyCode = $.ui.keyCode, + length = this.headers.length, + currentIndex = this.headers.index(event.target), + toFocus = false; + + switch (event.keyCode) { + case keyCode.RIGHT: + case keyCode.DOWN: + toFocus = this.headers[(currentIndex + 1) % length]; + break; + case keyCode.LEFT: + case keyCode.UP: + toFocus = this.headers[(currentIndex - 1 + length) % length]; + break; + case keyCode.SPACE: + case keyCode.ENTER: + this._eventHandler(event); + break; + case keyCode.HOME: + toFocus = this.headers[0]; + break; + case keyCode.END: + toFocus = this.headers[length - 1]; + break; + } + + if (toFocus) { + $(event.target).attr("tabIndex", -1); + $(toFocus).attr("tabIndex", 0); + toFocus.focus(); + event.preventDefault(); + } + }, + + _panelKeyDown: function (event) { + if (event.keyCode === $.ui.keyCode.UP && event.ctrlKey) { + $(event.currentTarget).prev().focus(); + } + }, + + refresh: function () { + var options = this.options; + this._processPanels(); + + // was collapsed or no panel + if ((options.active === false && options.collapsible === true) || !this.headers.length) { + options.active = false; + this.active = $(); + // active false only when collapsible is true + } else if (options.active === false) { + this._activate(0); + // was active, but active panel is gone + } else if (this.active.length && !$.contains(this.element[0], this.active[0])) { + // all remaining panel are disabled + if (this.headers.length === this.headers.find(".ui-state-disabled").length) { + options.active = false; + this.active = $(); + // activate previous panel + } else { + this._activate(Math.max(0, options.active - 1)); + } + // was active, active panel still exists + } else { + // make sure active index is correct + options.active = this.headers.index(this.active); + } + + this._destroyIcons(); + + this._refresh(); + }, + + _processPanels: function () { + this.headers = this.element.find(this.options.header) + .addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"); + + this.headers.next() + .addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom") + .filter(":not(.ui-accordion-content-active)") + .hide(); + }, + + _refresh: function () { + var maxHeight, + options = this.options, + heightStyle = options.heightStyle, + parent = this.element.parent(), + accordionId = this.accordionId = "ui-accordion-" + + (this.element.attr("id") || ++uid); + + this.active = this._findActive(options.active) + .addClass("ui-accordion-header-active ui-state-active ui-corner-top") + .removeClass("ui-corner-all"); + this.active.next() + .addClass("ui-accordion-content-active") + .show(); + + this.headers + .attr("role", "tab") + .each(function (i) { + var header = $(this), + headerId = header.attr("id"), + panel = header.next(), + panelId = panel.attr("id"); + if (!headerId) { + headerId = accordionId + "-header-" + i; + header.attr("id", headerId); + } + if (!panelId) { + panelId = accordionId + "-panel-" + i; + panel.attr("id", panelId); + } + header.attr("aria-controls", panelId); + panel.attr("aria-labelledby", headerId); + }) + .next() + .attr("role", "tabpanel"); + + this.headers + .not(this.active) + .attr({ + "aria-selected": "false", + "aria-expanded": "false", + tabIndex: -1 + }) + .next() + .attr({ + "aria-hidden": "true" + }) + .hide(); + + // make sure at least one header is in the tab order + if (!this.active.length) { + this.headers.eq(0).attr("tabIndex", 0); + } else { + this.active.attr({ + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + }) + .next() + .attr({ + "aria-hidden": "false" + }); + } + + this._createIcons(); + + this._setupEvents(options.event); + + if (heightStyle === "fill") { + maxHeight = parent.height(); + this.element.siblings(":visible").each(function () { + var elem = $(this), + position = elem.css("position"); + + if (position === "absolute" || position === "fixed") { + return; + } + maxHeight -= elem.outerHeight(true); + }); + + this.headers.each(function () { + maxHeight -= $(this).outerHeight(true); + }); + + this.headers.next() + .each(function () { + $(this).height(Math.max(0, maxHeight - + $(this).innerHeight() + $(this).height())); + }) + .css("overflow", "auto"); + } else if (heightStyle === "auto") { + maxHeight = 0; + this.headers.next() + .each(function () { + maxHeight = Math.max(maxHeight, $(this).css("height", "").height()); + }) + .height(maxHeight); + } + }, + + _activate: function (index) { + var active = this._findActive(index)[0]; + + // trying to activate the already active panel + if (active === this.active[0]) { + return; + } + + // trying to collapse, simulate a click on the currently active header + active = active || this.active[0]; + + this._eventHandler({ + target: active, + currentTarget: active, + preventDefault: $.noop + }); + }, + + _findActive: function (selector) { + return typeof selector === "number" ? this.headers.eq(selector) : $(); + }, + + _setupEvents: function (event) { + var events = { + keydown: "_keydown" + }; + if (event) { + $.each(event.split(" "), function (index, eventName) { + events[eventName] = "_eventHandler"; + }); + } + + this._off(this.headers.add(this.headers.next())); + this._on(this.headers, events); + this._on(this.headers.next(), {keydown: "_panelKeyDown"}); + this._hoverable(this.headers); + this._focusable(this.headers); + }, + + _eventHandler: function (event) { + var options = this.options, + active = this.active, + clicked = $(event.currentTarget), + clickedIsActive = clicked[0] === active[0], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : clicked.next(), + toHide = active.next(), + eventData = { + oldHeader: active, + oldPanel: toHide, + newHeader: collapsing ? $() : clicked, + newPanel: toShow + }; + + event.preventDefault(); + + if ( + // click on active header, but not collapsible + (clickedIsActive && !options.collapsible) || + // allow canceling activation + (this._trigger("beforeActivate", event, eventData) === false)) { + return; + } + + options.active = collapsing ? false : this.headers.index(clicked); + + // when the call to ._toggle() comes after the class changes + // it causes a very odd bug in IE 8 (see #6720) + this.active = clickedIsActive ? $() : clicked; + this._toggle(eventData); + + // switch classes + // corner classes on the previously active header stay after the animation + active.removeClass("ui-accordion-header-active ui-state-active"); + if (options.icons) { + active.children(".ui-accordion-header-icon") + .removeClass(options.icons.activeHeader) + .addClass(options.icons.header); + } + + if (!clickedIsActive) { + clicked + .removeClass("ui-corner-all") + .addClass("ui-accordion-header-active ui-state-active ui-corner-top"); + if (options.icons) { + clicked.children(".ui-accordion-header-icon") + .removeClass(options.icons.header) + .addClass(options.icons.activeHeader); + } + + clicked + .next() + .addClass("ui-accordion-content-active"); + } + }, + + _toggle: function (data) { + var toShow = data.newPanel, + toHide = this.prevShow.length ? this.prevShow : data.oldPanel; + + // handle activating a panel during the animation for another activation + this.prevShow.add(this.prevHide).stop(true, true); + this.prevShow = toShow; + this.prevHide = toHide; + + if (this.options.animate) { + this._animate(toShow, toHide, data); + } else { + toHide.hide(); + toShow.show(); + this._toggleComplete(data); + } + + toHide.attr({ + "aria-hidden": "true" + }); + toHide.prev().attr("aria-selected", "false"); + // if we're switching panels, remove the old header from the tab order + // if we're opening from collapsed state, remove the previous header from the tab order + // if we're collapsing, then keep the collapsing header in the tab order + if (toShow.length && toHide.length) { + toHide.prev().attr({ + "tabIndex": -1, + "aria-expanded": "false" + }); + } else if (toShow.length) { + this.headers.filter(function () { + return $(this).attr("tabIndex") === 0; + }) + .attr("tabIndex", -1); + } + + toShow + .attr("aria-hidden", "false") + .prev() + .attr({ + "aria-selected": "true", + tabIndex: 0, + "aria-expanded": "true" + }); + }, + + _animate: function (toShow, toHide, data) { + var total, easing, duration, + that = this, + adjust = 0, + down = toShow.length && + (!toHide.length || (toShow.index() < toHide.index())), + animate = this.options.animate || {}, + options = down && animate.down || animate, + complete = function () { + that._toggleComplete(data); + }; + + if (typeof options === "number") { + duration = options; + } + if (typeof options === "string") { + easing = options; + } + // fall back from options to animation in case of partial down settings + easing = easing || options.easing || animate.easing; + duration = duration || options.duration || animate.duration; + + if (!toHide.length) { + return toShow.animate(showProps, duration, easing, complete); + } + if (!toShow.length) { + return toHide.animate(hideProps, duration, easing, complete); + } + + total = toShow.show().outerHeight(); + toHide.animate(hideProps, { + duration: duration, + easing: easing, + step: function (now, fx) { + fx.now = Math.round(now); + } + }); + toShow + .hide() + .animate(showProps, { + duration: duration, + easing: easing, + complete: complete, + step: function (now, fx) { + fx.now = Math.round(now); + if (fx.prop !== "height") { + adjust += fx.now; + } else if (that.options.heightStyle !== "content") { + fx.now = Math.round(total - toHide.outerHeight() - adjust); + adjust = 0; + } + } + }); + }, + + _toggleComplete: function (data) { + var toHide = data.oldPanel; + + toHide + .removeClass("ui-accordion-content-active") + .prev() + .removeClass("ui-corner-top") + .addClass("ui-corner-all"); + + // Work around for rendering bug in IE (#5421) + if (toHide.length) { + toHide.parent()[0].className = toHide.parent()[0].className; + } + this._trigger("activate", null, data); + } + }); +}); diff --git a/lib/web/jquery/ui-modules/autocomplete.js b/lib/web/jquery/ui-modules/autocomplete.js new file mode 100644 index 000000000000..62d614c37ca2 --- /dev/null +++ b/lib/web/jquery/ui-modules/autocomplete.js @@ -0,0 +1,603 @@ +/*! + * jQuery UI Autocomplete - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/autocomplete/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/menu' +], function ($, undefined) { + + $.widget("ui.autocomplete", { + version: "1.10.4", + defaultElement: "<input>", + options: { + appendTo: null, + autoFocus: false, + delay: 300, + minLength: 1, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + source: null, + + // callbacks + change: null, + close: null, + focus: null, + open: null, + response: null, + search: null, + select: null + }, + + requestIndex: 0, + pending: 0, + + _create: function () { + // Some browsers only repeat keydown events, not keypress events, + // so we use the suppressKeyPress flag to determine if we've already + // handled the keydown event. #7269 + // Unfortunately the code for & in keypress is the same as the up arrow, + // so we use the suppressKeyPressRepeat flag to avoid handling keypress + // events when we know the keydown event was used to modify the + // search term. #7799 + var suppressKeyPress, suppressKeyPressRepeat, suppressInput, + nodeName = this.element[0].nodeName.toLowerCase(), + isTextarea = nodeName === "textarea", + isInput = nodeName === "input"; + + this.isMultiLine = + // Textareas are always multi-line + isTextarea ? true : + // Inputs are always single-line, even if inside a contentEditable element + // IE also treats inputs as contentEditable + isInput ? false : + // All other element types are determined by whether or not they're contentEditable + this.element.prop("isContentEditable"); + + this.valueMethod = this.element[isTextarea || isInput ? "val" : "text"]; + this.isNewMenu = true; + + this.element + .addClass("ui-autocomplete-input") + .attr("autocomplete", "off"); + + this._on(this.element, { + keydown: function (event) { + if (this.element.prop("readOnly")) { + suppressKeyPress = true; + suppressInput = true; + suppressKeyPressRepeat = true; + return; + } + + suppressKeyPress = false; + suppressInput = false; + suppressKeyPressRepeat = false; + var keyCode = $.ui.keyCode; + switch (event.keyCode) { + case keyCode.PAGE_UP: + suppressKeyPress = true; + this._move("previousPage", event); + break; + case keyCode.PAGE_DOWN: + suppressKeyPress = true; + this._move("nextPage", event); + break; + case keyCode.UP: + suppressKeyPress = true; + this._keyEvent("previous", event); + break; + case keyCode.DOWN: + suppressKeyPress = true; + this._keyEvent("next", event); + break; + case keyCode.ENTER: + case keyCode.NUMPAD_ENTER: + // when menu is open and has focus + if (this.menu.active) { + // #6055 - Opera still allows the keypress to occur + // which causes forms to submit + suppressKeyPress = true; + event.preventDefault(); + this.menu.select(event); + } + break; + case keyCode.TAB: + if (this.menu.active) { + this.menu.select(event); + } + break; + case keyCode.ESCAPE: + if (this.menu.element.is(":visible")) { + this._value(this.term); + this.close(event); + // Different browsers have different default behavior for escape + // Single press can mean undo or clear + // Double press in IE means clear the whole form + event.preventDefault(); + } + break; + default: + suppressKeyPressRepeat = true; + // search timeout should be triggered before the input value is changed + this._searchTimeout(event); + break; + } + }, + keypress: function (event) { + if (suppressKeyPress) { + suppressKeyPress = false; + if (!this.isMultiLine || this.menu.element.is(":visible")) { + event.preventDefault(); + } + return; + } + if (suppressKeyPressRepeat) { + return; + } + + // replicate some key handlers to allow them to repeat in Firefox and Opera + var keyCode = $.ui.keyCode; + switch (event.keyCode) { + case keyCode.PAGE_UP: + this._move("previousPage", event); + break; + case keyCode.PAGE_DOWN: + this._move("nextPage", event); + break; + case keyCode.UP: + this._keyEvent("previous", event); + break; + case keyCode.DOWN: + this._keyEvent("next", event); + break; + } + }, + input: function (event) { + if (suppressInput) { + suppressInput = false; + event.preventDefault(); + return; + } + this._searchTimeout(event); + }, + focus: function () { + this.selectedItem = null; + this.previous = this._value(); + }, + blur: function (event) { + if (this.cancelBlur) { + delete this.cancelBlur; + return; + } + + clearTimeout(this.searching); + this.close(event); + this._change(event); + } + }); + + this._initSource(); + this.menu = $("<ul>") + .addClass("ui-autocomplete ui-front") + .appendTo(this._appendTo()) + .menu({ + // disable ARIA support, the live region takes care of that + role: null + }) + .hide() + .data("ui-menu"); + + this._on(this.menu.element, { + mousedown: function (event) { + // prevent moving focus out of the text field + event.preventDefault(); + + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + this.cancelBlur = true; + this._delay(function () { + delete this.cancelBlur; + }); + + // clicking on the scrollbar causes focus to shift to the body + // but we can't detect a mouseup or a click immediately afterward + // so we have to track the next mousedown and close the menu if + // the user clicks somewhere outside of the autocomplete + var menuElement = this.menu.element[0]; + if (!$(event.target).closest(".ui-menu-item").length) { + this._delay(function () { + var that = this; + this.document.one("mousedown", function (event) { + if (event.target !== that.element[0] && + event.target !== menuElement && + !$.contains(menuElement, event.target)) { + that.close(); + } + }); + }); + } + }, + menufocus: function (event, ui) { + // support: Firefox + // Prevent accidental activation of menu items in Firefox (#7024 #9118) + if (this.isNewMenu) { + this.isNewMenu = false; + if (event.originalEvent && /^mouse/.test(event.originalEvent.type)) { + this.menu.blur(); + + this.document.one("mousemove", function () { + $(event.target).trigger(event.originalEvent); + }); + + return; + } + } + + var item = ui.item.data("ui-autocomplete-item"); + if (false !== this._trigger("focus", event, {item: item})) { + // use value to match what will end up in the input, if it was a key event + if (event.originalEvent && /^key/.test(event.originalEvent.type)) { + this._value(item.value); + } + } else { + // Normally the input is populated with the item's value as the + // menu is navigated, causing screen readers to notice a change and + // announce the item. Since the focus event was canceled, this doesn't + // happen, so we update the live region so that screen readers can + // still notice the change and announce it. + this.liveRegion.text(item.value); + } + }, + menuselect: function (event, ui) { + var item = ui.item.data("ui-autocomplete-item"), + previous = this.previous; + + // only trigger when focus was lost (click on menu) + if (this.element[0] !== this.document[0].activeElement) { + this.element.focus(); + this.previous = previous; + // #6109 - IE triggers two focus events and the second + // is asynchronous, so we need to reset the previous + // term synchronously and asynchronously :-( + this._delay(function () { + this.previous = previous; + this.selectedItem = item; + }); + } + + if (false !== this._trigger("select", event, {item: item})) { + this._value(item.value); + } + // reset the term after the select event + // this allows custom select handling to work properly + this.term = this._value(); + + this.close(event); + this.selectedItem = item; + } + }); + + this.liveRegion = $("<span>", { + role: "status", + "aria-live": "polite" + }) + .addClass("ui-helper-hidden-accessible") + .insertBefore(this.element); + + // turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on(this.window, { + beforeunload: function () { + this.element.removeAttr("autocomplete"); + } + }); + }, + + _destroy: function () { + clearTimeout(this.searching); + this.element + .removeClass("ui-autocomplete-input") + .removeAttr("autocomplete"); + this.menu.element.remove(); + this.liveRegion.remove(); + }, + + _setOption: function (key, value) { + this._super(key, value); + if (key === "source") { + this._initSource(); + } + if (key === "appendTo") { + this.menu.element.appendTo(this._appendTo()); + } + if (key === "disabled" && value && this.xhr) { + this.xhr.abort(); + } + }, + + _appendTo: function () { + var element = this.options.appendTo; + + if (element) { + element = element.jquery || element.nodeType ? + $(element) : + this.document.find(element).eq(0); + } + + if (!element) { + element = this.element.closest(".ui-front"); + } + + if (!element.length) { + element = this.document[0].body; + } + + return element; + }, + + _initSource: function () { + var array, url, + that = this; + if ($.isArray(this.options.source)) { + array = this.options.source; + this.source = function (request, response) { + response($.ui.autocomplete.filter(array, request.term)); + }; + } else if (typeof this.options.source === "string") { + url = this.options.source; + this.source = function (request, response) { + if (that.xhr) { + that.xhr.abort(); + } + that.xhr = $.ajax({ + url: url, + data: request, + dataType: "json", + success: function (data) { + response(data); + }, + error: function () { + response([]); + } + }); + }; + } else { + this.source = this.options.source; + } + }, + + _searchTimeout: function (event) { + clearTimeout(this.searching); + this.searching = this._delay(function () { + // only search if the value has changed + if (this.term !== this._value()) { + this.selectedItem = null; + this.search(null, event); + } + }, this.options.delay); + }, + + search: function (value, event) { + value = value != null ? value : this._value(); + + // always save the actual value, not the one passed as an argument + this.term = this._value(); + + if (value.length < this.options.minLength) { + return this.close(event); + } + + if (this._trigger("search", event) === false) { + return; + } + + return this._search(value); + }, + + _search: function (value) { + this.pending++; + this.element.addClass("ui-autocomplete-loading"); + this.cancelSearch = false; + + this.source({term: value}, this._response()); + }, + + _response: function () { + var index = ++this.requestIndex; + + return $.proxy(function (content) { + if (index === this.requestIndex) { + this.__response(content); + } + + this.pending--; + if (!this.pending) { + this.element.removeClass("ui-autocomplete-loading"); + } + }, this); + }, + + __response: function (content) { + if (content) { + content = this._normalize(content); + } + this._trigger("response", null, {content: content}); + if (!this.options.disabled && content && content.length && !this.cancelSearch) { + this._suggest(content); + this._trigger("open"); + } else { + // use ._close() instead of .close() so we don't cancel future searches + this._close(); + } + }, + + close: function (event) { + this.cancelSearch = true; + this._close(event); + }, + + _close: function (event) { + if (this.menu.element.is(":visible")) { + this.menu.element.hide(); + this.menu.blur(); + this.isNewMenu = true; + this._trigger("close", event); + } + }, + + _change: function (event) { + if (this.previous !== this._value()) { + this._trigger("change", event, {item: this.selectedItem}); + } + }, + + _normalize: function (items) { + // assume all items have the right format when the first item is complete + if (items.length && items[0].label && items[0].value) { + return items; + } + return $.map(items, function (item) { + if (typeof item === "string") { + return { + label: item, + value: item + }; + } + return $.extend({ + label: item.label || item.value, + value: item.value || item.label + }, item); + }); + }, + + _suggest: function (items) { + var ul = this.menu.element.empty(); + this._renderMenu(ul, items); + this.isNewMenu = true; + this.menu.refresh(); + + // size and position menu + ul.show(); + this._resizeMenu(); + ul.position($.extend({ + of: this.element + }, this.options.position)); + + if (this.options.autoFocus) { + this.menu.next(); + } + }, + + _resizeMenu: function () { + var ul = this.menu.element; + ul.outerWidth(Math.max( + // Firefox wraps long text (possibly a rounding bug) + // so we add 1px to avoid the wrapping (#7513) + ul.width("").outerWidth() + 1, + this.element.outerWidth() + )); + }, + + _renderMenu: function (ul, items) { + var that = this; + $.each(items, function (index, item) { + that._renderItemData(ul, item); + }); + }, + + _renderItemData: function (ul, item) { + return this._renderItem(ul, item).data("ui-autocomplete-item", item); + }, + + _renderItem: function (ul, item) { + return $("<li>") + .append($("<a>").text(item.label)) + .appendTo(ul); + }, + + _move: function (direction, event) { + if (!this.menu.element.is(":visible")) { + this.search(null, event); + return; + } + if (this.menu.isFirstItem() && /^previous/.test(direction) || + this.menu.isLastItem() && /^next/.test(direction)) { + this._value(this.term); + this.menu.blur(); + return; + } + this.menu[direction](event); + }, + + widget: function () { + return this.menu.element; + }, + + _value: function () { + return this.valueMethod.apply(this.element, arguments); + }, + + _keyEvent: function (keyEvent, event) { + if (!this.isMultiLine || this.menu.element.is(":visible")) { + this._move(keyEvent, event); + + // prevents moving cursor to beginning/end of the text field in some browsers + event.preventDefault(); + } + } + }); + + $.extend($.ui.autocomplete, { + escapeRegex: function (value) { + return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); + }, + filter: function (array, term) { + var matcher = new RegExp($.ui.autocomplete.escapeRegex(term), "i"); + return $.grep(array, function (value) { + return matcher.test(value.label || value.value || value); + }); + } + }); + + + // live region extension, adding a `messages` option + // NOTE: This is an experimental API. We are still investigating + // a full solution for string manipulation and internationalization. + $.widget("ui.autocomplete", $.ui.autocomplete, { + options: { + messages: { + noResults: "No search results.", + results: function (amount) { + return amount + (amount > 1 ? " results are" : " result is") + + " available, use up and down arrow keys to navigate."; + } + } + }, + + __response: function (content) { + var message; + this._superApply(arguments); + if (this.options.disabled || this.cancelSearch) { + return; + } + if (content && content.length) { + message = this.options.messages.results(content.length); + } else { + message = this.options.messages.noResults; + } + this.liveRegion.text(message); + } + }); +}); diff --git a/lib/web/jquery/ui-modules/button.js b/lib/web/jquery/ui-modules/button.js new file mode 100644 index 000000000000..571fd97d6c9a --- /dev/null +++ b/lib/web/jquery/ui-modules/button.js @@ -0,0 +1,395 @@ +/*! + * jQuery UI Button - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/button/ + */ +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/widget' +], function ($, undefined) { + + var lastActive, + baseClasses = "ui-button ui-widget ui-state-default ui-corner-all", + typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only", + formResetHandler = function () { + var form = $(this); + setTimeout(function () { + form.find(":ui-button").button("refresh"); + }, 1); + }, + radioGroup = function (radio) { + var name = radio.name, + form = radio.form, + radios = $([]); + if (name) { + name = name.replace(/'/g, "\\'"); + if (form) { + radios = $(form).find("[name='" + name + "']"); + } else { + radios = $("[name='" + name + "']", radio.ownerDocument) + .filter(function () { + return !this.form; + }); + } + } + return radios; + }; + + $.widget("ui.button", { + version: "1.10.4", + defaultElement: "<button>", + options: { + disabled: null, + text: true, + label: null, + icons: { + primary: null, + secondary: null + } + }, + _create: function () { + this.element.closest("form") + .unbind("reset" + this.eventNamespace) + .bind("reset" + this.eventNamespace, formResetHandler); + + if (typeof this.options.disabled !== "boolean") { + this.options.disabled = !!this.element.prop("disabled"); + } else { + this.element.prop("disabled", this.options.disabled); + } + + this._determineButtonType(); + this.hasTitle = !!this.buttonElement.attr("title"); + + var that = this, + options = this.options, + toggleButton = this.type === "checkbox" || this.type === "radio", + activeClass = !toggleButton ? "ui-state-active" : ""; + + if (options.label === null) { + options.label = (this.type === "input" ? this.buttonElement.val() : this.buttonElement.html()); + } + + this._hoverable(this.buttonElement); + + this.buttonElement + .addClass(baseClasses) + .attr("role", "button") + .bind("mouseenter" + this.eventNamespace, function () { + if (options.disabled) { + return; + } + if (this === lastActive) { + $(this).addClass("ui-state-active"); + } + }) + .bind("mouseleave" + this.eventNamespace, function () { + if (options.disabled) { + return; + } + $(this).removeClass(activeClass); + }) + .bind("click" + this.eventNamespace, function (event) { + if (options.disabled) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + }); + + // Can't use _focusable() because the element that receives focus + // and the element that gets the ui-state-focus class are different + this._on({ + focus: function () { + this.buttonElement.addClass("ui-state-focus"); + }, + blur: function () { + this.buttonElement.removeClass("ui-state-focus"); + } + }); + + if (toggleButton) { + this.element.bind("change" + this.eventNamespace, function () { + that.refresh(); + }); + } + + if (this.type === "checkbox") { + this.buttonElement.bind("click" + this.eventNamespace, function () { + if (options.disabled) { + return false; + } + }); + } else if (this.type === "radio") { + this.buttonElement.bind("click" + this.eventNamespace, function () { + if (options.disabled) { + return false; + } + $(this).addClass("ui-state-active"); + that.buttonElement.attr("aria-pressed", "true"); + + var radio = that.element[0]; + radioGroup(radio) + .not(radio) + .map(function () { + return $(this).button("widget")[0]; + }) + .removeClass("ui-state-active") + .attr("aria-pressed", "false"); + }); + } else { + this.buttonElement + .bind("mousedown" + this.eventNamespace, function () { + if (options.disabled) { + return false; + } + $(this).addClass("ui-state-active"); + lastActive = this; + that.document.one("mouseup", function () { + lastActive = null; + }); + }) + .bind("mouseup" + this.eventNamespace, function () { + if (options.disabled) { + return false; + } + $(this).removeClass("ui-state-active"); + }) + .bind("keydown" + this.eventNamespace, function (event) { + if (options.disabled) { + return false; + } + if (event.keyCode === $.ui.keyCode.SPACE || event.keyCode === $.ui.keyCode.ENTER) { + $(this).addClass("ui-state-active"); + } + }) + // see #8559, we bind to blur here in case the button element loses + // focus between keydown and keyup, it would be left in an "active" state + .bind("keyup" + this.eventNamespace + " blur" + this.eventNamespace, function () { + $(this).removeClass("ui-state-active"); + }); + + if (this.buttonElement.is("a")) { + this.buttonElement.keyup(function (event) { + if (event.keyCode === $.ui.keyCode.SPACE) { + // TODO pass through original event correctly (just as 2nd argument doesn't work) + $(this).click(); + } + }); + } + } + + // TODO: pull out $.Widget's handling for the disabled option into + // $.Widget.prototype._setOptionDisabled so it's easy to proxy and can + // be overridden by individual plugins + this._setOption("disabled", options.disabled); + this._resetButton(); + }, + + _determineButtonType: function () { + var ancestor, labelSelector, checked; + + if (this.element.is("[type=checkbox]")) { + this.type = "checkbox"; + } else if (this.element.is("[type=radio]")) { + this.type = "radio"; + } else if (this.element.is("input")) { + this.type = "input"; + } else { + this.type = "button"; + } + + if (this.type === "checkbox" || this.type === "radio") { + // we don't search against the document in case the element + // is disconnected from the DOM + ancestor = this.element.parents().last(); + labelSelector = "label[for='" + this.element.attr("id") + "']"; + this.buttonElement = ancestor.find(labelSelector); + if (!this.buttonElement.length) { + ancestor = ancestor.length ? ancestor.siblings() : this.element.siblings(); + this.buttonElement = ancestor.filter(labelSelector); + if (!this.buttonElement.length) { + this.buttonElement = ancestor.find(labelSelector); + } + } + this.element.addClass("ui-helper-hidden-accessible"); + + checked = this.element.is(":checked"); + if (checked) { + this.buttonElement.addClass("ui-state-active"); + } + this.buttonElement.prop("aria-pressed", checked); + } else { + this.buttonElement = this.element; + } + }, + + widget: function () { + return this.buttonElement; + }, + + _destroy: function () { + this.element + .removeClass("ui-helper-hidden-accessible"); + this.buttonElement + .removeClass(baseClasses + " ui-state-active " + typeClasses) + .removeAttr("role") + .removeAttr("aria-pressed") + .html(this.buttonElement.find(".ui-button-text").html()); + + if (!this.hasTitle) { + this.buttonElement.removeAttr("title"); + } + }, + + _setOption: function (key, value) { + this._super(key, value); + if (key === "disabled") { + this.element.prop("disabled", !!value); + if (value) { + this.buttonElement.removeClass("ui-state-focus"); + } + return; + } + this._resetButton(); + }, + + refresh: function () { + //See #8237 & #8828 + var isDisabled = this.element.is("input, button") ? this.element.is(":disabled") : this.element.hasClass("ui-button-disabled"); + + if (isDisabled !== this.options.disabled) { + this._setOption("disabled", isDisabled); + } + if (this.type === "radio") { + radioGroup(this.element[0]).each(function () { + if ($(this).is(":checked")) { + $(this).button("widget") + .addClass("ui-state-active") + .attr("aria-pressed", "true"); + } else { + $(this).button("widget") + .removeClass("ui-state-active") + .attr("aria-pressed", "false"); + } + }); + } else if (this.type === "checkbox") { + if (this.element.is(":checked")) { + this.buttonElement + .addClass("ui-state-active") + .attr("aria-pressed", "true"); + } else { + this.buttonElement + .removeClass("ui-state-active") + .attr("aria-pressed", "false"); + } + } + }, + + _resetButton: function () { + if (this.type === "input") { + if (this.options.label) { + this.element.val(this.options.label); + } + return; + } + var buttonElement = this.buttonElement.removeClass(typeClasses), + buttonText = $("<span></span>", this.document[0]) + .addClass("ui-button-text") + .html(this.options.label) + .appendTo(buttonElement.empty()) + .text(), + icons = this.options.icons, + multipleIcons = icons.primary && icons.secondary, + buttonClasses = []; + + if (icons.primary || icons.secondary) { + if (this.options.text) { + buttonClasses.push("ui-button-text-icon" + (multipleIcons ? "s" : (icons.primary ? "-primary" : "-secondary"))); + } + + if (icons.primary) { + buttonElement.prepend("<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>"); + } + + if (icons.secondary) { + buttonElement.append("<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>"); + } + + if (!this.options.text) { + buttonClasses.push(multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only"); + + if (!this.hasTitle) { + buttonElement.attr("title", $.trim(buttonText)); + } + } + } else { + buttonClasses.push("ui-button-text-only"); + } + buttonElement.addClass(buttonClasses.join(" ")); + } + }); + + $.widget("ui.buttonset", { + version: "1.10.4", + options: { + items: "button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(ui-button)" + }, + + _create: function () { + this.element.addClass("ui-buttonset"); + }, + + _init: function () { + this.refresh(); + }, + + _setOption: function (key, value) { + if (key === "disabled") { + this.buttons.button("option", key, value); + } + + this._super(key, value); + }, + + refresh: function () { + var rtl = this.element.css("direction") === "rtl"; + + this.buttons = this.element.find(this.options.items) + .filter(":ui-button") + .button("refresh") + .end() + .not(":ui-button") + .button() + .end() + .map(function () { + return $(this).button("widget")[0]; + }) + .removeClass("ui-corner-all ui-corner-left ui-corner-right") + .filter(":first") + .addClass(rtl ? "ui-corner-right" : "ui-corner-left") + .end() + .filter(":last") + .addClass(rtl ? "ui-corner-left" : "ui-corner-right") + .end() + .end(); + }, + + _destroy: function () { + this.element.removeClass("ui-buttonset"); + this.buttons + .map(function () { + return $(this).button("widget")[0]; + }) + .removeClass("ui-corner-left ui-corner-right") + .end() + .button("destroy"); + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/core.js b/lib/web/jquery/ui-modules/core.js new file mode 100644 index 000000000000..ec860f140f26 --- /dev/null +++ b/lib/web/jquery/ui-modules/core.js @@ -0,0 +1,319 @@ +/*! + * jQuery UI Core - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/ui-core/ + */ +define([ + 'jquery' +], function ($, undefined) { + + var uuid = 0, + runiqueId = /^ui-id-\d+$/; + +// $.ui might exist from components with no dependencies, e.g., $.ui.position + $.ui = $.ui || {}; + + $.extend($.ui, { + version: "1.10.4", + + keyCode: { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + NUMPAD_ADD: 107, + NUMPAD_DECIMAL: 110, + NUMPAD_DIVIDE: 111, + NUMPAD_ENTER: 108, + NUMPAD_MULTIPLY: 106, + NUMPAD_SUBTRACT: 109, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 + } + }); + +// plugins + $.fn.extend({ + focus: (function (orig) { + return function (delay, fn) { + return typeof delay === "number" ? + this.each(function () { + var elem = this; + setTimeout(function () { + $(elem).focus(); + if (fn) { + fn.call(elem); + } + }, delay); + }) : + orig.apply(this, arguments); + }; + })($.fn.focus), + + scrollParent: function () { + var scrollParent; + if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { + scrollParent = this.parents().filter(function () { + return (/(relative|absolute|fixed)/).test($.css(this, "position")) && (/(auto|scroll)/).test($.css(this, "overflow") + $.css(this, "overflow-y") + $.css(this, "overflow-x")); + }).eq(0); + } else { + scrollParent = this.parents().filter(function () { + return (/(auto|scroll)/).test($.css(this, "overflow") + $.css(this, "overflow-y") + $.css(this, "overflow-x")); + }).eq(0); + } + + return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; + }, + + zIndex: function (zIndex) { + if (zIndex !== undefined) { + return this.css("zIndex", zIndex); + } + + if (this.length) { + var elem = $(this[0]), position, value; + while (elem.length && elem[0] !== document) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css("position"); + if (position === "absolute" || position === "relative" || position === "fixed") { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> + value = parseInt(elem.css("zIndex"), 10); + if (!isNaN(value) && value !== 0) { + return value; + } + } + elem = elem.parent(); + } + } + + return 0; + }, + + uniqueId: function () { + return this.each(function () { + if (!this.id) { + this.id = "ui-id-" + (++uuid); + } + }); + }, + + removeUniqueId: function () { + return this.each(function () { + if (runiqueId.test(this.id)) { + $(this).removeAttr("id"); + } + }); + } + }); + +// selectors + function focusable(element, isTabIndexNotNaN) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ("area" === nodeName) { + map = element.parentNode; + mapName = map.name; + if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") { + return false; + } + img = $("img[usemap=#" + mapName + "]")[0]; + return !!img && visible(img); + } + return (/input|select|textarea|button|object/.test(nodeName) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN) && + // the element and all of its ancestors must be visible + visible(element); + } + + function visible(element) { + return $.expr.filters.visible(element) && + !$(element).parents().addBack().filter(function () { + return $.css(this, "visibility") === "hidden"; + }).length; + } + + $.extend($.expr[":"], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function (dataName) { + return function (elem) { + return !!$.data(elem, dataName); + }; + }) : + // support: jQuery <1.8 + function (elem, i, match) { + return !!$.data(elem, match[3]); + }, + + focusable: function (element) { + return focusable(element, !isNaN($.attr(element, "tabindex"))); + }, + + tabbable: function (element) { + var tabIndex = $.attr(element, "tabindex"), + isTabIndexNaN = isNaN(tabIndex); + return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); + } + }); + +// support: jQuery <1.8 + if (!$("<a>").outerWidth(1).jquery) { + $.each(["Width", "Height"], function (i, name) { + var side = name === "Width" ? ["Left", "Right"] : ["Top", "Bottom"], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce(elem, size, border, margin) { + $.each(side, function () { + size -= parseFloat($.css(elem, "padding" + this)) || 0; + if (border) { + size -= parseFloat($.css(elem, "border" + this + "Width")) || 0; + } + if (margin) { + size -= parseFloat($.css(elem, "margin" + this)) || 0; + } + }); + return size; + } + + $.fn["inner" + name] = function (size) { + if (size === undefined) { + return orig["inner" + name].call(this); + } + + return this.each(function () { + $(this).css(type, reduce(this, size) + "px"); + }); + }; + + $.fn["outer" + name] = function (size, margin) { + if (typeof size !== "number") { + return orig["outer" + name].call(this, size); + } + + return this.each(function () { + $(this).css(type, reduce(this, size, true, margin) + "px"); + }); + }; + }); + } + +// support: jQuery <1.8 + if (!$.fn.addBack) { + $.fn.addBack = function (selector) { + return this.add(selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + }; + } + +// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) + if ($("<a>").data("a-b", "a").removeData("a-b").data("a-b")) { + $.fn.removeData = (function (removeData) { + return function (key) { + if (arguments.length) { + return removeData.call(this, $.camelCase(key)); + } else { + return removeData.call(this); + } + }; + })($.fn.removeData); + } + + +// deprecated + $.ui.ie = !!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()); + + $.support.selectstart = "onselectstart" in document.createElement("div"); + $.fn.extend({ + disableSelection: function () { + return this.bind(($.support.selectstart ? "selectstart" : "mousedown") + + ".ui-disableSelection", function (event) { + event.preventDefault(); + }); + }, + + enableSelection: function () { + return this.unbind(".ui-disableSelection"); + } + }); + + $.extend($.ui, { + // $.ui.plugin is deprecated. Use $.widget() extensions instead. + plugin: { + add: function (module, option, set) { + var i, + proto = $.ui[module].prototype; + for (i in set) { + proto.plugins[i] = proto.plugins[i] || []; + proto.plugins[i].push([option, set[i]]); + } + }, + call: function (instance, name, args) { + var i, + set = instance.plugins[name]; + if (!set || !instance.element[0].parentNode || instance.element[0].parentNode.nodeType === 11) { + return; + } + + for (i = 0; i < set.length; i++) { + if (instance.options[set[i][0]]) { + set[i][1].apply(instance.element, args); + } + } + } + }, + + // only used by resizable + hasScroll: function (el, a) { + + //If overflow is hidden, the element might have extra content, but the user wants to hide it + if ($(el).css("overflow") === "hidden") { + return false; + } + + var scroll = (a && a === "left") ? "scrollLeft" : "scrollTop", + has = false; + + if (el[scroll] > 0) { + return true; + } + + // TODO: determine which cases actually cause this to happen + // if the element doesn't have the scroll set, see if it's possible to + // set the scroll + el[scroll] = 1; + has = (el[scroll] > 0); + el[scroll] = 0; + return has; + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/datepicker.js b/lib/web/jquery/ui-modules/datepicker.js new file mode 100644 index 000000000000..77684e255874 --- /dev/null +++ b/lib/web/jquery/ui-modules/datepicker.js @@ -0,0 +1,2069 @@ +/*! + * jQuery UI Datepicker - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/datepicker/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', +], function ($, undefined) { + + $.extend($.ui, {datepicker: {version: "1.10.4"}}); + + var PROP_NAME = "datepicker", + instActive; + + /* Date picker manager. + Use the singleton instance of this class, $.datepicker, to interact with the date picker. + Settings for (groups of) date pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ + + function Datepicker() { + this._curInst = null; // The current instance in use + this._keyEvent = false; // If the last event was a key event + this._disabledInputs = []; // List of date picker inputs that have been disabled + this._datepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division + this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class + this._appendClass = "ui-datepicker-append"; // The name of the append marker class + this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class + this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class + this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class + this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class + this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class + this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class + this.regional = []; // Available regional settings, indexed by language code + this.regional[""] = { // Default regional settings + closeText: "Done", // Display text for close link + prevText: "Prev", // Display text for previous month link + nextText: "Next", // Display text for next month link + currentText: "Today", // Display text for current month link + monthNames: ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"], // Names of months for drop-down and formatting + monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // For formatting + dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], // For formatting + dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], // For formatting + dayNamesMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], // Column headings for days starting at Sunday + weekHeader: "Wk", // Column header for week of the year + dateFormat: "mm/dd/yy", // See format options on parseDate + firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... + isRTL: false, // True if right-to-left language, false if left-to-right + showMonthAfterYear: false, // True if the year select precedes month, false for month then year + yearSuffix: "" // Additional text to append to the year in the month headers + }; + this._defaults = { // Global defaults for all the date picker instances + showOn: "focus", // "focus" for popup on focus, + // "button" for trigger button, or "both" for either + showAnim: "fadeIn", // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + defaultDate: null, // Used when field is blank: actual date, + // +/-number for offset from today, null for today + appendText: "", // Display text following the input box, e.g. showing the format + buttonText: "...", // Text for trigger button + buttonImage: "", // URL for trigger button image + buttonImageOnly: false, // True if the image appears alone, false if it appears on a button + hideIfNoPrevNext: false, // True to hide next/previous month links + // if not applicable, false to just disable them + navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links + gotoCurrent: false, // True if today link goes back to current selection instead + changeMonth: false, // True if month can be selected directly, false if only prev/next + changeYear: false, // True if year can be selected directly, false if only prev/next + yearRange: "c-10:c+10", // Range of years to display in drop-down, + // either relative to today's year (-nn:+nn), relative to currently displayed year + // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) + showOtherMonths: false, // True to show dates in other months, false to leave blank + selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable + showWeek: false, // True to show week of the year, false to not show it + calculateWeek: this.iso8601Week, // How to calculate the week of the year, + // takes a Date and returns the number of the week for it + shortYearCutoff: "+10", // Short year values < this are in the current century, + // > this are in the previous century, + // string value starting with "+" for current year + value + minDate: null, // The earliest selectable date, or null for no limit + maxDate: null, // The latest selectable date, or null for no limit + duration: "fast", // Duration of display/closure + beforeShowDay: null, // Function that takes a date and returns an array with + // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "", + // [2] = cell title (optional), e.g. $.datepicker.noWeekends + beforeShow: null, // Function that takes an input field and + // returns a set of custom settings for the date picker + onSelect: null, // Define a callback function when a date is selected + onChangeMonthYear: null, // Define a callback function when the month or year is changed + onClose: null, // Define a callback function when the datepicker is closed + numberOfMonths: 1, // Number of months to show at a time + showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) + stepMonths: 1, // Number of months to step back/forward + stepBigMonths: 12, // Number of months to step back/forward for the big links + altField: "", // Selector for an alternate field to store selected dates into + altFormat: "", // The date format to use for the alternate field + constrainInput: true, // The input is constrained by the current date format + showButtonPanel: false, // True to show button panel, false to not show it + autoSize: false, // True to size the input for the date format, false to leave as is + disabled: false // The initial disabled state + }; + $.extend(this._defaults, this.regional[""]); + this.dpDiv = bindHover($("<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")); + } + + $.extend(Datepicker.prototype, { + /* Class name added to elements to indicate already configured with a date picker. */ + markerClassName: "hasDatepicker", + + //Keep track of the maximum number of rows displayed (see #7043) + maxRows: 4, + + // TODO rename to "widget" when switching to widget factory + _widgetDatepicker: function () { + return this.dpDiv; + }, + + /* Override the default settings for all instances of the date picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function (settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* Attach the date picker to a jQuery selection. + * @param target element - the target input field or division or span + * @param settings object - the new settings to use for this date picker instance (anonymous) + */ + _attachDatepicker: function (target, settings) { + var nodeName, inline, inst; + nodeName = target.nodeName.toLowerCase(); + inline = (nodeName === "div" || nodeName === "span"); + if (!target.id) { + this.uuid += 1; + target.id = "dp" + this.uuid; + } + inst = this._newInst($(target), inline); + inst.settings = $.extend({}, settings || {}); + if (nodeName === "input") { + this._connectDatepicker(target, inst); + } else if (inline) { + this._inlineDatepicker(target, inst); + } + }, + + /* Create a new instance object. */ + _newInst: function (target, inline) { + var id = target[0].id.replace(/([^A-Za-z0-9_\-])/g, "\\\\$1"); // escape jQuery meta chars + return { + id: id, input: target, // associated target + selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection + drawMonth: 0, drawYear: 0, // month being drawn + inline: inline, // is datepicker inline or not + dpDiv: (!inline ? this.dpDiv : // presentation div + bindHover($("<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))) + }; + }, + + /* Attach the date picker to an input field. */ + _connectDatepicker: function (target, inst) { + var input = $(target); + inst.append = $([]); + inst.trigger = $([]); + if (input.hasClass(this.markerClassName)) { + return; + } + this._attachments(input, inst); + input.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp); + this._autoSize(inst); + $.data(target, PROP_NAME, inst); + //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) + if (inst.settings.disabled) { + this._disableDatepicker(target); + } + }, + + /* Make attachments based on settings. */ + _attachments: function (input, inst) { + var showOn, buttonText, buttonImage, + appendText = this._get(inst, "appendText"), + isRTL = this._get(inst, "isRTL"); + + if (inst.append) { + inst.append.remove(); + } + if (appendText) { + inst.append = $("<span class='" + this._appendClass + "'>" + appendText + "</span>"); + input[isRTL ? "before" : "after"](inst.append); + } + + input.unbind("focus", this._showDatepicker); + + if (inst.trigger) { + inst.trigger.remove(); + } + + showOn = this._get(inst, "showOn"); + if (showOn === "focus" || showOn === "both") { // pop-up date picker when in the marked field + input.focus(this._showDatepicker); + } + if (showOn === "button" || showOn === "both") { // pop-up date picker when button clicked + buttonText = this._get(inst, "buttonText"); + buttonImage = this._get(inst, "buttonImage"); + inst.trigger = $(this._get(inst, "buttonImageOnly") ? + $("<img/>").addClass(this._triggerClass).attr({ + src: buttonImage, + alt: buttonText, + title: buttonText + }) : + $("<button type='button'></button>").addClass(this._triggerClass).html(!buttonImage ? buttonText : $("<img/>").attr( + {src: buttonImage, alt: buttonText, title: buttonText}))); + input[isRTL ? "before" : "after"](inst.trigger); + inst.trigger.click(function () { + if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) { + $.datepicker._hideDatepicker(); + } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) { + $.datepicker._hideDatepicker(); + $.datepicker._showDatepicker(input[0]); + } else { + $.datepicker._showDatepicker(input[0]); + } + return false; + }); + } + }, + + /* Apply the maximum length for the date format. */ + _autoSize: function (inst) { + if (this._get(inst, "autoSize") && !inst.inline) { + var findMax, max, maxI, i, + date = new Date(2009, 12 - 1, 20), // Ensure double digits + dateFormat = this._get(inst, "dateFormat"); + + if (dateFormat.match(/[DM]/)) { + findMax = function (names) { + max = 0; + maxI = 0; + for (i = 0; i < names.length; i++) { + if (names[i].length > max) { + max = names[i].length; + maxI = i; + } + } + return maxI; + }; + date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? + "monthNames" : "monthNamesShort")))); + date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? + "dayNames" : "dayNamesShort"))) + 20 - date.getDay()); + } + inst.input.attr("size", this._formatDate(inst, date).length); + } + }, + + /* Attach an inline date picker to a div. */ + _inlineDatepicker: function (target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) { + return; + } + divSpan.addClass(this.markerClassName).append(inst.dpDiv); + $.data(target, PROP_NAME, inst); + this._setDate(inst, this._getDefaultDate(inst), true); + this._updateDatepicker(inst); + this._updateAlternate(inst); + //If disabled option is true, disable the datepicker before showing it (see ticket #5665) + if (inst.settings.disabled) { + this._disableDatepicker(target); + } + // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements + // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height + inst.dpDiv.css("display", "block"); + }, + + /* Pop-up the date picker in a "dialog" box. + * @param input element - ignored + * @param date string or Date - the initial date to display + * @param onSelect function - the function to call when a date is selected + * @param settings object - update the dialog date picker instance's settings (anonymous object) + * @param pos int[2] - coordinates for the dialog's position within the screen or + * event - with x/y coordinates or + * leave empty for default (screen centre) + * @return the manager object + */ + _dialogDatepicker: function (input, date, onSelect, settings, pos) { + var id, browserWidth, browserHeight, scrollX, scrollY, + inst = this._dialogInst; // internal instance + + if (!inst) { + this.uuid += 1; + id = "dp" + this.uuid; + this._dialogInput = $("<input type='text' id='" + id + + "' style='position: absolute; top: -100px; width: 0px;'/>"); + this._dialogInput.keydown(this._doKeyDown); + $("body").append(this._dialogInput); + inst = this._dialogInst = this._newInst(this._dialogInput, false); + inst.settings = {}; + $.data(this._dialogInput[0], PROP_NAME, inst); + } + extendRemove(inst.settings, settings || {}); + date = (date && date.constructor === Date ? this._formatDate(inst, date) : date); + this._dialogInput.val(date); + + this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); + if (!this._pos) { + browserWidth = document.documentElement.clientWidth; + browserHeight = document.documentElement.clientHeight; + scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + scrollY = document.documentElement.scrollTop || document.body.scrollTop; + this._pos = // should use actual width/height below + [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; + } + + // move input on screen for focus, but hidden behind dialog + this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px"); + inst.settings.onSelect = onSelect; + this._inDialog = true; + this.dpDiv.addClass(this._dialogClass); + this._showDatepicker(this._dialogInput[0]); + if ($.blockUI) { + $.blockUI(this.dpDiv); + } + $.data(this._dialogInput[0], PROP_NAME, inst); + return this; + }, + + /* Detach a datepicker from its control. + * @param target element - the target input field or division or span + */ + _destroyDatepicker: function (target) { + var nodeName, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + $.removeData(target, PROP_NAME); + if (nodeName === "input") { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName).unbind("focus", this._showDatepicker).unbind("keydown", this._doKeyDown).unbind("keypress", this._doKeyPress).unbind("keyup", this._doKeyUp); + } else if (nodeName === "div" || nodeName === "span") { + $target.removeClass(this.markerClassName).empty(); + } + }, + + /* Enable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _enableDatepicker: function (target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = false; + inst.trigger.filter("button").each(function () { + this.disabled = false; + }).end().filter("img").css({opacity: "1.0", cursor: ""}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().removeClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled", false); + } + this._disabledInputs = $.map(this._disabledInputs, + function (value) { + return (value === target ? null : value); + }); // delete entry + }, + + /* Disable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _disableDatepicker: function (target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = true; + inst.trigger.filter("button").each(function () { + this.disabled = true; + }).end().filter("img").css({opacity: "0.5", cursor: "default"}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().addClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled", true); + } + this._disabledInputs = $.map(this._disabledInputs, + function (value) { + return (value === target ? null : value); + }); // delete entry + this._disabledInputs[this._disabledInputs.length] = target; + }, + + /* Is the first field in a jQuery collection disabled as a datepicker? + * @param target element - the target input field or division or span + * @return boolean - true if disabled, false if enabled + */ + _isDisabledDatepicker: function (target) { + if (!target) { + return false; + } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] === target) { + return true; + } + } + return false; + }, + + /* Retrieve the instance data for the target control. + * @param target element - the target input field or division or span + * @return object - the associated instance data + * @throws error if a jQuery problem getting data + */ + _getInst: function (target) { + try { + return $.data(target, PROP_NAME); + } catch (err) { + throw "Missing instance data for this datepicker"; + } + }, + + /* Update or retrieve the settings for a date picker attached to an input field or division. + * @param target element - the target input field or division or span + * @param name object - the new settings to update or + * string - the name of the setting to change or retrieve, + * when retrieving also "all" for all instance settings or + * "defaults" for all global defaults + * @param value any - the new value for the setting + * (omit if above is an object or to retrieve a value) + */ + _optionDatepicker: function (target, name, value) { + var settings, date, minDate, maxDate, + inst = this._getInst(target); + + if (arguments.length === 2 && typeof name === "string") { + return (name === "defaults" ? $.extend({}, $.datepicker._defaults) : + (inst ? (name === "all" ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + + settings = name || {}; + if (typeof name === "string") { + settings = {}; + settings[name] = value; + } + + if (inst) { + if (this._curInst === inst) { + this._hideDatepicker(); + } + + date = this._getDateDatepicker(target, true); + minDate = this._getMinMaxDate(inst, "min"); + maxDate = this._getMinMaxDate(inst, "max"); + extendRemove(inst.settings, settings); + // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided + if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) { + inst.settings.minDate = this._formatDate(inst, minDate); + } + if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) { + inst.settings.maxDate = this._formatDate(inst, maxDate); + } + if ("disabled" in settings) { + if (settings.disabled) { + this._disableDatepicker(target); + } else { + this._enableDatepicker(target); + } + } + this._attachments($(target), inst); + this._autoSize(inst); + this._setDate(inst, date); + this._updateAlternate(inst); + this._updateDatepicker(inst); + } + }, + + // change method deprecated + _changeDatepicker: function (target, name, value) { + this._optionDatepicker(target, name, value); + }, + + /* Redraw the date picker attached to an input field or division. + * @param target element - the target input field or division or span + */ + _refreshDatepicker: function (target) { + var inst = this._getInst(target); + if (inst) { + this._updateDatepicker(inst); + } + }, + + /* Set the dates for a jQuery selection. + * @param target element - the target input field or division or span + * @param date Date - the new date + */ + _setDateDatepicker: function (target, date) { + var inst = this._getInst(target); + if (inst) { + this._setDate(inst, date); + this._updateDatepicker(inst); + this._updateAlternate(inst); + } + }, + + /* Get the date(s) for the first entry in a jQuery selection. + * @param target element - the target input field or division or span + * @param noDefault boolean - true if no default date is to be used + * @return Date - the current date + */ + _getDateDatepicker: function (target, noDefault) { + var inst = this._getInst(target); + if (inst && !inst.inline) { + this._setDateFromField(inst, noDefault); + } + return (inst ? this._getDate(inst) : null); + }, + + /* Handle keystrokes. */ + _doKeyDown: function (event) { + var onSelect, dateStr, sel, + inst = $.datepicker._getInst(event.target), + handled = true, + isRTL = inst.dpDiv.is(".ui-datepicker-rtl"); + + inst._keyEvent = true; + if ($.datepicker._datepickerShowing) { + switch (event.keyCode) { + case 9: + $.datepicker._hideDatepicker(); + handled = false; + break; // hide on tab out + case 13: + sel = $("td." + $.datepicker._dayOverClass + ":not(." + + $.datepicker._currentClass + ")", inst.dpDiv); + if (sel[0]) { + $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); + } + + onSelect = $.datepicker._get(inst, "onSelect"); + if (onSelect) { + dateStr = $.datepicker._formatDate(inst); + + // trigger custom callback + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); + } else { + $.datepicker._hideDatepicker(); + } + + return false; // don't submit the form + case 27: + $.datepicker._hideDatepicker(); + break; // hide on escape + case 33: + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + break; // previous month/year on page up/+ ctrl + case 34: + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + break; // next month/year on page down/+ ctrl + case 35: + if (event.ctrlKey || event.metaKey) { + $.datepicker._clearDate(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // clear on ctrl or command +end + case 36: + if (event.ctrlKey || event.metaKey) { + $.datepicker._gotoToday(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // current on ctrl or command +home + case 37: + if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // -1 day on ctrl or command +left + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +left on Mac + break; + case 38: + if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, -7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // -1 week on ctrl or command +up + case 39: + if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // +1 day on ctrl or command +right + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +right + break; + case 40: + if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, +7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // +1 week on ctrl or command +down + default: + handled = false; + } + } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home + $.datepicker._showDatepicker(this); + } else { + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Filter entered characters - based on date format. */ + _doKeyPress: function (event) { + var chars, chr, + inst = $.datepicker._getInst(event.target); + + if ($.datepicker._get(inst, "constrainInput")) { + chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat")); + chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode); + return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1); + } + }, + + /* Synchronise manual entry and field/alternate field. */ + _doKeyUp: function (event) { + var date, + inst = $.datepicker._getInst(event.target); + + if (inst.input.val() !== inst.lastVal) { + try { + date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + (inst.input ? inst.input.val() : null), + $.datepicker._getFormatConfig(inst)); + + if (date) { // only if valid + $.datepicker._setDateFromField(inst); + $.datepicker._updateAlternate(inst); + $.datepicker._updateDatepicker(inst); + } + } catch (err) { + } + } + return true; + }, + + /* Pop-up the date picker for a given input field. + * If false returned from beforeShow event handler do not show. + * @param input element - the input field attached to the date picker or + * event - if triggered by focus + */ + _showDatepicker: function (input) { + input = input.target || input; + if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger + input = $("input", input.parentNode)[0]; + } + + if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here + return; + } + + var inst, beforeShow, beforeShowSettings, isFixed, + offset, showAnim, duration; + + inst = $.datepicker._getInst(input); + if ($.datepicker._curInst && $.datepicker._curInst !== inst) { + $.datepicker._curInst.dpDiv.stop(true, true); + if (inst && $.datepicker._datepickerShowing) { + $.datepicker._hideDatepicker($.datepicker._curInst.input[0]); + } + } + + beforeShow = $.datepicker._get(inst, "beforeShow"); + beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; + if (beforeShowSettings === false) { + return; + } + extendRemove(inst.settings, beforeShowSettings); + + inst.lastVal = null; + $.datepicker._lastInput = input; + $.datepicker._setDateFromField(inst); + + if ($.datepicker._inDialog) { // hide cursor + input.value = ""; + } + if (!$.datepicker._pos) { // position below input + $.datepicker._pos = $.datepicker._findPos(input); + $.datepicker._pos[1] += input.offsetHeight; // add the height + } + + isFixed = false; + $(input).parents().each(function () { + isFixed |= $(this).css("position") === "fixed"; + return !isFixed; + }); + + offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; + $.datepicker._pos = null; + //to avoid flashes on Firefox + inst.dpDiv.empty(); + // determine sizing offscreen + inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); + $.datepicker._updateDatepicker(inst); + // fix width for dynamic number of date pickers + // and adjust position before showing + offset = $.datepicker._checkOffset(inst, offset, isFixed); + inst.dpDiv.css({ + position: ($.datepicker._inDialog && $.blockUI ? + "static" : (isFixed ? "fixed" : "absolute")), display: "none", + left: offset.left + "px", top: offset.top + "px" + }); + + if (!inst.inline) { + showAnim = $.datepicker._get(inst, "showAnim"); + duration = $.datepicker._get(inst, "duration"); + inst.dpDiv.zIndex($(input).zIndex() + 1); + $.datepicker._datepickerShowing = true; + + if ($.effects && $.effects.effect[showAnim]) { + inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration); + } else { + inst.dpDiv[showAnim || "show"](showAnim ? duration : null); + } + + if ($.datepicker._shouldFocusInput(inst)) { + inst.input.focus(); + } + + $.datepicker._curInst = inst; + } + }, + + /* Generate the date picker content. */ + _updateDatepicker: function (inst) { + this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) + instActive = inst; // for delegate hover events + inst.dpDiv.empty().append(this._generateHTML(inst)); + this._attachHandlers(inst); + inst.dpDiv.find("." + this._dayOverClass + " a").mouseover(); + + var origyearshtml, + numMonths = this._getNumberOfMonths(inst), + cols = numMonths[1], + width = 17; + + inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); + if (cols > 1) { + inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em"); + } + inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") + + "Class"]("ui-datepicker-multi"); + inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") + + "Class"]("ui-datepicker-rtl"); + + if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput(inst)) { + inst.input.focus(); + } + + // deffered render of the years select (to avoid flashes on Firefox) + if (inst.yearshtml) { + origyearshtml = inst.yearshtml; + setTimeout(function () { + //assure that inst.yearshtml didn't change. + if (origyearshtml === inst.yearshtml && inst.yearshtml) { + inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml); + } + origyearshtml = inst.yearshtml = null; + }, 0); + } + }, + + // #6694 - don't focus the input if it's already focused + // this breaks the change event in IE + // Support: IE and jQuery <1.9 + _shouldFocusInput: function (inst) { + return inst.input && inst.input.is(":visible") && !inst.input.is(":disabled") && !inst.input.is(":focus"); + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function (inst, offset, isFixed) { + var dpWidth = inst.dpDiv.outerWidth(), + dpHeight = inst.dpDiv.outerHeight(), + inputWidth = inst.input ? inst.input.outerWidth() : 0, + inputHeight = inst.input ? inst.input.outerHeight() : 0, + viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()), + viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop()); + + offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? + Math.abs(offset.left + dpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? + Math.abs(dpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function (obj) { + var position, + inst = this._getInst(obj), + isRTL = this._get(inst, "isRTL"); + + while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) { + obj = obj[isRTL ? "previousSibling" : "nextSibling"]; + } + + position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Hide the date picker from view. + * @param input element - the input field attached to the date picker + */ + _hideDatepicker: function (input) { + var showAnim, duration, postProcess, onClose, + inst = this._curInst; + + if (!inst || (input && inst !== $.data(input, PROP_NAME))) { + return; + } + + if (this._datepickerShowing) { + showAnim = this._get(inst, "showAnim"); + duration = this._get(inst, "duration"); + postProcess = function () { + $.datepicker._tidyDialog(inst); + }; + + // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed + if ($.effects && ($.effects.effect[showAnim] || $.effects[showAnim])) { + inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess); + } else { + inst.dpDiv[(showAnim === "slideDown" ? "slideUp" : + (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess); + } + + if (!showAnim) { + postProcess(); + } + this._datepickerShowing = false; + + onClose = this._get(inst, "onClose"); + if (onClose) { + onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]); + } + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({position: "absolute", left: "0", top: "-100px"}); + if ($.blockUI) { + $.unblockUI(); + $("body").append(this.dpDiv); + } + } + this._inDialog = false; + } + }, + + /* Tidy up after a dialog display. */ + _tidyDialog: function (inst) { + inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar"); + }, + + /* Close date picker if clicked elsewhere. */ + _checkExternalClick: function (event) { + if (!$.datepicker._curInst) { + return; + } + + var $target = $(event.target), + inst = $.datepicker._getInst($target[0]); + + if ((($target[0].id !== $.datepicker._mainDivId && + $target.parents("#" + $.datepicker._mainDivId).length === 0 && + !$target.hasClass($.datepicker.markerClassName) && + !$target.closest("." + $.datepicker._triggerClass).length && + $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI))) || + ($target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst)) { + $.datepicker._hideDatepicker(); + } + }, + + /* Adjust one of the date sub-fields. */ + _adjustDate: function (id, offset, period) { + var target = $(id), + inst = this._getInst(target[0]); + + if (this._isDisabledDatepicker(target[0])) { + return; + } + this._adjustInstDate(inst, offset + + (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning + period); + this._updateDatepicker(inst); + }, + + /* Action for current link. */ + _gotoToday: function (id) { + var date, + target = $(id), + inst = this._getInst(target[0]); + + if (this._get(inst, "gotoCurrent") && inst.currentDay) { + inst.selectedDay = inst.currentDay; + inst.drawMonth = inst.selectedMonth = inst.currentMonth; + inst.drawYear = inst.selectedYear = inst.currentYear; + } else { + date = new Date(); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + } + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a new month/year. */ + _selectMonthYear: function (id, select, period) { + var target = $(id), + inst = this._getInst(target[0]); + + inst["selected" + (period === "M" ? "Month" : "Year")] = + inst["draw" + (period === "M" ? "Month" : "Year")] = + parseInt(select.options[select.selectedIndex].value, 10); + + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a day. */ + _selectDay: function (id, month, year, td) { + var inst, + target = $(id); + + if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { + return; + } + + inst = this._getInst(target[0]); + inst.selectedDay = inst.currentDay = $("a", td).html(); + inst.selectedMonth = inst.currentMonth = month; + inst.selectedYear = inst.currentYear = year; + this._selectDate(id, this._formatDate(inst, + inst.currentDay, inst.currentMonth, inst.currentYear)); + }, + + /* Erase the input field and hide the date picker. */ + _clearDate: function (id) { + var target = $(id); + this._selectDate(target, ""); + }, + + /* Update the input field with the selected date. */ + _selectDate: function (id, dateStr) { + var onSelect, + target = $(id), + inst = this._getInst(target[0]); + + dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); + if (inst.input) { + inst.input.val(dateStr); + } + this._updateAlternate(inst); + + onSelect = this._get(inst, "onSelect"); + if (onSelect) { + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback + } else if (inst.input) { + inst.input.trigger("change"); // fire the change event + } + + if (inst.inline) { + this._updateDatepicker(inst); + } else { + this._hideDatepicker(); + this._lastInput = inst.input[0]; + if (typeof (inst.input[0]) !== "object") { + inst.input.focus(); // restore focus + } + this._lastInput = null; + } + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function (inst) { + var altFormat, date, dateStr, + altField = this._get(inst, "altField"); + + if (altField) { // update alternate field too + altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat"); + date = this._getDate(inst); + dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); + $(altField).each(function () { + $(this).val(dateStr); + }); + } + }, + + /* Set as beforeShowDay function to prevent selection of weekends. + * @param date Date - the date to customise + * @return [boolean, string] - is this date selectable?, what is its CSS class? + */ + noWeekends: function (date) { + var day = date.getDay(); + return [(day > 0 && day < 6), ""]; + }, + + /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * @param date Date - the date to get the week for + * @return number - the number of the week within the year that contains this date + */ + iso8601Week: function (date) { + var time, + checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + }, + + /* Parse a string value into a date object. + * See formatDate below for the possible formats. + * + * @param format string - the expected format of the date + * @param value string - the date in the above format + * @param settings Object - attributes include: + * shortYearCutoff number - the cutoff year for determining the century (optional) + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return Date - the extracted date value or null if value is blank + */ + parseDate: function (format, value, settings) { + if (format == null || value == null) { + throw "Invalid arguments"; + } + + value = (typeof value === "object" ? value.toString() : value + ""); + if (value === "") { + return null; + } + + var iFormat, dim, extra, + iValue = 0, + shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff, + shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : + new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)), + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + year = -1, + month = -1, + day = -1, + doy = -1, + literal = false, + date, + // Check whether a format character is doubled + lookAhead = function (match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Extract a number from the string value + getNumber = function (match) { + var isDoubled = lookAhead(match), + size = (match === "@" ? 14 : (match === "!" ? 20 : + (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))), + digits = new RegExp("^\\d{1," + size + "}"), + num = value.substring(iValue).match(digits); + if (!num) { + throw "Missing number at position " + iValue; + } + iValue += num[0].length; + return parseInt(num[0], 10); + }, + // Extract a name from the string value and convert to an index + getName = function (match, shortNames, longNames) { + var index = -1, + names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { + return [[k, v]]; + }).sort(function (a, b) { + return -(a[1].length - b[1].length); + }); + + $.each(names, function (i, pair) { + var name = pair[1]; + if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) { + index = pair[0]; + iValue += name.length; + return false; + } + }); + if (index !== -1) { + return index + 1; + } else { + throw "Unknown name at position " + iValue; + } + }, + // Confirm that a literal character matches the string value + checkLiteral = function () { + if (value.charAt(iValue) !== format.charAt(iFormat)) { + throw "Unexpected literal at position " + iValue; + } + iValue++; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + checkLiteral(); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + day = getNumber("d"); + break; + case "D": + getName("D", dayNamesShort, dayNames); + break; + case "o": + doy = getNumber("o"); + break; + case "m": + month = getNumber("m"); + break; + case "M": + month = getName("M", monthNamesShort, monthNames); + break; + case "y": + year = getNumber("y"); + break; + case "@": + date = new Date(getNumber("@")); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "!": + date = new Date((getNumber("!") - this._ticksTo1970) / 10000); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "'": + if (lookAhead("'")) { + checkLiteral(); + } else { + literal = true; + } + break; + default: + checkLiteral(); + } + } + } + + if (iValue < value.length) { + extra = value.substr(iValue); + if (!/^\s+/.test(extra)) { + throw "Extra/unparsed characters found in date: " + extra; + } + } + + if (year === -1) { + year = new Date().getFullYear(); + } else if (year < 100) { + year += new Date().getFullYear() - new Date().getFullYear() % 100 + + (year <= shortYearCutoff ? 0 : -100); + } + + if (doy > -1) { + month = 1; + day = doy; + do { + dim = this._getDaysInMonth(year, month - 1); + if (day <= dim) { + break; + } + month++; + day -= dim; + } while (true); + } + + date = this._daylightSavingAdjust(new Date(year, month - 1, day)); + if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { + throw "Invalid date"; // E.g. 31/02/00 + } + return date; + }, + + /* Standard date formats. */ + ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) + COOKIE: "D, dd M yy", + ISO_8601: "yy-mm-dd", + RFC_822: "D, d M y", + RFC_850: "DD, dd-M-y", + RFC_1036: "D, d M y", + RFC_1123: "D, d M yy", + RFC_2822: "D, d M yy", + RSS: "D, d M y", // RFC 822 + TICKS: "!", + TIMESTAMP: "@", + W3C: "yy-mm-dd", // ISO 8601 + + _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), + + /* Format a date object into a string value. + * The format can be combinations of the following: + * d - day of month (no leading zero) + * dd - day of month (two digit) + * o - day of year (no leading zeros) + * oo - day of year (three digit) + * D - day name short + * DD - day name long + * m - month of year (no leading zero) + * mm - month of year (two digit) + * M - month name short + * MM - month name long + * y - year (two digit) + * yy - year (four digit) + * @ - Unix timestamp (ms since 01/01/1970) + * ! - Windows ticks (100ns since 01/01/0001) + * "..." - literal text + * '' - single quote + * + * @param format string - the desired format of the date + * @param date Date - the date value to format + * @param settings Object - attributes include: + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return string - the date in the above format + */ + formatDate: function (format, date, settings) { + if (!date) { + return ""; + } + + var iFormat, + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + // Check whether a format character is doubled + lookAhead = function (match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Format a number, with leading zero if necessary + formatNumber = function (match, value, len) { + var num = "" + value; + if (lookAhead(match)) { + while (num.length < len) { + num = "0" + num; + } + } + return num; + }, + // Format a name, short or long as requested + formatName = function (match, value, shortNames, longNames) { + return (lookAhead(match) ? longNames[value] : shortNames[value]); + }, + output = "", + literal = false; + + if (date) { + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + output += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + output += formatNumber("d", date.getDate(), 2); + break; + case "D": + output += formatName("D", date.getDay(), dayNamesShort, dayNames); + break; + case "o": + output += formatNumber("o", + Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); + break; + case "m": + output += formatNumber("m", date.getMonth() + 1, 2); + break; + case "M": + output += formatName("M", date.getMonth(), monthNamesShort, monthNames); + break; + case "y": + output += (lookAhead("y") ? date.getFullYear() : + (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100); + break; + case "@": + output += date.getTime(); + break; + case "!": + output += date.getTime() * 10000 + this._ticksTo1970; + break; + case "'": + if (lookAhead("'")) { + output += "'"; + } else { + literal = true; + } + break; + default: + output += format.charAt(iFormat); + } + } + } + } + return output; + }, + + /* Extract all possible characters from the date format. */ + _possibleChars: function (format) { + var iFormat, + chars = "", + literal = false, + // Check whether a format character is doubled + lookAhead = function (match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + chars += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + case "m": + case "y": + case "@": + chars += "0123456789"; + break; + case "D": + case "M": + return null; // Accept anything + case "'": + if (lookAhead("'")) { + chars += "'"; + } else { + literal = true; + } + break; + default: + chars += format.charAt(iFormat); + } + } + } + return chars; + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function (inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing date and initialise date picker. */ + _setDateFromField: function (inst, noDefault) { + if (inst.input.val() === inst.lastVal) { + return; + } + + var dateFormat = this._get(inst, "dateFormat"), + dates = inst.lastVal = inst.input ? inst.input.val() : null, + defaultDate = this._getDefaultDate(inst), + date = defaultDate, + settings = this._getFormatConfig(inst); + + try { + date = this.parseDate(dateFormat, dates, settings) || defaultDate; + } catch (event) { + dates = (noDefault ? "" : dates); + } + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + inst.currentDay = (dates ? date.getDate() : 0); + inst.currentMonth = (dates ? date.getMonth() : 0); + inst.currentYear = (dates ? date.getFullYear() : 0); + this._adjustInstDate(inst); + }, + + /* Retrieve the default date shown on opening. */ + _getDefaultDate: function (inst) { + return this._restrictMinMax(inst, + this._determineDate(inst, this._get(inst, "defaultDate"), new Date())); + }, + + /* A date may be specified as an exact value or a relative one. */ + _determineDate: function (inst, date, defaultDate) { + var offsetNumeric = function (offset) { + var date = new Date(); + date.setDate(date.getDate() + offset); + return date; + }, + offsetString = function (offset) { + try { + return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + offset, $.datepicker._getFormatConfig(inst)); + } catch (e) { + // Ignore + } + + var date = (offset.toLowerCase().match(/^c/) ? + $.datepicker._getDate(inst) : null) || new Date(), + year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, + matches = pattern.exec(offset); + + while (matches) { + switch (matches[2] || "d") { + case "d" : + case "D" : + day += parseInt(matches[1], 10); + break; + case "w" : + case "W" : + day += parseInt(matches[1], 10) * 7; + break; + case "m" : + case "M" : + month += parseInt(matches[1], 10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + case "y": + case "Y" : + year += parseInt(matches[1], 10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + } + matches = pattern.exec(offset); + } + return new Date(year, month, day); + }, + newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) : + (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); + + newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate); + if (newDate) { + newDate.setHours(0); + newDate.setMinutes(0); + newDate.setSeconds(0); + newDate.setMilliseconds(0); + } + return this._daylightSavingAdjust(newDate); + }, + + /* Handle switch to/from daylight saving. + * Hours may be non-zero on daylight saving cut-over: + * > 12 when midnight changeover, but then cannot generate + * midnight datetime, so jump to 1AM, otherwise reset. + * @param date (Date) the date to check + * @return (Date) the corrected date + */ + _daylightSavingAdjust: function (date) { + if (!date) { + return null; + } + date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); + return date; + }, + + /* Set the date(s) directly. */ + _setDate: function (inst, date, noChange) { + var clear = !date, + origMonth = inst.selectedMonth, + origYear = inst.selectedYear, + newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); + + inst.selectedDay = inst.currentDay = newDate.getDate(); + inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); + inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); + if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) { + this._notifyChange(inst); + } + this._adjustInstDate(inst); + if (inst.input) { + inst.input.val(clear ? "" : this._formatDate(inst)); + } + }, + + /* Retrieve the date(s) directly. */ + _getDate: function (inst) { + var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null : + this._daylightSavingAdjust(new Date( + inst.currentYear, inst.currentMonth, inst.currentDay))); + return startDate; + }, + + /* Attach the onxxx handlers. These are declared statically so + * they work with static code transformers like Caja. + */ + _attachHandlers: function (inst) { + var stepMonths = this._get(inst, "stepMonths"), + id = "#" + inst.id.replace(/\\\\/g, "\\"); + inst.dpDiv.find("[data-handler]").map(function () { + var handler = { + prev: function () { + $.datepicker._adjustDate(id, -stepMonths, "M"); + }, + next: function () { + $.datepicker._adjustDate(id, +stepMonths, "M"); + }, + hide: function () { + $.datepicker._hideDatepicker(); + }, + today: function () { + $.datepicker._gotoToday(id); + }, + selectDay: function () { + $.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this); + return false; + }, + selectMonth: function () { + $.datepicker._selectMonthYear(id, this, "M"); + return false; + }, + selectYear: function () { + $.datepicker._selectMonthYear(id, this, "Y"); + return false; + } + }; + $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]); + }); + }, + + /* Generate the HTML for the current state of the date picker. */ + _generateHTML: function (inst) { + var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, + controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, + monthNames, monthNamesShort, beforeShowDay, showOtherMonths, + selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, + cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, + printDate, dRow, tbody, daySettings, otherMonth, unselectable, + tempDate = new Date(), + today = this._daylightSavingAdjust( + new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time + isRTL = this._get(inst, "isRTL"), + showButtonPanel = this._get(inst, "showButtonPanel"), + hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"), + navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"), + numMonths = this._getNumberOfMonths(inst), + showCurrentAtPos = this._get(inst, "showCurrentAtPos"), + stepMonths = this._get(inst, "stepMonths"), + isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1), + currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : + new Date(inst.currentYear, inst.currentMonth, inst.currentDay))), + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + drawMonth = inst.drawMonth - showCurrentAtPos, + drawYear = inst.drawYear; + + if (drawMonth < 0) { + drawMonth += 12; + drawYear--; + } + if (maxDate) { + maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), + maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); + maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); + while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { + drawMonth--; + if (drawMonth < 0) { + drawMonth = 11; + drawYear--; + } + } + } + inst.drawMonth = drawMonth; + inst.drawYear = drawYear; + + prevText = this._get(inst, "prevText"); + prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), + this._getFormatConfig(inst))); + + prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? + "<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'" + + " title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + (isRTL ? "e" : "w") + "'>" + prevText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + (isRTL ? "e" : "w") + "'>" + prevText + "</span></a>")); + + nextText = this._get(inst, "nextText"); + nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), + this._getFormatConfig(inst))); + + next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? + "<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'" + + " title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + (isRTL ? "w" : "e") + "'>" + nextText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + (isRTL ? "w" : "e") + "'>" + nextText + "</span></a>")); + + currentText = this._get(inst, "currentText"); + gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today); + currentText = (!navigationAsDateFormat ? currentText : + this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); + + controls = (!inst.inline ? "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" + + this._get(inst, "closeText") + "</button>" : ""); + + buttonPanel = (showButtonPanel) ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (isRTL ? controls : "") + + (this._isInRange(inst, gotoDate) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'" + + ">" + currentText + "</button>" : "") + (isRTL ? "" : controls) + "</div>" : ""; + + firstDay = parseInt(this._get(inst, "firstDay"), 10); + firstDay = (isNaN(firstDay) ? 0 : firstDay); + + showWeek = this._get(inst, "showWeek"); + dayNames = this._get(inst, "dayNames"); + dayNamesMin = this._get(inst, "dayNamesMin"); + monthNames = this._get(inst, "monthNames"); + monthNamesShort = this._get(inst, "monthNamesShort"); + beforeShowDay = this._get(inst, "beforeShowDay"); + showOtherMonths = this._get(inst, "showOtherMonths"); + selectOtherMonths = this._get(inst, "selectOtherMonths"); + defaultDate = this._getDefaultDate(inst); + html = ""; + dow; + for (row = 0; row < numMonths[0]; row++) { + group = ""; + this.maxRows = 4; + for (col = 0; col < numMonths[1]; col++) { + selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); + cornerClass = " ui-corner-all"; + calender = ""; + if (isMultiMonth) { + calender += "<div class='ui-datepicker-group"; + if (numMonths[1] > 1) { + switch (col) { + case 0: + calender += " ui-datepicker-group-first"; + cornerClass = " ui-corner-" + (isRTL ? "right" : "left"); + break; + case numMonths[1] - 1: + calender += " ui-datepicker-group-last"; + cornerClass = " ui-corner-" + (isRTL ? "left" : "right"); + break; + default: + calender += " ui-datepicker-group-middle"; + cornerClass = ""; + break; + } + } + calender += "'>"; + } + calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" + + (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") + + (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") + + this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, + row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers + "</div><table class='ui-datepicker-calendar'><thead>" + + "<tr>"; + thead = (showWeek ? "<th class='ui-datepicker-week-col'>" + this._get(inst, "weekHeader") + "</th>" : ""); + for (dow = 0; dow < 7; dow++) { // days of the week + day = (dow + firstDay) % 7; + thead += "<th" + ((dow + firstDay + 6) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "") + ">" + + "<span title='" + dayNames[day] + "'>" + dayNamesMin[day] + "</span></th>"; + } + calender += thead + "</tr></thead><tbody>"; + daysInMonth = this._getDaysInMonth(drawYear, drawMonth); + if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) { + inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); + } + leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; + curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate + numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) + this.maxRows = numRows; + printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); + for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows + calender += "<tr>"; + tbody = (!showWeek ? "" : "<td class='ui-datepicker-week-col'>" + + this._get(inst, "calculateWeek")(printDate) + "</td>"); + for (dow = 0; dow < 7; dow++) { // create date picker days + daySettings = (beforeShowDay ? + beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]); + otherMonth = (printDate.getMonth() !== drawMonth); + unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || + (minDate && printDate < minDate) || (maxDate && printDate > maxDate); + tbody += "<td class='" + + ((dow + firstDay + 6) % 7 >= 5 ? " ui-datepicker-week-end" : "") + // highlight weekends + (otherMonth ? " ui-datepicker-other-month" : "") + // highlight days from other months + ((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key + (defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ? + // or defaultDate is current printedDate and defaultDate is selectedDate + " " + this._dayOverClass : "") + // highlight selected day + (unselectable ? " " + this._unselectableClass + " ui-state-disabled" : "") + // highlight unselectable days + (otherMonth && !showOtherMonths ? "" : " " + daySettings[1] + // highlight custom dates + (printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "") + // highlight selected day + (printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "")) + "'" + // highlight today (if different) + ((!otherMonth || showOtherMonths) && daySettings[2] ? " title='" + daySettings[2].replace(/'/g, "'") + "'" : "") + // cell title + (unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions + (otherMonth && !showOtherMonths ? " " : // display for other months + (unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" + + (printDate.getTime() === today.getTime() ? " ui-state-highlight" : "") + + (printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "") + // highlight selected day + (otherMonth ? " ui-priority-secondary" : "") + // distinguish dates from other months + "' href='#'>" + printDate.getDate() + "</a>")) + "</td>"; // display selectable date + printDate.setDate(printDate.getDate() + 1); + printDate = this._daylightSavingAdjust(printDate); + } + calender += tbody + "</tr>"; + } + drawMonth++; + if (drawMonth > 11) { + drawMonth = 0; + drawYear++; + } + calender += "</tbody></table>" + (isMultiMonth ? "</div>" + + ((numMonths[0] > 0 && col === numMonths[1] - 1) ? "<div class='ui-datepicker-row-break'></div>" : "") : ""); + group += calender; + } + html += group; + } + html += buttonPanel; + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function (inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + + var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, + changeMonth = this._get(inst, "changeMonth"), + changeYear = this._get(inst, "changeYear"), + showMonthAfterYear = this._get(inst, "showMonthAfterYear"), + html = "<div class='ui-datepicker-title'>", + monthHtml = ""; + + // month selection + if (secondary || !changeMonth) { + monthHtml += "<span class='ui-datepicker-month'>" + monthNames[drawMonth] + "</span>"; + } else { + inMinYear = (minDate && minDate.getFullYear() === drawYear); + inMaxYear = (maxDate && maxDate.getFullYear() === drawYear); + monthHtml += "<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>"; + for (month = 0; month < 12; month++) { + if ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) { + monthHtml += "<option value='" + month + "'" + + (month === drawMonth ? " selected='selected'" : "") + + ">" + monthNamesShort[month] + "</option>"; + } + } + monthHtml += "</select>"; + } + + if (!showMonthAfterYear) { + html += monthHtml + (secondary || !(changeMonth && changeYear) ? " " : ""); + } + + // year selection + if (!inst.yearshtml) { + inst.yearshtml = ""; + if (secondary || !changeYear) { + html += "<span class='ui-datepicker-year'>" + drawYear + "</span>"; + } else { + // determine range of years to display + years = this._get(inst, "yearRange").split(":"); + thisYear = new Date().getFullYear(); + determineYear = function (value) { + var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + year = determineYear(years[0]); + endYear = Math.max(year, determineYear(years[1] || "")); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>"; + for (; year <= endYear; year++) { + inst.yearshtml += "<option value='" + year + "'" + + (year === drawYear ? " selected='selected'" : "") + + ">" + year + "</option>"; + } + inst.yearshtml += "</select>"; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + + html += this._get(inst, "yearSuffix"); + if (showMonthAfterYear) { + html += (secondary || !(changeMonth && changeYear) ? " " : "") + monthHtml; + } + html += "</div>"; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function (inst, offset, period) { + var year = inst.drawYear + (period === "Y" ? offset : 0), + month = inst.drawMonth + (period === "M" ? offset : 0), + day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0), + date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day))); + + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period === "M" || period === "Y") { + this._notifyChange(inst); + } + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function (inst, date) { + var minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + newDate = (minDate && date < minDate ? minDate : date); + return (maxDate && newDate > maxDate ? maxDate : newDate); + }, + + /* Notify change of month/year. */ + _notifyChange: function (inst) { + var onChange = this._get(inst, "onChangeMonthYear"); + if (onChange) { + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + } + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function (inst) { + var numMonths = this._get(inst, "numberOfMonths"); + return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function (inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + "Date"), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function (year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function (year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function (inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst), + date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + + if (offset < 0) { + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + } + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function (inst, date) { + var yearSplit, currentYear, + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + minYear = null, + maxYear = null, + years = this._get(inst, "yearRange"); + if (years) { + yearSplit = years.split(":"); + currentYear = new Date().getFullYear(); + minYear = parseInt(yearSplit[0], 10); + maxYear = parseInt(yearSplit[1], 10); + if (yearSplit[0].match(/[+\-].*/)) { + minYear += currentYear; + } + if (yearSplit[1].match(/[+\-].*/)) { + maxYear += currentYear; + } + } + + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime()) && + (!minYear || date.getFullYear() >= minYear) && + (!maxYear || date.getFullYear() <= maxYear)); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function (inst) { + var shortYearCutoff = this._get(inst, "shortYearCutoff"); + shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return { + shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"), + monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames") + }; + }, + + /* Format the given date for display. */ + _formatDate: function (inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day === "object" ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst)); + } + }); + + /* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ + function bindHover(dpDiv) { + var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; + return dpDiv.delegate(selector, "mouseout", function () { + $(this).removeClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).removeClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).removeClass("ui-datepicker-next-hover"); + } + }) + .delegate(selector, "mouseover", function () { + if (!$.datepicker._isDisabledDatepicker(instActive.inline ? dpDiv.parent()[0] : instActive.input[0])) { + $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); + $(this).addClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).addClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).addClass("ui-datepicker-next-hover"); + } + } + }); + } + + /* jQuery extend now ignores nulls! */ + function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = props[name]; + } + } + return target; + } + + /* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ + $.fn.datepicker = function (options) { + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if (!this.length) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick); + $.datepicker.initialized = true; + } + + /* Append datepicker main container to body if not exist. */ + if ($("#" + $.datepicker._mainDivId).length === 0) { + $("body").append($.datepicker.dpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) { + return $.datepicker["_" + options + "Datepicker"].apply($.datepicker, [this[0]].concat(otherArgs)); + } + if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") { + return $.datepicker["_" + options + "Datepicker"].apply($.datepicker, [this[0]].concat(otherArgs)); + } + return this.each(function () { + typeof options === "string" ? + $.datepicker["_" + options + "Datepicker"].apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); + }; + + $.datepicker = new Datepicker(); // singleton instance + $.datepicker.initialized = false; + $.datepicker.uuid = new Date().getTime(); + $.datepicker.version = "1.10.4"; + +}); diff --git a/lib/web/jquery/ui-modules/dialog.js b/lib/web/jquery/ui-modules/dialog.js new file mode 100644 index 000000000000..430ec90534e9 --- /dev/null +++ b/lib/web/jquery/ui-modules/dialog.js @@ -0,0 +1,823 @@ +/*! + * jQuery UI Dialog - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/dialog/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/button', + 'jquery-ui-modules/draggable', + 'jquery-ui-modules/position', + 'jquery-ui-modules/resizable' +], function ($, undefined) { + + var sizeRelatedOptions = { + buttons: true, + height: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + width: true + }, + resizableRelatedOptions = { + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true + }; + + $.widget("ui.dialog", { + version: "1.10.4", + options: { + appendTo: "body", + autoOpen: true, + buttons: [], + closeOnEscape: true, + closeText: "close", + dialogClass: "", + draggable: true, + hide: null, + height: "auto", + maxHeight: null, + maxWidth: null, + minHeight: 150, + minWidth: 150, + modal: false, + position: { + my: "center", + at: "center", + of: window, + collision: "fit", + // Ensure the titlebar is always visible + using: function (pos) { + var topOffset = $(this).css(pos).offset().top; + if (topOffset < 0) { + $(this).css("top", pos.top - topOffset); + } + } + }, + resizable: true, + show: null, + title: null, + width: 300, + + // callbacks + beforeClose: null, + close: null, + drag: null, + dragStart: null, + dragStop: null, + focus: null, + open: null, + resize: null, + resizeStart: null, + resizeStop: null + }, + + _create: function () { + this.originalCss = { + display: this.element[0].style.display, + width: this.element[0].style.width, + minHeight: this.element[0].style.minHeight, + maxHeight: this.element[0].style.maxHeight, + height: this.element[0].style.height + }; + this.originalPosition = { + parent: this.element.parent(), + index: this.element.parent().children().index(this.element) + }; + this.originalTitle = this.element.attr("title"); + this.options.title = this.options.title || this.originalTitle; + + this._createWrapper(); + + this.element + .show() + .removeAttr("title") + .addClass("ui-dialog-content ui-widget-content") + .appendTo(this.uiDialog); + + this._createTitlebar(); + this._createButtonPane(); + + if (this.options.draggable && $.fn.draggable) { + this._makeDraggable(); + } + if (this.options.resizable && $.fn.resizable) { + this._makeResizable(); + } + + this._isOpen = false; + }, + + _init: function () { + if (this.options.autoOpen) { + this.open(); + } + }, + + _appendTo: function () { + var element = this.options.appendTo; + if (element && (element.jquery || element.nodeType)) { + return $(element); + } + return this.document.find(element || "body").eq(0); + }, + + _destroy: function () { + var next, + originalPosition = this.originalPosition; + + this._destroyOverlay(); + + this.element + .removeUniqueId() + .removeClass("ui-dialog-content ui-widget-content") + .css(this.originalCss) + // Without detaching first, the following becomes really slow + .detach(); + + this.uiDialog.stop(true, true).remove(); + + if (this.originalTitle) { + this.element.attr("title", this.originalTitle); + } + + next = originalPosition.parent.children().eq(originalPosition.index); + // Don't try to place the dialog next to itself (#8613) + if (next.length && next[0] !== this.element[0]) { + next.before(this.element); + } else { + originalPosition.parent.append(this.element); + } + }, + + widget: function () { + return this.uiDialog; + }, + + disable: $.noop, + enable: $.noop, + + close: function (event) { + var activeElement, + that = this; + + if (!this._isOpen || this._trigger("beforeClose", event) === false) { + return; + } + + this._isOpen = false; + this._destroyOverlay(); + + if (!this.opener.filter(":focusable").focus().length) { + + // support: IE9 + // IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe> + try { + activeElement = this.document[0].activeElement; + + // Support: IE9, IE10 + // If the <body> is blurred, IE will switch windows, see #4520 + if (activeElement && activeElement.nodeName.toLowerCase() !== "body") { + + // Hiding a focused element doesn't trigger blur in WebKit + // so in case we have nothing to focus on, explicitly blur the active element + // https://bugs.webkit.org/show_bug.cgi?id=47182 + $(activeElement).blur(); + } + } catch (error) { + } + } + + this._hide(this.uiDialog, this.options.hide, function () { + that._trigger("close", event); + }); + }, + + isOpen: function () { + return this._isOpen; + }, + + moveToTop: function () { + this._moveToTop(); + }, + + _moveToTop: function (event, silent) { + var moved = !!this.uiDialog.nextAll(":visible").insertBefore(this.uiDialog).length; + if (moved && !silent) { + this._trigger("focus", event); + } + return moved; + }, + + open: function () { + var that = this; + if (this._isOpen) { + if (this._moveToTop()) { + this._focusTabbable(); + } + return; + } + + this._isOpen = true; + this.opener = $(this.document[0].activeElement); + + this._size(); + this._position(); + this._createOverlay(); + this._moveToTop(null, true); + this._show(this.uiDialog, this.options.show, function () { + that._focusTabbable(); + that._trigger("focus"); + }); + + this._trigger("open"); + }, + + _focusTabbable: function () { + // Set focus to the first match: + // 1. First element inside the dialog matching [autofocus] + // 2. Tabbable element inside the content element + // 3. Tabbable element inside the buttonpane + // 4. The close button + // 5. The dialog itself + var hasFocus = this.element.find("[autofocus]"); + if (!hasFocus.length) { + hasFocus = this.element.find(":tabbable"); + } + if (!hasFocus.length) { + hasFocus = this.uiDialogButtonPane.find(":tabbable"); + } + if (!hasFocus.length) { + hasFocus = this.uiDialogTitlebarClose.filter(":tabbable"); + } + if (!hasFocus.length) { + hasFocus = this.uiDialog; + } + hasFocus.eq(0).focus(); + }, + + _keepFocus: function (event) { + function checkFocus() { + var activeElement = this.document[0].activeElement, + isActive = this.uiDialog[0] === activeElement || + $.contains(this.uiDialog[0], activeElement); + if (!isActive) { + this._focusTabbable(); + } + } + + event.preventDefault(); + checkFocus.call(this); + // support: IE + // IE <= 8 doesn't prevent moving focus even with event.preventDefault() + // so we check again later + this._delay(checkFocus); + }, + + _createWrapper: function () { + this.uiDialog = $("<div>") + .addClass("ui-dialog ui-widget ui-widget-content ui-corner-all ui-front " + + this.options.dialogClass) + .hide() + .attr({ + // Setting tabIndex makes the div focusable + tabIndex: -1, + role: "dialog" + }) + .appendTo(this._appendTo()); + + this._on(this.uiDialog, { + keydown: function (event) { + if (this.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE) { + event.preventDefault(); + this.close(event); + return; + } + + // prevent tabbing out of dialogs + if (event.keyCode !== $.ui.keyCode.TAB) { + return; + } + var tabbables = this.uiDialog.find(":tabbable"), + first = tabbables.filter(":first"), + last = tabbables.filter(":last"); + + if ((event.target === last[0] || event.target === this.uiDialog[0]) && !event.shiftKey) { + first.focus(1); + event.preventDefault(); + } else if ((event.target === first[0] || event.target === this.uiDialog[0]) && event.shiftKey) { + last.focus(1); + event.preventDefault(); + } + }, + mousedown: function (event) { + if (this._moveToTop(event)) { + this._focusTabbable(); + } + } + }); + + // We assume that any existing aria-describedby attribute means + // that the dialog content is marked up properly + // otherwise we brute force the content as the description + if (!this.element.find("[aria-describedby]").length) { + this.uiDialog.attr({ + "aria-describedby": this.element.uniqueId().attr("id") + }); + } + }, + + _createTitlebar: function () { + var uiDialogTitle; + + this.uiDialogTitlebar = $("<div>") + .addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix") + .prependTo(this.uiDialog); + this._on(this.uiDialogTitlebar, { + mousedown: function (event) { + // Don't prevent click on close button (#8838) + // Focusing a dialog that is partially scrolled out of view + // causes the browser to scroll it into view, preventing the click event + if (!$(event.target).closest(".ui-dialog-titlebar-close")) { + // Dialog isn't getting focus when dragging (#8063) + this.uiDialog.focus(); + } + } + }); + + // support: IE + // Use type="button" to prevent enter keypresses in textboxes from closing the + // dialog in IE (#9312) + this.uiDialogTitlebarClose = $("<button type='button'></button>") + .button({ + label: this.options.closeText, + icons: { + primary: "ui-icon-closethick" + }, + text: false + }) + .addClass("ui-dialog-titlebar-close") + .appendTo(this.uiDialogTitlebar); + this._on(this.uiDialogTitlebarClose, { + click: function (event) { + event.preventDefault(); + this.close(event); + } + }); + + uiDialogTitle = $("<span>") + .uniqueId() + .addClass("ui-dialog-title") + .prependTo(this.uiDialogTitlebar); + this._title(uiDialogTitle); + + this.uiDialog.attr({ + "aria-labelledby": uiDialogTitle.attr("id") + }); + }, + + _title: function (title) { + if (!this.options.title) { + title.html(" "); + } + title.text(this.options.title); + }, + + _createButtonPane: function () { + this.uiDialogButtonPane = $("<div>") + .addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"); + + this.uiButtonSet = $("<div>") + .addClass("ui-dialog-buttonset") + .appendTo(this.uiDialogButtonPane); + + this._createButtons(); + }, + + _createButtons: function () { + var that = this, + buttons = this.options.buttons; + + // if we already have a button pane, remove it + this.uiDialogButtonPane.remove(); + this.uiButtonSet.empty(); + + if ($.isEmptyObject(buttons) || ($.isArray(buttons) && !buttons.length)) { + this.uiDialog.removeClass("ui-dialog-buttons"); + return; + } + + $.each(buttons, function (name, props) { + var click, buttonOptions; + props = $.isFunction(props) ? + {click: props, text: name} : + props; + // Default to a non-submitting button + props = $.extend({type: "button"}, props); + // Change the context for the click callback to be the main element + click = props.click; + props.click = function () { + click.apply(that.element[0], arguments); + }; + buttonOptions = { + icons: props.icons, + text: props.showText + }; + delete props.icons; + delete props.showText; + $("<button></button>", props) + .button(buttonOptions) + .appendTo(that.uiButtonSet); + }); + this.uiDialog.addClass("ui-dialog-buttons"); + this.uiDialogButtonPane.appendTo(this.uiDialog); + }, + + _makeDraggable: function () { + var that = this, + options = this.options; + + function filteredUi(ui) { + return { + position: ui.position, + offset: ui.offset + }; + } + + this.uiDialog.draggable({ + cancel: ".ui-dialog-content, .ui-dialog-titlebar-close", + handle: ".ui-dialog-titlebar", + containment: "document", + start: function (event, ui) { + $(this).addClass("ui-dialog-dragging"); + that._blockFrames(); + that._trigger("dragStart", event, filteredUi(ui)); + }, + drag: function (event, ui) { + that._trigger("drag", event, filteredUi(ui)); + }, + stop: function (event, ui) { + options.position = [ + ui.position.left - that.document.scrollLeft(), + ui.position.top - that.document.scrollTop() + ]; + $(this).removeClass("ui-dialog-dragging"); + that._unblockFrames(); + that._trigger("dragStop", event, filteredUi(ui)); + } + }); + }, + + _makeResizable: function () { + var that = this, + options = this.options, + handles = options.resizable, + // .ui-resizable has position: relative defined in the stylesheet + // but dialogs have to use absolute or fixed positioning + position = this.uiDialog.css("position"), + resizeHandles = typeof handles === "string" ? + handles : + "n,e,s,w,se,sw,ne,nw"; + + function filteredUi(ui) { + return { + originalPosition: ui.originalPosition, + originalSize: ui.originalSize, + position: ui.position, + size: ui.size + }; + } + + this.uiDialog.resizable({ + cancel: ".ui-dialog-content", + containment: "document", + alsoResize: this.element, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + minWidth: options.minWidth, + minHeight: this._minHeight(), + handles: resizeHandles, + start: function (event, ui) { + $(this).addClass("ui-dialog-resizing"); + that._blockFrames(); + that._trigger("resizeStart", event, filteredUi(ui)); + }, + resize: function (event, ui) { + that._trigger("resize", event, filteredUi(ui)); + }, + stop: function (event, ui) { + options.height = $(this).height(); + options.width = $(this).width(); + $(this).removeClass("ui-dialog-resizing"); + that._unblockFrames(); + that._trigger("resizeStop", event, filteredUi(ui)); + } + }) + .css("position", position); + }, + + _minHeight: function () { + var options = this.options; + + return options.height === "auto" ? + options.minHeight : + Math.min(options.minHeight, options.height); + }, + + _position: function () { + // Need to show the dialog to get the actual offset in the position plugin + var isVisible = this.uiDialog.is(":visible"); + if (!isVisible) { + this.uiDialog.show(); + } + this.uiDialog.position(this.options.position); + if (!isVisible) { + this.uiDialog.hide(); + } + }, + + _setOptions: function (options) { + var that = this, + resize = false, + resizableOptions = {}; + + $.each(options, function (key, value) { + that._setOption(key, value); + + if (key in sizeRelatedOptions) { + resize = true; + } + if (key in resizableRelatedOptions) { + resizableOptions[key] = value; + } + }); + + if (resize) { + this._size(); + this._position(); + } + if (this.uiDialog.is(":data(ui-resizable)")) { + this.uiDialog.resizable("option", resizableOptions); + } + }, + + _setOption: function (key, value) { + var isDraggable, isResizable, + uiDialog = this.uiDialog; + + if (key === "dialogClass") { + uiDialog + .removeClass(this.options.dialogClass) + .addClass(value); + } + + if (key === "disabled") { + return; + } + + this._super(key, value); + + if (key === "appendTo") { + this.uiDialog.appendTo(this._appendTo()); + } + + if (key === "buttons") { + this._createButtons(); + } + + if (key === "closeText") { + this.uiDialogTitlebarClose.button({ + // Ensure that we always pass a string + label: "" + value + }); + } + + if (key === "draggable") { + isDraggable = uiDialog.is(":data(ui-draggable)"); + if (isDraggable && !value) { + uiDialog.draggable("destroy"); + } + + if (!isDraggable && value) { + this._makeDraggable(); + } + } + + if (key === "position") { + this._position(); + } + + if (key === "resizable") { + // currently resizable, becoming non-resizable + isResizable = uiDialog.is(":data(ui-resizable)"); + if (isResizable && !value) { + uiDialog.resizable("destroy"); + } + + // currently resizable, changing handles + if (isResizable && typeof value === "string") { + uiDialog.resizable("option", "handles", value); + } + + // currently non-resizable, becoming resizable + if (!isResizable && value !== false) { + this._makeResizable(); + } + } + + if (key === "title") { + this._title(this.uiDialogTitlebar.find(".ui-dialog-title")); + } + }, + + _size: function () { + // If the user has resized the dialog, the .ui-dialog and .ui-dialog-content + // divs will both have width and height set, so we need to reset them + var nonContentHeight, minContentHeight, maxContentHeight, + options = this.options; + + // Reset content sizing + this.element.show().css({ + width: "auto", + minHeight: 0, + maxHeight: "none", + height: 0 + }); + + if (options.minWidth > options.width) { + options.width = options.minWidth; + } + + // reset wrapper sizing + // determine the height of all the non-content elements + nonContentHeight = this.uiDialog.css({ + height: "auto", + width: options.width + }) + .outerHeight(); + minContentHeight = Math.max(0, options.minHeight - nonContentHeight); + maxContentHeight = typeof options.maxHeight === "number" ? + Math.max(0, options.maxHeight - nonContentHeight) : + "none"; + + if (options.height === "auto") { + this.element.css({ + minHeight: minContentHeight, + maxHeight: maxContentHeight, + height: "auto" + }); + } else { + this.element.height(Math.max(0, options.height - nonContentHeight)); + } + + if (this.uiDialog.is(":data(ui-resizable)")) { + this.uiDialog.resizable("option", "minHeight", this._minHeight()); + } + }, + + _blockFrames: function () { + this.iframeBlocks = this.document.find("iframe").map(function () { + var iframe = $(this); + + return $("<div>") + .css({ + position: "absolute", + width: iframe.outerWidth(), + height: iframe.outerHeight() + }) + .appendTo(iframe.parent()) + .offset(iframe.offset())[0]; + }); + }, + + _unblockFrames: function () { + if (this.iframeBlocks) { + this.iframeBlocks.remove(); + delete this.iframeBlocks; + } + }, + + _allowInteraction: function (event) { + if ($(event.target).closest(".ui-dialog").length) { + return true; + } + + // TODO: Remove hack when datepicker implements + // the .ui-front logic (#8989) + return !!$(event.target).closest(".ui-datepicker").length; + }, + + _createOverlay: function () { + if (!this.options.modal) { + return; + } + + var that = this, + widgetFullName = this.widgetFullName; + if (!$.ui.dialog.overlayInstances) { + // Prevent use of anchors and inputs. + // We use a delay in case the overlay is created from an + // event that we're going to be cancelling. (#2804) + this._delay(function () { + // Handle .dialog().dialog("close") (#4065) + if ($.ui.dialog.overlayInstances) { + this.document.bind("focusin.dialog", function (event) { + if (!that._allowInteraction(event)) { + event.preventDefault(); + $(".ui-dialog:visible:last .ui-dialog-content") + .data(widgetFullName)._focusTabbable(); + } + }); + } + }); + } + + this.overlay = $("<div>") + .addClass("ui-widget-overlay ui-front") + .appendTo(this._appendTo()); + this._on(this.overlay, { + mousedown: "_keepFocus" + }); + $.ui.dialog.overlayInstances++; + }, + + _destroyOverlay: function () { + if (!this.options.modal) { + return; + } + + if (this.overlay) { + $.ui.dialog.overlayInstances--; + + if (!$.ui.dialog.overlayInstances) { + this.document.unbind("focusin.dialog"); + } + this.overlay.remove(); + this.overlay = null; + } + } + }); + + $.ui.dialog.overlayInstances = 0; + +// DEPRECATED + if ($.uiBackCompat !== false) { + // position option with array notation + // just override with old implementation + $.widget("ui.dialog", $.ui.dialog, { + _position: function () { + var position = this.options.position, + myAt = [], + offset = [0, 0], + isVisible; + + if (position) { + if (typeof position === "string" || (typeof position === "object" && "0" in position)) { + myAt = position.split ? position.split(" ") : [position[0], position[1]]; + if (myAt.length === 1) { + myAt[1] = myAt[0]; + } + + $.each(["left", "top"], function (i, offsetPosition) { + if (+myAt[i] === myAt[i]) { + offset[i] = myAt[i]; + myAt[i] = offsetPosition; + } + }); + + position = { + my: myAt[0] + (offset[0] < 0 ? offset[0] : "+" + offset[0]) + " " + + myAt[1] + (offset[1] < 0 ? offset[1] : "+" + offset[1]), + at: myAt.join(" ") + }; + } + + position = $.extend({}, $.ui.dialog.prototype.options.position, position); + } else { + position = $.ui.dialog.prototype.options.position; + } + + // need to show the dialog to get the actual offset in the position plugin + isVisible = this.uiDialog.is(":visible"); + if (!isVisible) { + this.uiDialog.show(); + } + this.uiDialog.position(position); + if (!isVisible) { + this.uiDialog.hide(); + } + } + }); + } + +}); diff --git a/lib/web/jquery/ui-modules/draggable.js b/lib/web/jquery/ui-modules/draggable.js new file mode 100644 index 000000000000..050ff32c9cf0 --- /dev/null +++ b/lib/web/jquery/ui-modules/draggable.js @@ -0,0 +1,980 @@ +/*! + * jQuery UI Draggable - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/draggable/ + * + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/mouse' +], function ($, undefined) { + + $.widget("ui.draggable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "drag", + options: { + addClasses: true, + appendTo: "parent", + axis: false, + connectToSortable: false, + containment: false, + cursor: "auto", + cursorAt: false, + grid: false, + handle: false, + helper: "original", + iframeFix: false, + opacity: false, + refreshPositions: false, + revert: false, + revertDuration: 500, + scope: "default", + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + snap: false, + snapMode: "both", + snapTolerance: 20, + stack: false, + zIndex: false, + + // callbacks + drag: null, + start: null, + stop: null + }, + _create: function () { + + if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { + this.element[0].style.position = "relative"; + } + if (this.options.addClasses) { + this.element.addClass("ui-draggable"); + } + if (this.options.disabled) { + this.element.addClass("ui-draggable-disabled"); + } + + this._mouseInit(); + + }, + + _destroy: function () { + this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"); + this._mouseDestroy(); + }, + + _mouseCapture: function (event) { + + var o = this.options; + + // among others, prevent a drag on a resizable-handle + if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { + return false; + } + + //Quit if we're not on a valid handle + this.handle = this._getHandle(event); + if (!this.handle) { + return false; + } + + $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function () { + $("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>") + .css({ + width: this.offsetWidth + "px", height: this.offsetHeight + "px", + position: "absolute", opacity: "0.001", zIndex: 1000 + }) + .css($(this).offset()) + .appendTo("body"); + }); + + return true; + + }, + + _mouseStart: function (event) { + + var o = this.options; + + //Create and append the visible helper + this.helper = this._createHelper(event); + + this.helper.addClass("ui-draggable-dragging"); + + //Cache the helper size + this._cacheHelperProportions(); + + //If ddmanager is used for droppables, set the global draggable + if ($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Store the helper's css position + this.cssPosition = this.helper.css("position"); + this.scrollParent = this.helper.scrollParent(); + this.offsetParent = this.helper.offsetParent(); + this.offsetParentCssPosition = this.offsetParent.css("position"); + + //The element's absolute position on the page minus margins + this.offset = this.positionAbs = this.element.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + //Reset scroll cache + this.offset.scroll = false; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + //Generate the original position + this.originalPosition = this.position = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Set a containment if given in the options + this._setContainment(); + + //Trigger event + callbacks + if (this._trigger("start", event) === false) { + this._clear(); + return false; + } + + //Recache the helper size + this._cacheHelperProportions(); + + //Prepare the droppable offsets + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + + this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position + + //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) + if ($.ui.ddmanager) { + $.ui.ddmanager.dragStart(this, event); + } + + return true; + }, + + _mouseDrag: function (event, noPropagation) { + // reset any necessary cached properties (see #5009) + if (this.offsetParentCssPosition === "fixed") { + this.offset.parent = this._getParentOffset(); + } + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + //Call plugins and callbacks and use the resulting position if something is returned + if (!noPropagation) { + var ui = this._uiHash(); + if (this._trigger("drag", event, ui) === false) { + this._mouseUp({}); + return false; + } + this.position = ui.position; + } + + if (!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left + "px"; + } + if (!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top + "px"; + } + if ($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + return false; + }, + + _mouseStop: function (event) { + + //If we are using droppables, inform the manager about the drop + var that = this, + dropped = false; + if ($.ui.ddmanager && !this.options.dropBehaviour) { + dropped = $.ui.ddmanager.drop(this, event); + } + + //if a drop comes from outside (a sortable) + if (this.dropped) { + dropped = this.dropped; + this.dropped = false; + } + + //if the original element is no longer in the DOM don't bother to continue (see #8269) + if (this.options.helper === "original" && !$.contains(this.element[0].ownerDocument, this.element[0])) { + return false; + } + + if ((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { + $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function () { + if (that._trigger("stop", event) !== false) { + that._clear(); + } + }); + } else { + if (this._trigger("stop", event) !== false) { + this._clear(); + } + } + + return false; + }, + + _mouseUp: function (event) { + //Remove frame helpers + $("div.ui-draggable-iframeFix").each(function () { + this.parentNode.removeChild(this); + }); + + //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) + if ($.ui.ddmanager) { + $.ui.ddmanager.dragStop(this, event); + } + + return $.ui.mouse.prototype._mouseUp.call(this, event); + }, + + cancel: function () { + + if (this.helper.is(".ui-draggable-dragging")) { + this._mouseUp({}); + } else { + this._clear(); + } + + return this; + + }, + + _getHandle: function (event) { + return this.options.handle ? + !!$(event.target).closest(this.element.find(this.options.handle)).length : + true; + }, + + _createHelper: function (event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); + + if (!helper.parents("body").length) { + helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); + } + + if (helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { + helper.css("position", "absolute"); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function (obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function () { + + //Get the offsetParent and cache its position + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if (this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + //This needs to be actually done for all browsers, since pageX/pageY includes this information + //Ugly IE fix + if ((this.offsetParent[0] === document.body) || + (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = {top: 0, left: 0}; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"), 10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"), 10) || 0) + }; + + }, + + _getRelativeOffset: function () { + + if (this.cssPosition === "relative") { + var p = this.element.position(); + return { + top: p.top - (parseInt(this.helper.css("top"), 10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"), 10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return {top: 0, left: 0}; + } + + }, + + _cacheMargins: function () { + this.margins = { + left: (parseInt(this.element.css("marginLeft"), 10) || 0), + top: (parseInt(this.element.css("marginTop"), 10) || 0), + right: (parseInt(this.element.css("marginRight"), 10) || 0), + bottom: (parseInt(this.element.css("marginBottom"), 10) || 0) + }; + }, + + _cacheHelperProportions: function () { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function () { + + var over, c, ce, + o = this.options; + + if (!o.containment) { + this.containment = null; + return; + } + + if (o.containment === "window") { + this.containment = [ + $(window).scrollLeft() - this.offset.relative.left - this.offset.parent.left, + $(window).scrollTop() - this.offset.relative.top - this.offset.parent.top, + $(window).scrollLeft() + $(window).width() - this.helperProportions.width - this.margins.left, + $(window).scrollTop() + ($(window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if (o.containment === "document") { + this.containment = [ + 0, + 0, + $(document).width() - this.helperProportions.width - this.margins.left, + ($(document).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if (o.containment.constructor === Array) { + this.containment = o.containment; + return; + } + + if (o.containment === "parent") { + o.containment = this.helper[0].parentNode; + } + + c = $(o.containment); + ce = c[0]; + + if (!ce) { + return; + } + + over = c.css("overflow") !== "hidden"; + + this.containment = [ + (parseInt(c.css("borderLeftWidth"), 10) || 0) + (parseInt(c.css("paddingLeft"), 10) || 0), + (parseInt(c.css("borderTopWidth"), 10) || 0) + (parseInt(c.css("paddingTop"), 10) || 0), + (over ? Math.max(ce.scrollWidth, ce.offsetWidth) : ce.offsetWidth) - (parseInt(c.css("borderRightWidth"), 10) || 0) - (parseInt(c.css("paddingRight"), 10) || 0) - this.helperProportions.width - this.margins.left - this.margins.right, + (over ? Math.max(ce.scrollHeight, ce.offsetHeight) : ce.offsetHeight) - (parseInt(c.css("borderBottomWidth"), 10) || 0) - (parseInt(c.css("paddingBottom"), 10) || 0) - this.helperProportions.height - this.margins.top - this.margins.bottom + ]; + this.relative_container = c; + }, + + _convertPositionTo: function (d, pos) { + + if (!pos) { + pos = this.position; + } + + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top: scroll.scrollTop(), left: scroll.scrollLeft()}; + } + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ((this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top) * mod) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ((this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left) * mod) + ) + }; + + }, + + _generatePosition: function (event) { + + var containment, co, top, left, + o = this.options, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, + pageX = event.pageX, + pageY = event.pageY; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top: scroll.scrollTop(), left: scroll.scrollLeft()}; + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + // If we are not dragging yet, we won't check for options + if (this.originalPosition) { + if (this.containment) { + if (this.relative_container) { + co = this.relative_container.offset(); + containment = [ + this.containment[0] + co.left, + this.containment[1] + co.top, + this.containment[2] + co.left, + this.containment[3] + co.top + ]; + } else { + containment = this.containment; + } + + if (event.pageX - this.offset.click.left < containment[0]) { + pageX = containment[0] + this.offset.click.left; + } + if (event.pageY - this.offset.click.top < containment[1]) { + pageY = containment[1] + this.offset.click.top; + } + if (event.pageX - this.offset.click.left > containment[2]) { + pageX = containment[2] + this.offset.click.left; + } + if (event.pageY - this.offset.click.top > containment[3]) { + pageY = containment[3] + this.offset.click.top; + } + } + + if (o.grid) { + //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) + top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; + pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; + pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + (this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + (this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left) + ) + }; + + }, + + _clear: function () { + this.helper.removeClass("ui-draggable-dragging"); + if (this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { + this.helper.remove(); + } + this.helper = null; + this.cancelHelperRemoval = false; + }, + + // From now on bulk stuff - mainly helpers + + _trigger: function (type, event, ui) { + ui = ui || this._uiHash(); + $.ui.plugin.call(this, type, [event, ui]); + //The absolute position has to be recalculated after plugins + if (type === "drag") { + this.positionAbs = this._convertPositionTo("absolute"); + } + return $.Widget.prototype._trigger.call(this, type, event, ui); + }, + + plugins: {}, + + _uiHash: function () { + return { + helper: this.helper, + position: this.position, + originalPosition: this.originalPosition, + offset: this.positionAbs + }; + } + + }); + + $.ui.plugin.add("draggable", "connectToSortable", { + start: function (event, ui) { + + var inst = $(this).data("ui-draggable"), o = inst.options, + uiSortable = $.extend({}, ui, {item: inst.element}); + inst.sortables = []; + $(o.connectToSortable).each(function () { + var sortable = $.data(this, "ui-sortable"); + if (sortable && !sortable.options.disabled) { + inst.sortables.push({ + instance: sortable, + shouldRevert: sortable.options.revert + }); + sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). + sortable._trigger("activate", event, uiSortable); + } + }); + + }, + stop: function (event, ui) { + + //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper + var inst = $(this).data("ui-draggable"), + uiSortable = $.extend({}, ui, {item: inst.element}); + + $.each(inst.sortables, function () { + if (this.instance.isOver) { + + this.instance.isOver = 0; + + inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance + this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) + + //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" + if (this.shouldRevert) { + this.instance.options.revert = this.shouldRevert; + } + + //Trigger the stop of the sortable + this.instance._mouseStop(event); + + this.instance.options.helper = this.instance.options._helper; + + //If the helper has been the original item, restore properties in the sortable + if (inst.options.helper === "original") { + this.instance.currentItem.css({top: "auto", left: "auto"}); + } + + } else { + this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance + this.instance._trigger("deactivate", event, uiSortable); + } + + }); + + }, + drag: function (event, ui) { + + var inst = $(this).data("ui-draggable"), that = this; + + $.each(inst.sortables, function () { + + var innermostIntersecting = false, + thisSortable = this; + + //Copy over some variables to allow calling the sortable's native _intersectsWith + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + + if (this.instance._intersectsWith(this.instance.containerCache)) { + innermostIntersecting = true; + $.each(inst.sortables, function () { + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + if (this !== thisSortable && + this.instance._intersectsWith(this.instance.containerCache) && + $.contains(thisSortable.instance.element[0], this.instance.element[0]) + ) { + innermostIntersecting = false; + } + return innermostIntersecting; + }); + } + + + if (innermostIntersecting) { + //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once + if (!this.instance.isOver) { + + this.instance.isOver = 1; + //Now we fake the start of dragging for the sortable instance, + //by cloning the list group item, appending it to the sortable and using it as inst.currentItem + //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) + this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); + this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it + this.instance.options.helper = function () { + return ui.helper[0]; + }; + + event.target = this.instance.currentItem[0]; + this.instance._mouseCapture(event, true); + this.instance._mouseStart(event, true, true); + + //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes + this.instance.offset.click.top = inst.offset.click.top; + this.instance.offset.click.left = inst.offset.click.left; + this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; + this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; + + inst._trigger("toSortable", event); + inst.dropped = this.instance.element; //draggable revert needs that + //hack so receive/update callbacks work (mostly) + inst.currentItem = inst.element; + this.instance.fromOutside = inst; + + } + + //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable + if (this.instance.currentItem) { + this.instance._mouseDrag(event); + } + + } else { + + //If it doesn't intersect with the sortable, and it intersected before, + //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval + if (this.instance.isOver) { + + this.instance.isOver = 0; + this.instance.cancelHelperRemoval = true; + + //Prevent reverting on this forced stop + this.instance.options.revert = false; + + // The out event needs to be triggered independently + this.instance._trigger("out", event, this.instance._uiHash(this.instance)); + + this.instance._mouseStop(event, true); + this.instance.options.helper = this.instance.options._helper; + + //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size + this.instance.currentItem.remove(); + if (this.instance.placeholder) { + this.instance.placeholder.remove(); + } + + inst._trigger("fromSortable", event); + inst.dropped = false; //draggable revert needs that + } + + } + + }); + + } + }); + + $.ui.plugin.add("draggable", "cursor", { + start: function () { + var t = $("body"), o = $(this).data("ui-draggable").options; + if (t.css("cursor")) { + o._cursor = t.css("cursor"); + } + t.css("cursor", o.cursor); + }, + stop: function () { + var o = $(this).data("ui-draggable").options; + if (o._cursor) { + $("body").css("cursor", o._cursor); + } + } + }); + + $.ui.plugin.add("draggable", "opacity", { + start: function (event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if (t.css("opacity")) { + o._opacity = t.css("opacity"); + } + t.css("opacity", o.opacity); + }, + stop: function (event, ui) { + var o = $(this).data("ui-draggable").options; + if (o._opacity) { + $(ui.helper).css("opacity", o._opacity); + } + } + }); + + $.ui.plugin.add("draggable", "scroll", { + start: function () { + var i = $(this).data("ui-draggable"); + if (i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + i.overflowOffset = i.scrollParent.offset(); + } + }, + drag: function (event) { + + var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; + + if (i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + + if (!o.axis || o.axis !== "x") { + if ((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; + } else if (event.pageY - i.overflowOffset.top < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; + } + } + + if (!o.axis || o.axis !== "y") { + if ((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if (event.pageX - i.overflowOffset.left < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; + } + } + + } else { + + if (!o.axis || o.axis !== "x") { + if (event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if ($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + } + + if (!o.axis || o.axis !== "y") { + if (event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if ($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + } + + } + + if (scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(i, event); + } + + } + }); + + $.ui.plugin.add("draggable", "snap", { + start: function () { + + var i = $(this).data("ui-draggable"), + o = i.options; + + i.snapElements = []; + + $(o.snap.constructor !== String ? (o.snap.items || ":data(ui-draggable)") : o.snap).each(function () { + var $t = $(this), + $o = $t.offset(); + if (this !== i.element[0]) { + i.snapElements.push({ + item: this, + width: $t.outerWidth(), height: $t.outerHeight(), + top: $o.top, left: $o.left + }); + } + }); + + }, + drag: function (event, ui) { + + var ts, bs, ls, rs, l, r, t, b, i, first, + inst = $(this).data("ui-draggable"), + o = inst.options, + d = o.snapTolerance, + x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, + y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; + + for (i = inst.snapElements.length - 1; i >= 0; i--) { + + l = inst.snapElements[i].left; + r = l + inst.snapElements[i].width; + t = inst.snapElements[i].top; + b = t + inst.snapElements[i].height; + + if (x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains(inst.snapElements[i].item.ownerDocument, inst.snapElements[i].item)) { + if (inst.snapElements[i].snapping) { + (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), {snapItem: inst.snapElements[i].item}))); + } + inst.snapElements[i].snapping = false; + continue; + } + + if (o.snapMode !== "inner") { + ts = Math.abs(t - y2) <= d; + bs = Math.abs(b - y1) <= d; + ls = Math.abs(l - x2) <= d; + rs = Math.abs(r - x1) <= d; + if (ts) { + ui.position.top = inst._convertPositionTo("relative", { + top: t - inst.helperProportions.height, + left: 0 + }).top - inst.margins.top; + } + if (bs) { + ui.position.top = inst._convertPositionTo("relative", {top: b, left: 0}).top - inst.margins.top; + } + if (ls) { + ui.position.left = inst._convertPositionTo("relative", { + top: 0, + left: l - inst.helperProportions.width + }).left - inst.margins.left; + } + if (rs) { + ui.position.left = inst._convertPositionTo("relative", { + top: 0, + left: r + }).left - inst.margins.left; + } + } + + first = (ts || bs || ls || rs); + + if (o.snapMode !== "outer") { + ts = Math.abs(t - y1) <= d; + bs = Math.abs(b - y2) <= d; + ls = Math.abs(l - x1) <= d; + rs = Math.abs(r - x2) <= d; + if (ts) { + ui.position.top = inst._convertPositionTo("relative", {top: t, left: 0}).top - inst.margins.top; + } + if (bs) { + ui.position.top = inst._convertPositionTo("relative", { + top: b - inst.helperProportions.height, + left: 0 + }).top - inst.margins.top; + } + if (ls) { + ui.position.left = inst._convertPositionTo("relative", { + top: 0, + left: l + }).left - inst.margins.left; + } + if (rs) { + ui.position.left = inst._convertPositionTo("relative", { + top: 0, + left: r - inst.helperProportions.width + }).left - inst.margins.left; + } + } + + if (!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { + (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), {snapItem: inst.snapElements[i].item}))); + } + inst.snapElements[i].snapping = (ts || bs || ls || rs || first); + + } + + } + }); + + $.ui.plugin.add("draggable", "stack", { + start: function () { + var min, + o = this.data("ui-draggable").options, + group = $.makeArray($(o.stack)).sort(function (a, b) { + return (parseInt($(a).css("zIndex"), 10) || 0) - (parseInt($(b).css("zIndex"), 10) || 0); + }); + + if (!group.length) { + return; + } + + min = parseInt($(group[0]).css("zIndex"), 10) || 0; + $(group).each(function (i) { + $(this).css("zIndex", min + i); + }); + this.css("zIndex", (min + group.length)); + } + }); + + $.ui.plugin.add("draggable", "zIndex", { + start: function (event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if (t.css("zIndex")) { + o._zIndex = t.css("zIndex"); + } + t.css("zIndex", o.zIndex); + }, + stop: function (event, ui) { + var o = $(this).data("ui-draggable").options; + if (o._zIndex) { + $(ui.helper).css("zIndex", o._zIndex); + } + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/droppable.js b/lib/web/jquery/ui-modules/droppable.js new file mode 100644 index 000000000000..726874a393ed --- /dev/null +++ b/lib/web/jquery/ui-modules/droppable.js @@ -0,0 +1,390 @@ +/*! + * jQuery UI Droppable - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/droppable/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/draggable' +], function ($, undefined) { + + function isOverAxis(x, reference, size) { + return (x > reference) && (x < (reference + size)); + } + + $.widget("ui.droppable", { + version: "1.10.4", + widgetEventPrefix: "drop", + options: { + accept: "*", + activeClass: false, + addClasses: true, + greedy: false, + hoverClass: false, + scope: "default", + tolerance: "intersect", + + // callbacks + activate: null, + deactivate: null, + drop: null, + out: null, + over: null + }, + _create: function () { + + var proportions, + o = this.options, + accept = o.accept; + + this.isover = false; + this.isout = true; + + this.accept = $.isFunction(accept) ? accept : function (d) { + return d.is(accept); + }; + + this.proportions = function ( /* valueToWrite */) { + if (arguments.length) { + // Store the droppable's proportions + proportions = arguments[0]; + } else { + // Retrieve or derive the droppable's proportions + return proportions ? + proportions : + proportions = { + width: this.element[0].offsetWidth, + height: this.element[0].offsetHeight + }; + } + }; + + // Add the reference and positions to the manager + $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; + $.ui.ddmanager.droppables[o.scope].push(this); + + (o.addClasses && this.element.addClass("ui-droppable")); + + }, + + _destroy: function () { + var i = 0, + drop = $.ui.ddmanager.droppables[this.options.scope]; + + for (; i < drop.length; i++) { + if (drop[i] === this) { + drop.splice(i, 1); + } + } + + this.element.removeClass("ui-droppable ui-droppable-disabled"); + }, + + _setOption: function (key, value) { + + if (key === "accept") { + this.accept = $.isFunction(value) ? value : function (d) { + return d.is(value); + }; + } + $.Widget.prototype._setOption.apply(this, arguments); + }, + + _activate: function (event) { + var draggable = $.ui.ddmanager.current; + if (this.options.activeClass) { + this.element.addClass(this.options.activeClass); + } + if (draggable) { + this._trigger("activate", event, this.ui(draggable)); + } + }, + + _deactivate: function (event) { + var draggable = $.ui.ddmanager.current; + if (this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if (draggable) { + this._trigger("deactivate", event, this.ui(draggable)); + } + }, + + _over: function (event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0], (draggable.currentItem || draggable.element))) { + if (this.options.hoverClass) { + this.element.addClass(this.options.hoverClass); + } + this._trigger("over", event, this.ui(draggable)); + } + + }, + + _out: function (event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0], (draggable.currentItem || draggable.element))) { + if (this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("out", event, this.ui(draggable)); + } + + }, + + _drop: function (event, custom) { + + var draggable = custom || $.ui.ddmanager.current, + childrenIntersection = false; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return false; + } + + this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function () { + var inst = $.data(this, "ui-droppable"); + if ( + inst.options.greedy && + !inst.options.disabled && + inst.options.scope === draggable.options.scope && + inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && + $.ui.intersect(draggable, $.extend(inst, {offset: inst.element.offset()}), inst.options.tolerance) + ) { + childrenIntersection = true; + return false; + } + }); + if (childrenIntersection) { + return false; + } + + if (this.accept.call(this.element[0], (draggable.currentItem || draggable.element))) { + if (this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if (this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("drop", event, this.ui(draggable)); + return this.element; + } + + return false; + + }, + + ui: function (c) { + return { + draggable: (c.currentItem || c.element), + helper: c.helper, + position: c.position, + offset: c.positionAbs + }; + } + + }); + + $.ui.intersect = function (draggable, droppable, toleranceMode) { + + if (!droppable.offset) { + return false; + } + + var draggableLeft, draggableTop, + x1 = (draggable.positionAbs || draggable.position.absolute).left, + y1 = (draggable.positionAbs || draggable.position.absolute).top, + x2 = x1 + draggable.helperProportions.width, + y2 = y1 + draggable.helperProportions.height, + l = droppable.offset.left, + t = droppable.offset.top, + r = l + droppable.proportions().width, + b = t + droppable.proportions().height; + + switch (toleranceMode) { + case "fit": + return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); + case "intersect": + return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half + x2 - (draggable.helperProportions.width / 2) < r && // Left Half + t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half + y2 - (draggable.helperProportions.height / 2) < b); // Top Half + case "pointer": + draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); + draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); + return isOverAxis(draggableTop, t, droppable.proportions().height) && isOverAxis(draggableLeft, l, droppable.proportions().width); + case "touch": + return ( + (y1 >= t && y1 <= b) || // Top edge touching + (y2 >= t && y2 <= b) || // Bottom edge touching + (y1 < t && y2 > b) // Surrounded vertically + ) && ( + (x1 >= l && x1 <= r) || // Left edge touching + (x2 >= l && x2 <= r) || // Right edge touching + (x1 < l && x2 > r) // Surrounded horizontally + ); + default: + return false; + } + + }; + + /* + This manager tracks offsets of draggables and droppables + */ + $.ui.ddmanager = { + current: null, + droppables: {"default": []}, + prepareOffsets: function (t, event) { + + var i, j, + m = $.ui.ddmanager.droppables[t.options.scope] || [], + type = event ? event.type : null, // workaround for #2317 + list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); + + droppablesLoop: for (i = 0; i < m.length; i++) { + + //No disabled and non-accepted + if (m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0], (t.currentItem || t.element)))) { + continue; + } + + // Filter out elements in the current dragged item + for (j = 0; j < list.length; j++) { + if (list[j] === m[i].element[0]) { + m[i].proportions().height = 0; + continue droppablesLoop; + } + } + + m[i].visible = m[i].element.css("display") !== "none"; + if (!m[i].visible) { + continue; + } + + //Activate the droppable if used directly from draggables + if (type === "mousedown") { + m[i]._activate.call(m[i], event); + } + + m[i].offset = m[i].element.offset(); + m[i].proportions({width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight}); + + } + + }, + drop: function (draggable, event) { + + var dropped = false; + // Create a copy of the droppables in case the list changes during the drop (#9116) + $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function () { + + if (!this.options) { + return; + } + if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { + dropped = this._drop.call(this, event) || dropped; + } + + if (!this.options.disabled && this.visible && this.accept.call(this.element[0], (draggable.currentItem || draggable.element))) { + this.isout = true; + this.isover = false; + this._deactivate.call(this, event); + } + + }); + return dropped; + + }, + dragStart: function (draggable, event) { + //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) + draggable.element.parentsUntil("body").bind("scroll.droppable", function () { + if (!draggable.options.refreshPositions) { + $.ui.ddmanager.prepareOffsets(draggable, event); + } + }); + }, + drag: function (draggable, event) { + + //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. + if (draggable.options.refreshPositions) { + $.ui.ddmanager.prepareOffsets(draggable, event); + } + + //Run through all droppables and check their positions based on specific tolerance options + $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function () { + + if (this.options.disabled || this.greedyChild || !this.visible) { + return; + } + + var parentInstance, scope, parent, + intersects = $.ui.intersect(draggable, this, this.options.tolerance), + c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); + if (!c) { + return; + } + + if (this.options.greedy) { + // find droppable parents with same scope + scope = this.options.scope; + parent = this.element.parents(":data(ui-droppable)").filter(function () { + return $.data(this, "ui-droppable").options.scope === scope; + }); + + if (parent.length) { + parentInstance = $.data(parent[0], "ui-droppable"); + parentInstance.greedyChild = (c === "isover"); + } + } + + // we just moved into a greedy child + if (parentInstance && c === "isover") { + parentInstance.isover = false; + parentInstance.isout = true; + parentInstance._out.call(parentInstance, event); + } + + this[c] = true; + this[c === "isout" ? "isover" : "isout"] = false; + this[c === "isover" ? "_over" : "_out"].call(this, event); + + // we just moved out of a greedy child + if (parentInstance && c === "isout") { + parentInstance.isout = false; + parentInstance.isover = true; + parentInstance._over.call(parentInstance, event); + } + }); + + }, + dragStop: function (draggable, event) { + draggable.element.parentsUntil("body").unbind("scroll.droppable"); + //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) + if (!draggable.options.refreshPositions) { + $.ui.ddmanager.prepareOffsets(draggable, event); + } + } + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-blind.js b/lib/web/jquery/ui-modules/effect-blind.js new file mode 100644 index 000000000000..afd1a5285cad --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-blind.js @@ -0,0 +1,83 @@ +/*! + * jQuery UI Effects Blind - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/blind-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + var rvertical = /up|down|vertical/, + rpositivemotion = /up|left|vertical|horizontal/; + + $.effects.effect.blind = function (o, done) { + // Create element + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "height", "width"], + mode = $.effects.setMode(el, o.mode || "hide"), + direction = o.direction || "up", + vertical = rvertical.test(direction), + ref = vertical ? "height" : "width", + ref2 = vertical ? "top" : "left", + motion = rpositivemotion.test(direction), + animation = {}, + show = mode === "show", + wrapper, distance, margin; + + // if already wrapped, the wrapper's properties are my property. #6245 + if (el.parent().is(".ui-effects-wrapper")) { + $.effects.save(el.parent(), props); + } else { + $.effects.save(el, props); + } + el.show(); + wrapper = $.effects.createWrapper(el).css({ + overflow: "hidden" + }); + + distance = wrapper[ref](); + margin = parseFloat(wrapper.css(ref2)) || 0; + + animation[ref] = show ? distance : 0; + if (!motion) { + el + .css(vertical ? "bottom" : "right", 0) + .css(vertical ? "top" : "left", "auto") + .css({position: "absolute"}); + + animation[ref2] = show ? margin : distance + margin; + } + + // start at 0 if we are showing + if (show) { + wrapper.css(ref, 0); + if (!motion) { + wrapper.css(ref2, margin + distance); + } + } + + // Animate + wrapper.animate(animation, { + duration: o.duration, + easing: o.easing, + queue: false, + complete: function () { + if (mode === "hide") { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + } + }); + + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-bounce.js b/lib/web/jquery/ui-modules/effect-bounce.js new file mode 100644 index 000000000000..1902dc1b88f5 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-bounce.js @@ -0,0 +1,116 @@ +/*! + * jQuery UI Effects Bounce - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/bounce-effect/ + * + * Depends: + * jquery.ui.effect.js + */ +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.bounce = function (o, done) { + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "height", "width"], + + // defaults: + mode = $.effects.setMode(el, o.mode || "effect"), + hide = mode === "hide", + show = mode === "show", + direction = o.direction || "up", + distance = o.distance, + times = o.times || 5, + + // number of internal animations + anims = times * 2 + (show || hide ? 1 : 0), + speed = o.duration / anims, + easing = o.easing, + + // utility: + ref = (direction === "up" || direction === "down") ? "top" : "left", + motion = (direction === "up" || direction === "left"), + i, + upAnim, + downAnim, + + // we will need to re-assemble the queue to stack our animations in place + queue = el.queue(), + queuelen = queue.length; + + // Avoid touching opacity to prevent clearType and PNG issues in IE + if (show || hide) { + props.push("opacity"); + } + + $.effects.save(el, props); + el.show(); + $.effects.createWrapper(el); // Create Wrapper + + // default distance for the BIGGEST bounce is the outer Distance / 3 + if (!distance) { + distance = el[ref === "top" ? "outerHeight" : "outerWidth"]() / 3; + } + + if (show) { + downAnim = {opacity: 1}; + downAnim[ref] = 0; + + // if we are showing, force opacity 0 and set the initial position + // then do the "first" animation + el.css("opacity", 0) + .css(ref, motion ? -distance * 2 : distance * 2) + .animate(downAnim, speed, easing); + } + + // start at the smallest distance if we are hiding + if (hide) { + distance = distance / Math.pow(2, times - 1); + } + + downAnim = {}; + downAnim[ref] = 0; + // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here + for (i = 0; i < times; i++) { + upAnim = {}; + upAnim[ref] = (motion ? "-=" : "+=") + distance; + + el.animate(upAnim, speed, easing) + .animate(downAnim, speed, easing); + + distance = hide ? distance * 2 : distance / 2; + } + + // Last Bounce when Hiding + if (hide) { + upAnim = {opacity: 0}; + upAnim[ref] = (motion ? "-=" : "+=") + distance; + + el.animate(upAnim, speed, easing); + } + + el.queue(function () { + if (hide) { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + }); + + // inject all the animations we just queued to be first in line (after "inprogress") + if (queuelen > 1) { + queue.splice.apply(queue, + [1, 0].concat(queue.splice(queuelen, anims + 1))); + } + el.dequeue(); + + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-clip.js b/lib/web/jquery/ui-modules/effect-clip.js new file mode 100644 index 000000000000..a3651b303f05 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-clip.js @@ -0,0 +1,67 @@ +/*! + * jQuery UI Effects Clip - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/clip-effect/ + */ +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.clip = function (o, done) { + // Create element + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "height", "width"], + mode = $.effects.setMode(el, o.mode || "hide"), + show = mode === "show", + direction = o.direction || "vertical", + vert = direction === "vertical", + size = vert ? "height" : "width", + position = vert ? "top" : "left", + animation = {}, + wrapper, animate, distance; + + // Save & Show + $.effects.save(el, props); + el.show(); + + // Create Wrapper + wrapper = $.effects.createWrapper(el).css({ + overflow: "hidden" + }); + animate = (el[0].tagName === "IMG") ? wrapper : el; + distance = animate[size](); + + // Shift + if (show) { + animate.css(size, 0); + animate.css(position, distance / 2); + } + + // Create Animation Object: + animation[size] = show ? distance : 0; + animation[position] = show ? 0 : distance / 2; + + // Animate + animate.animate(animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function () { + if (!show) { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + } + }); + + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-drop.js b/lib/web/jquery/ui-modules/effect-drop.js new file mode 100644 index 000000000000..4b5025a2aa59 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-drop.js @@ -0,0 +1,66 @@ +/*! + * jQuery UI Effects Drop - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/drop-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.drop = function (o, done) { + + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "opacity", "height", "width"], + mode = $.effects.setMode(el, o.mode || "hide"), + show = mode === "show", + direction = o.direction || "left", + ref = (direction === "up" || direction === "down") ? "top" : "left", + motion = (direction === "up" || direction === "left") ? "pos" : "neg", + animation = { + opacity: show ? 1 : 0 + }, + distance; + + // Adjust + $.effects.save(el, props); + el.show(); + $.effects.createWrapper(el); + + distance = o.distance || el[ref === "top" ? "outerHeight" : "outerWidth"](true) / 2; + + if (show) { + el + .css("opacity", 0) + .css(ref, motion === "pos" ? -distance : distance); + } + + // Animation + animation[ref] = (show ? + (motion === "pos" ? "+=" : "-=") : + (motion === "pos" ? "-=" : "+=")) + + distance; + + // Animate + el.animate(animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function () { + if (mode === "hide") { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + } + }); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-explode.js b/lib/web/jquery/ui-modules/effect-explode.js new file mode 100644 index 000000000000..3ec72fbf5a8c --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-explode.js @@ -0,0 +1,98 @@ +/*! + * jQuery UI Effects Explode - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/explode-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.explode = function (o, done) { + + var rows = o.pieces ? Math.round(Math.sqrt(o.pieces)) : 3, + cells = rows, + el = $(this), + mode = $.effects.setMode(el, o.mode || "hide"), + show = mode === "show", + + // show and then visibility:hidden the element before calculating offset + offset = el.show().css("visibility", "hidden").offset(), + + // width and height of a piece + width = Math.ceil(el.outerWidth() / cells), + height = Math.ceil(el.outerHeight() / rows), + pieces = [], + + // loop + i, j, left, top, mx, my; + + // children animate complete: + function childComplete() { + pieces.push(this); + if (pieces.length === rows * cells) { + animComplete(); + } + } + + // clone the element for each row and cell. + for (i = 0; i < rows; i++) { // ===> + top = offset.top + i * height; + my = i - (rows - 1) / 2; + + for (j = 0; j < cells; j++) { // ||| + left = offset.left + j * width; + mx = j - (cells - 1) / 2; + + // Create a clone of the now hidden main element that will be absolute positioned + // within a wrapper div off the -left and -top equal to size of our pieces + el + .clone() + .appendTo("body") + .wrap("<div></div>") + .css({ + position: "absolute", + visibility: "visible", + left: -j * width, + top: -i * height + }) + + // select the wrapper - make it overflow: hidden and absolute positioned based on + // where the original was located +left and +top equal to the size of pieces + .parent() + .addClass("ui-effects-explode") + .css({ + position: "absolute", + overflow: "hidden", + width: width, + height: height, + left: left + (show ? mx * width : 0), + top: top + (show ? my * height : 0), + opacity: show ? 0 : 1 + }).animate({ + left: left + (show ? 0 : mx * width), + top: top + (show ? 0 : my * height), + opacity: show ? 1 : 0 + }, o.duration || 500, o.easing, childComplete); + } + } + + function animComplete() { + el.css({ + visibility: "visible" + }); + $(pieces).remove(); + if (!show) { + el.hide(); + } + done(); + } + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-fade.js b/lib/web/jquery/ui-modules/effect-fade.js new file mode 100644 index 000000000000..ba289cbeb018 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-fade.js @@ -0,0 +1,34 @@ +/*! + * jQuery UI Effects Fade - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/fade-effect/ + * + * Depends: + * jquery.ui.effect.js + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.fade = function (o, done) { + var el = $(this), + mode = $.effects.setMode(el, o.mode || "toggle"); + + el.animate({ + opacity: mode + }, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: done + }); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-fold.js b/lib/web/jquery/ui-modules/effect-fold.js new file mode 100644 index 000000000000..a9816cc53d7d --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-fold.js @@ -0,0 +1,76 @@ +/*! + * jQuery UI Effects Fold - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/fold-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.fold = function (o, done) { + + // Create element + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "height", "width"], + mode = $.effects.setMode(el, o.mode || "hide"), + show = mode === "show", + hide = mode === "hide", + size = o.size || 15, + percent = /([0-9]+)%/.exec(size), + horizFirst = !!o.horizFirst, + widthFirst = show !== horizFirst, + ref = widthFirst ? ["width", "height"] : ["height", "width"], + duration = o.duration / 2, + wrapper, distance, + animation1 = {}, + animation2 = {}; + + $.effects.save(el, props); + el.show(); + + // Create Wrapper + wrapper = $.effects.createWrapper(el).css({ + overflow: "hidden" + }); + distance = widthFirst ? + [wrapper.width(), wrapper.height()] : + [wrapper.height(), wrapper.width()]; + + if (percent) { + size = parseInt(percent[1], 10) / 100 * distance[hide ? 0 : 1]; + } + if (show) { + wrapper.css(horizFirst ? { + height: 0, + width: size + } : { + height: size, + width: 0 + }); + } + + // Animation + animation1[ref[0]] = show ? distance[0] : size; + animation2[ref[1]] = show ? distance[1] : 0; + + // Animate + wrapper + .animate(animation1, duration, o.easing) + .animate(animation2, duration, o.easing, function () { + if (hide) { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + }); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-highlight.js b/lib/web/jquery/ui-modules/effect-highlight.js new file mode 100644 index 000000000000..caa2596e95d4 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-highlight.js @@ -0,0 +1,51 @@ +/*! + * jQuery UI Effects Highlight - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/highlight-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.highlight = function (o, done) { + var elem = $(this), + props = ["backgroundImage", "backgroundColor", "opacity"], + mode = $.effects.setMode(elem, o.mode || "show"), + animation = { + backgroundColor: elem.css("backgroundColor") + }; + + if (mode === "hide") { + animation.opacity = 0; + } + + $.effects.save(elem, props); + + elem + .show() + .css({ + backgroundImage: "none", + backgroundColor: o.color || "#ffff99" + }) + .animate(animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function () { + if (mode === "hide") { + elem.hide(); + } + $.effects.restore(elem, props); + done(); + } + }); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-pulsate.js b/lib/web/jquery/ui-modules/effect-pulsate.js new file mode 100644 index 000000000000..cd9f202a153c --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-pulsate.js @@ -0,0 +1,64 @@ +/*! + * jQuery UI Effects Pulsate - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/pulsate-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.pulsate = function (o, done) { + var elem = $(this), + mode = $.effects.setMode(elem, o.mode || "show"), + show = mode === "show", + hide = mode === "hide", + showhide = (show || mode === "hide"), + + // showing or hiding leaves of the "last" animation + anims = ((o.times || 5) * 2) + (showhide ? 1 : 0), + duration = o.duration / anims, + animateTo = 0, + queue = elem.queue(), + queuelen = queue.length, + i; + + if (show || !elem.is(":visible")) { + elem.css("opacity", 0).show(); + animateTo = 1; + } + + // anims - 1 opacity "toggles" + for (i = 1; i < anims; i++) { + elem.animate({ + opacity: animateTo + }, duration, o.easing); + animateTo = 1 - animateTo; + } + + elem.animate({ + opacity: animateTo + }, duration, o.easing); + + elem.queue(function () { + if (hide) { + elem.hide(); + } + done(); + }); + + // We just queued up "anims" animations, we need to put them next in the queue + if (queuelen > 1) { + queue.splice.apply(queue, + [1, 0].concat(queue.splice(queuelen, anims + 1))); + } + elem.dequeue(); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-scale.js b/lib/web/jquery/ui-modules/effect-scale.js new file mode 100644 index 000000000000..18968e7183d6 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-scale.js @@ -0,0 +1,319 @@ +/*! + * jQuery UI Effects Scale - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/scale-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.puff = function (o, done) { + var elem = $(this), + mode = $.effects.setMode(elem, o.mode || "hide"), + hide = mode === "hide", + percent = parseInt(o.percent, 10) || 150, + factor = percent / 100, + original = { + height: elem.height(), + width: elem.width(), + outerHeight: elem.outerHeight(), + outerWidth: elem.outerWidth() + }; + + $.extend(o, { + effect: "scale", + queue: false, + fade: true, + mode: mode, + complete: done, + percent: hide ? percent : 100, + from: hide ? + original : + { + height: original.height * factor, + width: original.width * factor, + outerHeight: original.outerHeight * factor, + outerWidth: original.outerWidth * factor + } + }); + + elem.effect(o); + }; + + $.effects.effect.scale = function (o, done) { + + // Create element + var el = $(this), + options = $.extend(true, {}, o), + mode = $.effects.setMode(el, o.mode || "effect"), + percent = parseInt(o.percent, 10) || + (parseInt(o.percent, 10) === 0 ? 0 : (mode === "hide" ? 0 : 100)), + direction = o.direction || "both", + origin = o.origin, + original = { + height: el.height(), + width: el.width(), + outerHeight: el.outerHeight(), + outerWidth: el.outerWidth() + }, + factor = { + y: direction !== "horizontal" ? (percent / 100) : 1, + x: direction !== "vertical" ? (percent / 100) : 1 + }; + + // We are going to pass this effect to the size effect: + options.effect = "size"; + options.queue = false; + options.complete = done; + + // Set default origin and restore for show/hide + if (mode !== "effect") { + options.origin = origin || ["middle", "center"]; + options.restore = true; + } + + options.from = o.from || (mode === "show" ? { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + } : original); + options.to = { + height: original.height * factor.y, + width: original.width * factor.x, + outerHeight: original.outerHeight * factor.y, + outerWidth: original.outerWidth * factor.x + }; + + // Fade option to support puff + if (options.fade) { + if (mode === "show") { + options.from.opacity = 0; + options.to.opacity = 1; + } + if (mode === "hide") { + options.from.opacity = 1; + options.to.opacity = 0; + } + } + + // Animate + el.effect(options); + + }; + + $.effects.effect.size = function (o, done) { + + // Create element + var original, baseline, factor, + el = $(this), + props0 = ["position", "top", "bottom", "left", "right", "width", "height", "overflow", "opacity"], + + // Always restore + props1 = ["position", "top", "bottom", "left", "right", "overflow", "opacity"], + + // Copy for children + props2 = ["width", "height", "overflow"], + cProps = ["fontSize"], + vProps = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"], + hProps = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"], + + // Set options + mode = $.effects.setMode(el, o.mode || "effect"), + restore = o.restore || mode !== "effect", + scale = o.scale || "both", + origin = o.origin || ["middle", "center"], + position = el.css("position"), + props = restore ? props0 : props1, + zero = { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + }; + + if (mode === "show") { + el.show(); + } + original = { + height: el.height(), + width: el.width(), + outerHeight: el.outerHeight(), + outerWidth: el.outerWidth() + }; + + if (o.mode === "toggle" && mode === "show") { + el.from = o.to || zero; + el.to = o.from || original; + } else { + el.from = o.from || (mode === "show" ? zero : original); + el.to = o.to || (mode === "hide" ? zero : original); + } + + // Set scaling factor + factor = { + from: { + y: el.from.height / original.height, + x: el.from.width / original.width + }, + to: { + y: el.to.height / original.height, + x: el.to.width / original.width + } + }; + + // Scale the css box + if (scale === "box" || scale === "both") { + + // Vertical props scaling + if (factor.from.y !== factor.to.y) { + props = props.concat(vProps); + el.from = $.effects.setTransition(el, vProps, factor.from.y, el.from); + el.to = $.effects.setTransition(el, vProps, factor.to.y, el.to); + } + + // Horizontal props scaling + if (factor.from.x !== factor.to.x) { + props = props.concat(hProps); + el.from = $.effects.setTransition(el, hProps, factor.from.x, el.from); + el.to = $.effects.setTransition(el, hProps, factor.to.x, el.to); + } + } + + // Scale the content + if (scale === "content" || scale === "both") { + + // Vertical props scaling + if (factor.from.y !== factor.to.y) { + props = props.concat(cProps).concat(props2); + el.from = $.effects.setTransition(el, cProps, factor.from.y, el.from); + el.to = $.effects.setTransition(el, cProps, factor.to.y, el.to); + } + } + + $.effects.save(el, props); + el.show(); + $.effects.createWrapper(el); + el.css("overflow", "hidden").css(el.from); + + // Adjust + if (origin) { // Calculate baseline shifts + baseline = $.effects.getBaseline(origin, original); + el.from.top = (original.outerHeight - el.outerHeight()) * baseline.y; + el.from.left = (original.outerWidth - el.outerWidth()) * baseline.x; + el.to.top = (original.outerHeight - el.to.outerHeight) * baseline.y; + el.to.left = (original.outerWidth - el.to.outerWidth) * baseline.x; + } + el.css(el.from); // set top & left + + // Animate + if (scale === "content" || scale === "both") { // Scale the children + + // Add margins/font-size + vProps = vProps.concat(["marginTop", "marginBottom"]).concat(cProps); + hProps = hProps.concat(["marginLeft", "marginRight"]); + props2 = props0.concat(vProps).concat(hProps); + + el.find("*[width]").each(function () { + var child = $(this), + c_original = { + height: child.height(), + width: child.width(), + outerHeight: child.outerHeight(), + outerWidth: child.outerWidth() + }; + if (restore) { + $.effects.save(child, props2); + } + + child.from = { + height: c_original.height * factor.from.y, + width: c_original.width * factor.from.x, + outerHeight: c_original.outerHeight * factor.from.y, + outerWidth: c_original.outerWidth * factor.from.x + }; + child.to = { + height: c_original.height * factor.to.y, + width: c_original.width * factor.to.x, + outerHeight: c_original.height * factor.to.y, + outerWidth: c_original.width * factor.to.x + }; + + // Vertical props scaling + if (factor.from.y !== factor.to.y) { + child.from = $.effects.setTransition(child, vProps, factor.from.y, child.from); + child.to = $.effects.setTransition(child, vProps, factor.to.y, child.to); + } + + // Horizontal props scaling + if (factor.from.x !== factor.to.x) { + child.from = $.effects.setTransition(child, hProps, factor.from.x, child.from); + child.to = $.effects.setTransition(child, hProps, factor.to.x, child.to); + } + + // Animate children + child.css(child.from); + child.animate(child.to, o.duration, o.easing, function () { + + // Restore children + if (restore) { + $.effects.restore(child, props2); + } + }); + }); + } + + // Animate + el.animate(el.to, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function () { + if (el.to.opacity === 0) { + el.css("opacity", el.from.opacity); + } + if (mode === "hide") { + el.hide(); + } + $.effects.restore(el, props); + if (!restore) { + + // we need to calculate our new positioning based on the scaling + if (position === "static") { + el.css({ + position: "relative", + top: el.to.top, + left: el.to.left + }); + } else { + $.each(["top", "left"], function (idx, pos) { + el.css(pos, function (_, str) { + var val = parseInt(str, 10), + toRef = idx ? el.to.left : el.to.top; + + // if original was "auto", recalculate the new value from wrapper + if (str === "auto") { + return toRef + "px"; + } + + return val + toRef + "px"; + }); + }); + } + } + + $.effects.removeWrapper(el); + done(); + } + }); + + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-shake.js b/lib/web/jquery/ui-modules/effect-shake.js new file mode 100644 index 000000000000..21dce9ccc70a --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-shake.js @@ -0,0 +1,75 @@ +/*! + * jQuery UI Effects Shake - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/shake-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.shake = function (o, done) { + + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "height", "width"], + mode = $.effects.setMode(el, o.mode || "effect"), + direction = o.direction || "left", + distance = o.distance || 20, + times = o.times || 3, + anims = times * 2 + 1, + speed = Math.round(o.duration / anims), + ref = (direction === "up" || direction === "down") ? "top" : "left", + positiveMotion = (direction === "up" || direction === "left"), + animation = {}, + animation1 = {}, + animation2 = {}, + i, + + // we will need to re-assemble the queue to stack our animations in place + queue = el.queue(), + queuelen = queue.length; + + $.effects.save(el, props); + el.show(); + $.effects.createWrapper(el); + + // Animation + animation[ref] = (positiveMotion ? "-=" : "+=") + distance; + animation1[ref] = (positiveMotion ? "+=" : "-=") + distance * 2; + animation2[ref] = (positiveMotion ? "-=" : "+=") + distance * 2; + + // Animate + el.animate(animation, speed, o.easing); + + // Shakes + for (i = 1; i < times; i++) { + el.animate(animation1, speed, o.easing).animate(animation2, speed, o.easing); + } + el + .animate(animation1, speed, o.easing) + .animate(animation, speed / 2, o.easing) + .queue(function () { + if (mode === "hide") { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + }); + + // inject all the animations we just queued to be first in line (after "inprogress") + if (queuelen > 1) { + queue.splice.apply(queue, + [1, 0].concat(queue.splice(queuelen, anims + 1))); + } + el.dequeue(); + + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-slide.js b/lib/web/jquery/ui-modules/effect-slide.js new file mode 100644 index 000000000000..eb81d8716918 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-slide.js @@ -0,0 +1,65 @@ +/*! + * jQuery UI Effects Slide - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/slide-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.slide = function (o, done) { + + // Create element + var el = $(this), + props = ["position", "top", "bottom", "left", "right", "width", "height"], + mode = $.effects.setMode(el, o.mode || "show"), + show = mode === "show", + direction = o.direction || "left", + ref = (direction === "up" || direction === "down") ? "top" : "left", + positiveMotion = (direction === "up" || direction === "left"), + distance, + animation = {}; + + // Adjust + $.effects.save(el, props); + el.show(); + distance = o.distance || el[ref === "top" ? "outerHeight" : "outerWidth"](true); + + $.effects.createWrapper(el).css({ + overflow: "hidden" + }); + + if (show) { + el.css(ref, positiveMotion ? (isNaN(distance) ? "-" + distance : -distance) : distance); + } + + // Animation + animation[ref] = (show ? + (positiveMotion ? "+=" : "-=") : + (positiveMotion ? "-=" : "+=")) + + distance; + + // Animate + el.animate(animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function () { + if (mode === "hide") { + el.hide(); + } + $.effects.restore(el, props); + $.effects.removeWrapper(el); + done(); + } + }); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect-transfer.js b/lib/web/jquery/ui-modules/effect-transfer.js new file mode 100644 index 000000000000..a3d0af86beb8 --- /dev/null +++ b/lib/web/jquery/ui-modules/effect-transfer.js @@ -0,0 +1,48 @@ +/*! + * jQuery UI Effects Transfer - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/transfer-effect/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/effect' +], function ($, undefined) { + + $.effects.effect.transfer = function (o, done) { + var elem = $(this), + target = $(o.to), + targetFixed = target.css("position") === "fixed", + body = $("body"), + fixTop = targetFixed ? body.scrollTop() : 0, + fixLeft = targetFixed ? body.scrollLeft() : 0, + endPosition = target.offset(), + animation = { + top: endPosition.top - fixTop, + left: endPosition.left - fixLeft, + height: target.innerHeight(), + width: target.innerWidth() + }, + startPosition = elem.offset(), + transfer = $("<div class='ui-effects-transfer'></div>") + .appendTo(document.body) + .addClass(o.className) + .css({ + top: startPosition.top - fixTop, + left: startPosition.left - fixLeft, + height: elem.innerHeight(), + width: elem.innerWidth(), + position: targetFixed ? "fixed" : "absolute" + }) + .animate(animation, o.duration, o.easing, function () { + transfer.remove(); + done(); + }); + }; + +}); diff --git a/lib/web/jquery/ui-modules/effect.js b/lib/web/jquery/ui-modules/effect.js new file mode 100644 index 000000000000..5a1e8adf7cae --- /dev/null +++ b/lib/web/jquery/ui-modules/effect.js @@ -0,0 +1,1307 @@ +/*! + * jQuery UI Effects - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/effects-core/ + */ + +define([ + 'jquery' +], function ($, undefined) { + + var dataSpace = "ui-effects-"; + + $.effects = { + effect: {} + }; + + /*! + * jQuery Color Animations v2.1.2 + * https://github.com/jquery/jquery-color + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * Date: Wed Jan 16 08:47:09 2013 -0600 + */ + (function (jQuery, undefined) { + + var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", + + // plusequals test for += 100 -= 100 + rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, + // a set of RE's that can match strings and generate color tuples. + stringParsers = [{ + re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function (execResult) { + return [ + execResult[1], + execResult[2], + execResult[3], + execResult[4] + ]; + } + }, { + re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function (execResult) { + return [ + execResult[1] * 2.55, + execResult[2] * 2.55, + execResult[3] * 2.55, + execResult[4] + ]; + } + }, { + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/, + parse: function (execResult) { + return [ + parseInt(execResult[1], 16), + parseInt(execResult[2], 16), + parseInt(execResult[3], 16) + ]; + } + }, { + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9])([a-f0-9])([a-f0-9])/, + parse: function (execResult) { + return [ + parseInt(execResult[1] + execResult[1], 16), + parseInt(execResult[2] + execResult[2], 16), + parseInt(execResult[3] + execResult[3], 16) + ]; + } + }, { + re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + space: "hsla", + parse: function (execResult) { + return [ + execResult[1], + execResult[2] / 100, + execResult[3] / 100, + execResult[4] + ]; + } + }], + + // jQuery.Color( ) + color = jQuery.Color = function (color, green, blue, alpha) { + return new jQuery.Color.fn.parse(color, green, blue, alpha); + }, + spaces = { + rgba: { + props: { + red: { + idx: 0, + type: "byte" + }, + green: { + idx: 1, + type: "byte" + }, + blue: { + idx: 2, + type: "byte" + } + } + }, + + hsla: { + props: { + hue: { + idx: 0, + type: "degrees" + }, + saturation: { + idx: 1, + type: "percent" + }, + lightness: { + idx: 2, + type: "percent" + } + } + } + }, + propTypes = { + "byte": { + floor: true, + max: 255 + }, + "percent": { + max: 1 + }, + "degrees": { + mod: 360, + floor: true + } + }, + support = color.support = {}, + + // element for support tests + supportElem = jQuery("<p>")[0], + + // colors = jQuery.Color.names + colors, + + // local aliases of functions called often + each = jQuery.each; + +// determine rgba support immediately + supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; + support.rgba = supportElem.style.backgroundColor.indexOf("rgba") > -1; + +// define cache name and alpha properties +// for rgba and hsla spaces + each(spaces, function (spaceName, space) { + space.cache = "_" + spaceName; + space.props.alpha = { + idx: 3, + type: "percent", + def: 1 + }; + }); + + function clamp(value, prop, allowEmpty) { + var type = propTypes[prop.type] || {}; + + if (value == null) { + return (allowEmpty || !prop.def) ? null : prop.def; + } + + // ~~ is an short way of doing floor for positive numbers + value = type.floor ? ~~value : parseFloat(value); + + // IE will pass in empty strings as value for alpha, + // which will hit this case + if (isNaN(value)) { + return prop.def; + } + + if (type.mod) { + // we add mod before modding to make sure that negatives values + // get converted properly: -10 -> 350 + return (value + type.mod) % type.mod; + } + + // for now all property types without mod have min and max + return 0 > value ? 0 : type.max < value ? type.max : value; + } + + function stringParse(string) { + var inst = color(), + rgba = inst._rgba = []; + + string = string.toLowerCase(); + + each(stringParsers, function (i, parser) { + var parsed, + match = parser.re.exec(string), + values = match && parser.parse(match), + spaceName = parser.space || "rgba"; + + if (values) { + parsed = inst[spaceName](values); + + // if this was an rgba parse the assignment might happen twice + // oh well.... + inst[spaces[spaceName].cache] = parsed[spaces[spaceName].cache]; + rgba = inst._rgba = parsed._rgba; + + // exit each( stringParsers ) here because we matched + return false; + } + }); + + // Found a stringParser that handled it + if (rgba.length) { + + // if this came from a parsed string, force "transparent" when alpha is 0 + // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) + if (rgba.join() === "0,0,0,0") { + jQuery.extend(rgba, colors.transparent); + } + return inst; + } + + // named colors + return colors[string]; + } + + color.fn = jQuery.extend(color.prototype, { + parse: function (red, green, blue, alpha) { + if (red === undefined) { + this._rgba = [null, null, null, null]; + return this; + } + if (red.jquery || red.nodeType) { + red = jQuery(red).css(green); + green = undefined; + } + + var inst = this, + type = jQuery.type(red), + rgba = this._rgba = []; + + // more than 1 argument specified - assume ( red, green, blue, alpha ) + if (green !== undefined) { + red = [red, green, blue, alpha]; + type = "array"; + } + + if (type === "string") { + return this.parse(stringParse(red) || colors._default); + } + + if (type === "array") { + each(spaces.rgba.props, function (key, prop) { + rgba[prop.idx] = clamp(red[prop.idx], prop); + }); + return this; + } + + if (type === "object") { + if (red instanceof color) { + each(spaces, function (spaceName, space) { + if (red[space.cache]) { + inst[space.cache] = red[space.cache].slice(); + } + }); + } else { + each(spaces, function (spaceName, space) { + var cache = space.cache; + each(space.props, function (key, prop) { + + // if the cache doesn't exist, and we know how to convert + if (!inst[cache] && space.to) { + + // if the value was null, we don't need to copy it + // if the key was alpha, we don't need to copy it either + if (key === "alpha" || red[key] == null) { + return; + } + inst[cache] = space.to(inst._rgba); + } + + // this is the only case where we allow nulls for ALL properties. + // call clamp with alwaysAllowEmpty + inst[cache][prop.idx] = clamp(red[key], prop, true); + }); + + // everything defined but alpha? + if (inst[cache] && jQuery.inArray(null, inst[cache].slice(0, 3)) < 0) { + // use the default of 1 + inst[cache][3] = 1; + if (space.from) { + inst._rgba = space.from(inst[cache]); + } + } + }); + } + return this; + } + }, + is: function (compare) { + var is = color(compare), + same = true, + inst = this; + + each(spaces, function (_, space) { + var localCache, + isCache = is[space.cache]; + if (isCache) { + localCache = inst[space.cache] || space.to && space.to(inst._rgba) || []; + each(space.props, function (_, prop) { + if (isCache[prop.idx] != null) { + same = (isCache[prop.idx] === localCache[prop.idx]); + return same; + } + }); + } + return same; + }); + return same; + }, + _space: function () { + var used = [], + inst = this; + each(spaces, function (spaceName, space) { + if (inst[space.cache]) { + used.push(spaceName); + } + }); + return used.pop(); + }, + transition: function (other, distance) { + var end = color(other), + spaceName = end._space(), + space = spaces[spaceName], + startColor = this.alpha() === 0 ? color("transparent") : this, + start = startColor[space.cache] || space.to(startColor._rgba), + result = start.slice(); + + end = end[space.cache]; + each(space.props, function (key, prop) { + var index = prop.idx, + startValue = start[index], + endValue = end[index], + type = propTypes[prop.type] || {}; + + // if null, don't override start value + if (endValue === null) { + return; + } + // if null - use end + if (startValue === null) { + result[index] = endValue; + } else { + if (type.mod) { + if (endValue - startValue > type.mod / 2) { + startValue += type.mod; + } else if (startValue - endValue > type.mod / 2) { + startValue -= type.mod; + } + } + result[index] = clamp((endValue - startValue) * distance + startValue, prop); + } + }); + return this[spaceName](result); + }, + blend: function (opaque) { + // if we are already opaque - return ourself + if (this._rgba[3] === 1) { + return this; + } + + var rgb = this._rgba.slice(), + a = rgb.pop(), + blend = color(opaque)._rgba; + + return color(jQuery.map(rgb, function (v, i) { + return (1 - a) * blend[i] + a * v; + })); + }, + toRgbaString: function () { + var prefix = "rgba(", + rgba = jQuery.map(this._rgba, function (v, i) { + return v == null ? (i > 2 ? 1 : 0) : v; + }); + + if (rgba[3] === 1) { + rgba.pop(); + prefix = "rgb("; + } + + return prefix + rgba.join() + ")"; + }, + toHslaString: function () { + var prefix = "hsla(", + hsla = jQuery.map(this.hsla(), function (v, i) { + if (v == null) { + v = i > 2 ? 1 : 0; + } + + // catch 1 and 2 + if (i && i < 3) { + v = Math.round(v * 100) + "%"; + } + return v; + }); + + if (hsla[3] === 1) { + hsla.pop(); + prefix = "hsl("; + } + return prefix + hsla.join() + ")"; + }, + toHexString: function (includeAlpha) { + var rgba = this._rgba.slice(), + alpha = rgba.pop(); + + if (includeAlpha) { + rgba.push(~~(alpha * 255)); + } + + return "#" + jQuery.map(rgba, function (v) { + + // default to 0 when nulls exist + v = (v || 0).toString(16); + return v.length === 1 ? "0" + v : v; + }).join(""); + }, + toString: function () { + return this._rgba[3] === 0 ? "transparent" : this.toRgbaString(); + } + }); + color.fn.parse.prototype = color.fn; + +// hsla conversions adapted from: +// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 + + function hue2rgb(p, q, h) { + h = (h + 1) % 1; + if (h * 6 < 1) { + return p + (q - p) * h * 6; + } + if (h * 2 < 1) { + return q; + } + if (h * 3 < 2) { + return p + (q - p) * ((2 / 3) - h) * 6; + } + return p; + } + + spaces.hsla.to = function (rgba) { + if (rgba[0] == null || rgba[1] == null || rgba[2] == null) { + return [null, null, null, rgba[3]]; + } + var r = rgba[0] / 255, + g = rgba[1] / 255, + b = rgba[2] / 255, + a = rgba[3], + max = Math.max(r, g, b), + min = Math.min(r, g, b), + diff = max - min, + add = max + min, + l = add * 0.5, + h, s; + + if (min === max) { + h = 0; + } else if (r === max) { + h = (60 * (g - b) / diff) + 360; + } else if (g === max) { + h = (60 * (b - r) / diff) + 120; + } else { + h = (60 * (r - g) / diff) + 240; + } + + // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% + // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) + if (diff === 0) { + s = 0; + } else if (l <= 0.5) { + s = diff / add; + } else { + s = diff / (2 - add); + } + return [Math.round(h) % 360, s, l, a == null ? 1 : a]; + }; + + spaces.hsla.from = function (hsla) { + if (hsla[0] == null || hsla[1] == null || hsla[2] == null) { + return [null, null, null, hsla[3]]; + } + var h = hsla[0] / 360, + s = hsla[1], + l = hsla[2], + a = hsla[3], + q = l <= 0.5 ? l * (1 + s) : l + s - l * s, + p = 2 * l - q; + + return [ + Math.round(hue2rgb(p, q, h + (1 / 3)) * 255), + Math.round(hue2rgb(p, q, h) * 255), + Math.round(hue2rgb(p, q, h - (1 / 3)) * 255), + a + ]; + }; + + + each(spaces, function (spaceName, space) { + var props = space.props, + cache = space.cache, + to = space.to, + from = space.from; + + // makes rgba() and hsla() + color.fn[spaceName] = function (value) { + + // generate a cache for this space if it doesn't exist + if (to && !this[cache]) { + this[cache] = to(this._rgba); + } + if (value === undefined) { + return this[cache].slice(); + } + + var ret, + type = jQuery.type(value), + arr = (type === "array" || type === "object") ? value : arguments, + local = this[cache].slice(); + + each(props, function (key, prop) { + var val = arr[type === "object" ? key : prop.idx]; + if (val == null) { + val = local[prop.idx]; + } + local[prop.idx] = clamp(val, prop); + }); + + if (from) { + ret = color(from(local)); + ret[cache] = local; + return ret; + } else { + return color(local); + } + }; + + // makes red() green() blue() alpha() hue() saturation() lightness() + each(props, function (key, prop) { + // alpha is included in more than one space + if (color.fn[key]) { + return; + } + color.fn[key] = function (value) { + var vtype = jQuery.type(value), + fn = (key === "alpha" ? (this._hsla ? "hsla" : "rgba") : spaceName), + local = this[fn](), + cur = local[prop.idx], + match; + + if (vtype === "undefined") { + return cur; + } + + if (vtype === "function") { + value = value.call(this, cur); + vtype = jQuery.type(value); + } + if (value == null && prop.empty) { + return this; + } + if (vtype === "string") { + match = rplusequals.exec(value); + if (match) { + value = cur + parseFloat(match[2]) * (match[1] === "+" ? 1 : -1); + } + } + local[prop.idx] = value; + return this[fn](local); + }; + }); + }); + +// add cssHook and .fx.step function for each named hook. +// accept a space separated string of properties + color.hook = function (hook) { + var hooks = hook.split(" "); + each(hooks, function (i, hook) { + jQuery.cssHooks[hook] = { + set: function (elem, value) { + var parsed, curElem, + backgroundColor = ""; + + if (value !== "transparent" && (jQuery.type(value) !== "string" || (parsed = stringParse(value)))) { + value = color(parsed || value); + if (!support.rgba && value._rgba[3] !== 1) { + curElem = hook === "backgroundColor" ? elem.parentNode : elem; + while ( + (backgroundColor === "" || backgroundColor === "transparent") && + curElem && curElem.style + ) { + try { + backgroundColor = jQuery.css(curElem, "backgroundColor"); + curElem = curElem.parentNode; + } catch (e) { + } + } + + value = value.blend(backgroundColor && backgroundColor !== "transparent" ? + backgroundColor : + "_default"); + } + + value = value.toRgbaString(); + } + try { + elem.style[hook] = value; + } catch (e) { + // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' + } + } + }; + jQuery.fx.step[hook] = function (fx) { + if (!fx.colorInit) { + fx.start = color(fx.elem, hook); + fx.end = color(fx.end); + fx.colorInit = true; + } + jQuery.cssHooks[hook].set(fx.elem, fx.start.transition(fx.end, fx.pos)); + }; + }); + + }; + + color.hook(stepHooks); + + jQuery.cssHooks.borderColor = { + expand: function (value) { + var expanded = {}; + + each(["Top", "Right", "Bottom", "Left"], function (i, part) { + expanded["border" + part + "Color"] = value; + }); + return expanded; + } + }; + +// Basic color names only. +// Usage of any of the other color names requires adding yourself or including +// jquery.color.svg-names.js. + colors = jQuery.Color.names = { + // 4.1. Basic color keywords + aqua: "#00ffff", + black: "#000000", + blue: "#0000ff", + fuchsia: "#ff00ff", + gray: "#808080", + green: "#008000", + lime: "#00ff00", + maroon: "#800000", + navy: "#000080", + olive: "#808000", + purple: "#800080", + red: "#ff0000", + silver: "#c0c0c0", + teal: "#008080", + white: "#ffffff", + yellow: "#ffff00", + + // 4.2.3. "transparent" color keyword + transparent: [null, null, null, 0], + + _default: "#ffffff" + }; + + })(jQuery); + + + /******************************************************************************/ + /****************************** CLASS ANIMATIONS ******************************/ + /******************************************************************************/ + (function () { + + var classAnimationActions = ["add", "remove", "toggle"], + shorthandStyles = { + border: 1, + borderBottom: 1, + borderColor: 1, + borderLeft: 1, + borderRight: 1, + borderTop: 1, + borderWidth: 1, + margin: 1, + padding: 1 + }; + + $.each(["borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle"], function (_, prop) { + $.fx.step[prop] = function (fx) { + if (fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr) { + jQuery.style(fx.elem, prop, fx.end); + fx.setAttr = true; + } + }; + }); + + function getElementStyles(elem) { + var key, len, + style = elem.ownerDocument.defaultView ? + elem.ownerDocument.defaultView.getComputedStyle(elem, null) : + elem.currentStyle, + styles = {}; + + if (style && style.length && style[0] && style[style[0]]) { + len = style.length; + while (len--) { + key = style[len]; + if (typeof style[key] === "string") { + styles[$.camelCase(key)] = style[key]; + } + } + // support: Opera, IE <9 + } else { + for (key in style) { + if (typeof style[key] === "string") { + styles[key] = style[key]; + } + } + } + + return styles; + } + + + function styleDifference(oldStyle, newStyle) { + var diff = {}, + name, value; + + for (name in newStyle) { + value = newStyle[name]; + if (oldStyle[name] !== value) { + if (!shorthandStyles[name]) { + if ($.fx.step[name] || !isNaN(parseFloat(value))) { + diff[name] = value; + } + } + } + } + + return diff; + } + +// support: jQuery <1.8 + if (!$.fn.addBack) { + $.fn.addBack = function (selector) { + return this.add(selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + }; + } + + $.effects.animateClass = function (value, duration, easing, callback) { + var o = $.speed(duration, easing, callback); + + return this.queue(function () { + var animated = $(this), + baseClass = animated.attr("class") || "", + applyClassChange, + allAnimations = o.children ? animated.find("*").addBack() : animated; + + // map the animated objects to store the original styles. + allAnimations = allAnimations.map(function () { + var el = $(this); + return { + el: el, + start: getElementStyles(this) + }; + }); + + // apply class change + applyClassChange = function () { + $.each(classAnimationActions, function (i, action) { + if (value[action]) { + animated[action + "Class"](value[action]); + } + }); + }; + applyClassChange(); + + // map all animated objects again - calculate new styles and diff + allAnimations = allAnimations.map(function () { + this.end = getElementStyles(this.el[0]); + this.diff = styleDifference(this.start, this.end); + return this; + }); + + // apply original class + animated.attr("class", baseClass); + + // map all animated objects again - this time collecting a promise + allAnimations = allAnimations.map(function () { + var styleInfo = this, + dfd = $.Deferred(), + opts = $.extend({}, o, { + queue: false, + complete: function () { + dfd.resolve(styleInfo); + } + }); + + this.el.animate(this.diff, opts); + return dfd.promise(); + }); + + // once all animations have completed: + $.when.apply($, allAnimations.get()).done(function () { + + // set the final class + applyClassChange(); + + // for each animated element, + // clear all css properties that were animated + $.each(arguments, function () { + var el = this.el; + $.each(this.diff, function (key) { + el.css(key, ""); + }); + }); + + // this is guarnteed to be there if you use jQuery.speed() + // it also handles dequeuing the next anim... + o.complete.call(animated[0]); + }); + }); + }; + + $.fn.extend({ + addClass: (function (orig) { + return function (classNames, speed, easing, callback) { + return speed ? + $.effects.animateClass.call(this, + {add: classNames}, speed, easing, callback) : + orig.apply(this, arguments); + }; + })($.fn.addClass), + + removeClass: (function (orig) { + return function (classNames, speed, easing, callback) { + return arguments.length > 1 ? + $.effects.animateClass.call(this, + {remove: classNames}, speed, easing, callback) : + orig.apply(this, arguments); + }; + })($.fn.removeClass), + + toggleClass: (function (orig) { + return function (classNames, force, speed, easing, callback) { + if (typeof force === "boolean" || force === undefined) { + if (!speed) { + // without speed parameter + return orig.apply(this, arguments); + } else { + return $.effects.animateClass.call(this, + (force ? {add: classNames} : {remove: classNames}), + speed, easing, callback); + } + } else { + // without force parameter + return $.effects.animateClass.call(this, + {toggle: classNames}, force, speed, easing); + } + }; + })($.fn.toggleClass), + + switchClass: function (remove, add, speed, easing, callback) { + return $.effects.animateClass.call(this, { + add: add, + remove: remove + }, speed, easing, callback); + } + }); + + })(); + + /******************************************************************************/ + /*********************************** EFFECTS **********************************/ + /******************************************************************************/ + + (function () { + + $.extend($.effects, { + version: "1.10.4", + + // Saves a set of properties in a data storage + save: function (element, set) { + for (var i = 0; i < set.length; i++) { + if (set[i] !== null) { + element.data(dataSpace + set[i], element[0].style[set[i]]); + } + } + }, + + // Restores a set of previously saved properties from a data storage + restore: function (element, set) { + var val, i; + for (i = 0; i < set.length; i++) { + if (set[i] !== null) { + val = element.data(dataSpace + set[i]); + // support: jQuery 1.6.2 + // http://bugs.jquery.com/ticket/9917 + // jQuery 1.6.2 incorrectly returns undefined for any falsy value. + // We can't differentiate between "" and 0 here, so we just assume + // empty string since it's likely to be a more common value... + if (val === undefined) { + val = ""; + } + element.css(set[i], val); + } + } + }, + + setMode: function (el, mode) { + if (mode === "toggle") { + mode = el.is(":hidden") ? "show" : "hide"; + } + return mode; + }, + + // Translates a [top,left] array into a baseline value + // this should be a little more flexible in the future to handle a string & hash + getBaseline: function (origin, original) { + var y, x; + switch (origin[0]) { + case "top": + y = 0; + break; + case "middle": + y = 0.5; + break; + case "bottom": + y = 1; + break; + default: + y = origin[0] / original.height; + } + switch (origin[1]) { + case "left": + x = 0; + break; + case "center": + x = 0.5; + break; + case "right": + x = 1; + break; + default: + x = origin[1] / original.width; + } + return { + x: x, + y: y + }; + }, + + // Wraps the element around a wrapper that copies position properties + createWrapper: function (element) { + + // if the element is already wrapped, return it + if (element.parent().is(".ui-effects-wrapper")) { + return element.parent(); + } + + // wrap the element + var props = { + width: element.outerWidth(true), + height: element.outerHeight(true), + "float": element.css("float") + }, + wrapper = $("<div></div>") + .addClass("ui-effects-wrapper") + .css({ + fontSize: "100%", + background: "transparent", + border: "none", + margin: 0, + padding: 0 + }), + // Store the size in case width/height are defined in % - Fixes #5245 + size = { + width: element.width(), + height: element.height() + }, + active = document.activeElement; + + // support: Firefox + // Firefox incorrectly exposes anonymous content + // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 + try { + active.id; + } catch (e) { + active = document.body; + } + + element.wrap(wrapper); + + // Fixes #7595 - Elements lose focus when wrapped. + if (element[0] === active || $.contains(element[0], active)) { + $(active).focus(); + } + + wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually lose the reference to the wrapped element + + // transfer positioning properties to the wrapper + if (element.css("position") === "static") { + wrapper.css({position: "relative"}); + element.css({position: "relative"}); + } else { + $.extend(props, { + position: element.css("position"), + zIndex: element.css("z-index") + }); + $.each(["top", "left", "bottom", "right"], function (i, pos) { + props[pos] = element.css(pos); + if (isNaN(parseInt(props[pos], 10))) { + props[pos] = "auto"; + } + }); + element.css({ + position: "relative", + top: 0, + left: 0, + right: "auto", + bottom: "auto" + }); + } + element.css(size); + + return wrapper.css(props).show(); + }, + + removeWrapper: function (element) { + var active = document.activeElement; + + if (element.parent().is(".ui-effects-wrapper")) { + element.parent().replaceWith(element); + + // Fixes #7595 - Elements lose focus when wrapped. + if (element[0] === active || $.contains(element[0], active)) { + $(active).focus(); + } + } + + + return element; + }, + + setTransition: function (element, list, factor, value) { + value = value || {}; + $.each(list, function (i, x) { + var unit = element.cssUnit(x); + if (unit[0] > 0) { + value[x] = unit[0] * factor + unit[1]; + } + }); + return value; + } + }); + +// return an effect options object for the given parameters: + function _normalizeArguments(effect, options, speed, callback) { + + // allow passing all options as the first parameter + if ($.isPlainObject(effect)) { + options = effect; + effect = effect.effect; + } + + // convert to an object + effect = {effect: effect}; + + // catch (effect, null, ...) + if (options == null) { + options = {}; + } + + // catch (effect, callback) + if ($.isFunction(options)) { + callback = options; + speed = null; + options = {}; + } + + // catch (effect, speed, ?) + if (typeof options === "number" || $.fx.speeds[options]) { + callback = speed; + speed = options; + options = {}; + } + + // catch (effect, options, callback) + if ($.isFunction(speed)) { + callback = speed; + speed = null; + } + + // add options to effect + if (options) { + $.extend(effect, options); + } + + speed = speed || options.duration; + effect.duration = $.fx.off ? 0 : + typeof speed === "number" ? speed : + speed in $.fx.speeds ? $.fx.speeds[speed] : + $.fx.speeds._default; + + effect.complete = callback || options.complete; + + return effect; + } + + function standardAnimationOption(option) { + // Valid standard speeds (nothing, number, named speed) + if (!option || typeof option === "number" || $.fx.speeds[option]) { + return true; + } + + // Invalid strings - treat as "normal" speed + if (typeof option === "string" && !$.effects.effect[option]) { + return true; + } + + // Complete callback + if ($.isFunction(option)) { + return true; + } + + // Options hash (but not naming an effect) + if (typeof option === "object" && !option.effect) { + return true; + } + + // Didn't match any standard API + return false; + } + + $.fn.extend({ + effect: function ( /* effect, options, speed, callback */) { + var args = _normalizeArguments.apply(this, arguments), + mode = args.mode, + queue = args.queue, + effectMethod = $.effects.effect[args.effect]; + + if ($.fx.off || !effectMethod) { + // delegate to the original method (e.g., .show()) if possible + if (mode) { + return this[mode](args.duration, args.complete); + } else { + return this.each(function () { + if (args.complete) { + args.complete.call(this); + } + }); + } + } + + function run(next) { + var elem = $(this), + complete = args.complete, + mode = args.mode; + + function done() { + if ($.isFunction(complete)) { + complete.call(elem[0]); + } + if ($.isFunction(next)) { + next(); + } + } + + // If the element already has the correct final state, delegate to + // the core methods so the internal tracking of "olddisplay" works. + if (elem.is(":hidden") ? mode === "hide" : mode === "show") { + elem[mode](); + done(); + } else { + effectMethod.call(elem[0], args, done); + } + } + + return queue === false ? this.each(run) : this.queue(queue || "fx", run); + }, + + show: (function (orig) { + return function (option) { + if (standardAnimationOption(option)) { + return orig.apply(this, arguments); + } else { + var args = _normalizeArguments.apply(this, arguments); + args.mode = "show"; + return this.effect.call(this, args); + } + }; + })($.fn.show), + + hide: (function (orig) { + return function (option) { + if (standardAnimationOption(option)) { + return orig.apply(this, arguments); + } else { + var args = _normalizeArguments.apply(this, arguments); + args.mode = "hide"; + return this.effect.call(this, args); + } + }; + })($.fn.hide), + + toggle: (function (orig) { + return function (option) { + if (standardAnimationOption(option) || typeof option === "boolean") { + return orig.apply(this, arguments); + } else { + var args = _normalizeArguments.apply(this, arguments); + args.mode = "toggle"; + return this.effect.call(this, args); + } + }; + })($.fn.toggle), + + // helper functions + cssUnit: function (key) { + var style = this.css(key), + val = []; + + $.each(["em", "px", "%", "pt"], function (i, unit) { + if (style.indexOf(unit) > 0) { + val = [parseFloat(style), unit]; + } + }); + return val; + } + }); + + })(); + + /******************************************************************************/ + /*********************************** EASING ***********************************/ + /******************************************************************************/ + + (function () { + +// based on easing equations from Robert Penner (http://www.robertpenner.com/easing) + + var baseEasings = {}; + + $.each(["Quad", "Cubic", "Quart", "Quint", "Expo"], function (i, name) { + baseEasings[name] = function (p) { + return Math.pow(p, i + 2); + }; + }); + + $.extend(baseEasings, { + Sine: function (p) { + return 1 - Math.cos(p * Math.PI / 2); + }, + Circ: function (p) { + return 1 - Math.sqrt(1 - p * p); + }, + Elastic: function (p) { + return p === 0 || p === 1 ? p : + -Math.pow(2, 8 * (p - 1)) * Math.sin(((p - 1) * 80 - 7.5) * Math.PI / 15); + }, + Back: function (p) { + return p * p * (3 * p - 2); + }, + Bounce: function (p) { + var pow2, + bounce = 4; + + while (p < ((pow2 = Math.pow(2, --bounce)) - 1) / 11) { + } + return 1 / Math.pow(4, 3 - bounce) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - p, 2); + } + }); + + $.each(baseEasings, function (name, easeIn) { + $.easing["easeIn" + name] = easeIn; + $.easing["easeOut" + name] = function (p) { + return 1 - easeIn(1 - p); + }; + $.easing["easeInOut" + name] = function (p) { + return p < 0.5 ? + easeIn(p * 2) / 2 : + 1 - easeIn(p * -2 + 2) / 2; + }; + }); + + })(); + +}); diff --git a/lib/web/jquery/ui-modules/menu.js b/lib/web/jquery/ui-modules/menu.js new file mode 100644 index 000000000000..6d575b0462a9 --- /dev/null +++ b/lib/web/jquery/ui-modules/menu.js @@ -0,0 +1,628 @@ +/*! + * jQuery UI Menu - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/menu/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/widget', + 'jquery-ui-modules/position' +], function ($, undefined) { + + $.widget("ui.menu", { + version: "1.10.4", + defaultElement: "<ul>", + delay: 300, + options: { + icons: { + submenu: "ui-icon-carat-1-e" + }, + menus: "ul", + position: { + my: "left top", + at: "right top" + }, + role: "menu", + + // callbacks + blur: null, + focus: null, + select: null + }, + + _create: function () { + this.activeMenu = this.element; + // flag used to prevent firing of the click handler + // as the event bubbles up through nested menus + this.mouseHandled = false; + this.element + .uniqueId() + .addClass("ui-menu ui-widget ui-widget-content ui-corner-all") + .toggleClass("ui-menu-icons", !!this.element.find(".ui-icon").length) + .attr({ + role: this.options.role, + tabIndex: 0 + }) + // need to catch all clicks on disabled menu + // not possible through _on + .bind("click" + this.eventNamespace, $.proxy(function (event) { + if (this.options.disabled) { + event.preventDefault(); + } + }, this)); + + if (this.options.disabled) { + this.element + .addClass("ui-state-disabled") + .attr("aria-disabled", "true"); + } + + this._on({ + // Prevent focus from sticking to links inside menu after clicking + // them (focus should always stay on UL during navigation). + "mousedown .ui-menu-item > a": function (event) { + event.preventDefault(); + }, + "click .ui-state-disabled > a": function (event) { + event.preventDefault(); + }, + "click .ui-menu-item:has(a)": function (event) { + var target = $(event.target).closest(".ui-menu-item"); + if (!this.mouseHandled && target.not(".ui-state-disabled").length) { + this.select(event); + + // Only set the mouseHandled flag if the event will bubble, see #9469. + if (!event.isPropagationStopped()) { + this.mouseHandled = true; + } + + // Open submenu on click + if (target.has(".ui-menu").length) { + this.expand(event); + } else if (!this.element.is(":focus") && $(this.document[0].activeElement).closest(".ui-menu").length) { + + // Redirect focus to the menu + this.element.trigger("focus", [true]); + + // If the active item is on the top level, let it stay active. + // Otherwise, blur the active item since it is no longer visible. + if (this.active && this.active.parents(".ui-menu").length === 1) { + clearTimeout(this.timer); + } + } + } + }, + "mouseenter .ui-menu-item": function (event) { + var target = $(event.currentTarget); + // Remove ui-state-active class from siblings of the newly focused menu item + // to avoid a jump caused by adjacent elements both having a class with a border + target.siblings().children(".ui-state-active").removeClass("ui-state-active"); + this.focus(event, target); + }, + mouseleave: "collapseAll", + "mouseleave .ui-menu": "collapseAll", + focus: function (event, keepActiveItem) { + // If there's already an active item, keep it active + // If not, activate the first item + var item = this.active || this.element.children(".ui-menu-item").eq(0); + + if (!keepActiveItem) { + this.focus(event, item); + } + }, + blur: function (event) { + this._delay(function () { + if (!$.contains(this.element[0], this.document[0].activeElement)) { + this.collapseAll(event); + } + }); + }, + keydown: "_keydown" + }); + + this.refresh(); + + // Clicks outside of a menu collapse any open menus + this._on(this.document, { + click: function (event) { + if (!$(event.target).closest(".ui-menu").length) { + this.collapseAll(event); + } + + // Reset the mouseHandled flag + this.mouseHandled = false; + } + }); + }, + + _destroy: function () { + // Destroy (sub)menus + this.element + .removeAttr("aria-activedescendant") + .find(".ui-menu").addBack() + .removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons") + .removeAttr("role") + .removeAttr("tabIndex") + .removeAttr("aria-labelledby") + .removeAttr("aria-expanded") + .removeAttr("aria-hidden") + .removeAttr("aria-disabled") + .removeUniqueId() + .show(); + + // Destroy menu items + this.element.find(".ui-menu-item") + .removeClass("ui-menu-item") + .removeAttr("role") + .removeAttr("aria-disabled") + .children("a") + .removeUniqueId() + .removeClass("ui-corner-all ui-state-hover") + .removeAttr("tabIndex") + .removeAttr("role") + .removeAttr("aria-haspopup") + .children().each(function () { + var elem = $(this); + if (elem.data("ui-menu-submenu-carat")) { + elem.remove(); + } + }); + + // Destroy menu dividers + this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content"); + }, + + _keydown: function (event) { + var match, prev, character, skip, regex, + preventDefault = true; + + function escape(value) { + return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); + } + + switch (event.keyCode) { + case $.ui.keyCode.PAGE_UP: + this.previousPage(event); + break; + case $.ui.keyCode.PAGE_DOWN: + this.nextPage(event); + break; + case $.ui.keyCode.HOME: + this._move("first", "first", event); + break; + case $.ui.keyCode.END: + this._move("last", "last", event); + break; + case $.ui.keyCode.UP: + this.previous(event); + break; + case $.ui.keyCode.DOWN: + this.next(event); + break; + case $.ui.keyCode.LEFT: + this.collapse(event); + break; + case $.ui.keyCode.RIGHT: + if (this.active && !this.active.is(".ui-state-disabled")) { + this.expand(event); + } + break; + case $.ui.keyCode.ENTER: + case $.ui.keyCode.SPACE: + this._activate(event); + break; + case $.ui.keyCode.ESCAPE: + this.collapse(event); + break; + default: + preventDefault = false; + prev = this.previousFilter || ""; + character = String.fromCharCode(event.keyCode); + skip = false; + + clearTimeout(this.filterTimer); + + if (character === prev) { + skip = true; + } else { + character = prev + character; + } + + regex = new RegExp("^" + escape(character), "i"); + match = this.activeMenu.children(".ui-menu-item").filter(function () { + return regex.test($(this).children("a").text()); + }); + match = skip && match.index(this.active.next()) !== -1 ? + this.active.nextAll(".ui-menu-item") : + match; + + // If no matches on the current filter, reset to the last character pressed + // to move down the menu to the first item that starts with that character + if (!match.length) { + character = String.fromCharCode(event.keyCode); + regex = new RegExp("^" + escape(character), "i"); + match = this.activeMenu.children(".ui-menu-item").filter(function () { + return regex.test($(this).children("a").text()); + }); + } + + if (match.length) { + this.focus(event, match); + if (match.length > 1) { + this.previousFilter = character; + this.filterTimer = this._delay(function () { + delete this.previousFilter; + }, 1000); + } else { + delete this.previousFilter; + } + } else { + delete this.previousFilter; + } + } + + if (preventDefault) { + event.preventDefault(); + } + }, + + _activate: function (event) { + if (!this.active.is(".ui-state-disabled")) { + if (this.active.children("a[aria-haspopup='true']").length) { + this.expand(event); + } else { + this.select(event); + } + } + }, + + refresh: function () { + var menus, + icon = this.options.icons.submenu, + submenus = this.element.find(this.options.menus); + + this.element.toggleClass("ui-menu-icons", !!this.element.find(".ui-icon").length); + + // Initialize nested menus + submenus.filter(":not(.ui-menu)") + .addClass("ui-menu ui-widget ui-widget-content ui-corner-all") + .hide() + .attr({ + role: this.options.role, + "aria-hidden": "true", + "aria-expanded": "false" + }) + .each(function () { + var menu = $(this), + item = menu.prev("a"), + submenuCarat = $("<span>") + .addClass("ui-menu-icon ui-icon " + icon) + .data("ui-menu-submenu-carat", true); + + item + .attr("aria-haspopup", "true") + .prepend(submenuCarat); + menu.attr("aria-labelledby", item.attr("id")); + }); + + menus = submenus.add(this.element); + + // Don't refresh list items that are already adapted + menus.children(":not(.ui-menu-item):has(a)") + .addClass("ui-menu-item") + .attr("role", "presentation") + .children("a") + .uniqueId() + .addClass("ui-corner-all") + .attr({ + tabIndex: -1, + role: this._itemRole() + }); + + // Initialize unlinked menu-items containing spaces and/or dashes only as dividers + menus.children(":not(.ui-menu-item)").each(function () { + var item = $(this); + // hyphen, em dash, en dash + if (!/[^\-\u2014\u2013\s]/.test(item.text())) { + item.addClass("ui-widget-content ui-menu-divider"); + } + }); + + // Add aria-disabled attribute to any disabled menu item + menus.children(".ui-state-disabled").attr("aria-disabled", "true"); + + // If the active item has been removed, blur the menu + if (this.active && !$.contains(this.element[0], this.active[0])) { + this.blur(); + } + }, + + _itemRole: function () { + return { + menu: "menuitem", + listbox: "option" + }[this.options.role]; + }, + + _setOption: function (key, value) { + if (key === "icons") { + this.element.find(".ui-menu-icon") + .removeClass(this.options.icons.submenu) + .addClass(value.submenu); + } + this._super(key, value); + }, + + focus: function (event, item) { + var nested, focused; + this.blur(event, event && event.type === "focus"); + + this._scrollIntoView(item); + + this.active = item.first(); + focused = this.active.children("a").addClass("ui-state-focus"); + // Only update aria-activedescendant if there's a role + // otherwise we assume focus is managed elsewhere + if (this.options.role) { + this.element.attr("aria-activedescendant", focused.attr("id")); + } + + // Highlight active parent menu item, if any + this.active + .parent() + .closest(".ui-menu-item") + .children("a:first") + .addClass("ui-state-active"); + + if (event && event.type === "keydown") { + this._close(); + } else { + this.timer = this._delay(function () { + this._close(); + }, this.delay); + } + + nested = item.children(".ui-menu"); + if (nested.length && event && (/^mouse/.test(event.type))) { + this._startOpening(nested); + } + this.activeMenu = item.parent(); + + this._trigger("focus", event, {item: item}); + }, + + _scrollIntoView: function (item) { + var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; + if (this._hasScroll()) { + borderTop = parseFloat($.css(this.activeMenu[0], "borderTopWidth")) || 0; + paddingTop = parseFloat($.css(this.activeMenu[0], "paddingTop")) || 0; + offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; + scroll = this.activeMenu.scrollTop(); + elementHeight = this.activeMenu.height(); + itemHeight = item.height(); + + if (offset < 0) { + this.activeMenu.scrollTop(scroll + offset); + } else if (offset + itemHeight > elementHeight) { + this.activeMenu.scrollTop(scroll + offset - elementHeight + itemHeight); + } + } + }, + + blur: function (event, fromFocus) { + if (!fromFocus) { + clearTimeout(this.timer); + } + + if (!this.active) { + return; + } + + this.active.children("a").removeClass("ui-state-focus"); + this.active = null; + + this._trigger("blur", event, {item: this.active}); + }, + + _startOpening: function (submenu) { + clearTimeout(this.timer); + + // Don't open if already open fixes a Firefox bug that caused a .5 pixel + // shift in the submenu position when mousing over the carat icon + if (submenu.attr("aria-hidden") !== "true") { + return; + } + + this.timer = this._delay(function () { + this._close(); + this._open(submenu); + }, this.delay); + }, + + _open: function (submenu) { + var position = $.extend({ + of: this.active + }, this.options.position); + + clearTimeout(this.timer); + this.element.find(".ui-menu").not(submenu.parents(".ui-menu")) + .hide() + .attr("aria-hidden", "true"); + + submenu + .show() + .removeAttr("aria-hidden") + .attr("aria-expanded", "true") + .position(position); + }, + + collapseAll: function (event, all) { + clearTimeout(this.timer); + this.timer = this._delay(function () { + // If we were passed an event, look for the submenu that contains the event + var currentMenu = all ? this.element : + $(event && event.target).closest(this.element.find(".ui-menu")); + + // If we found no valid submenu ancestor, use the main menu to close all sub menus anyway + if (!currentMenu.length) { + currentMenu = this.element; + } + + this._close(currentMenu); + + this.blur(event); + this.activeMenu = currentMenu; + }, this.delay); + }, + + // With no arguments, closes the currently active menu - if nothing is active + // it closes all menus. If passed an argument, it will search for menus BELOW + _close: function (startMenu) { + if (!startMenu) { + startMenu = this.active ? this.active.parent() : this.element; + } + + startMenu + .find(".ui-menu") + .hide() + .attr("aria-hidden", "true") + .attr("aria-expanded", "false") + .end() + .find("a.ui-state-active") + .removeClass("ui-state-active"); + }, + + collapse: function (event) { + var newItem = this.active && + this.active.parent().closest(".ui-menu-item", this.element); + if (newItem && newItem.length) { + this._close(); + this.focus(event, newItem); + } + }, + + expand: function (event) { + var newItem = this.active && + this.active + .children(".ui-menu ") + .children(".ui-menu-item") + .first(); + + if (newItem && newItem.length) { + this._open(newItem.parent()); + + // Delay so Firefox will not hide activedescendant change in expanding submenu from AT + this._delay(function () { + this.focus(event, newItem); + }); + } + }, + + next: function (event) { + this._move("next", "first", event); + }, + + previous: function (event) { + this._move("prev", "last", event); + }, + + isFirstItem: function () { + return this.active && !this.active.prevAll(".ui-menu-item").length; + }, + + isLastItem: function () { + return this.active && !this.active.nextAll(".ui-menu-item").length; + }, + + _move: function (direction, filter, event) { + var next; + if (this.active) { + if (direction === "first" || direction === "last") { + next = this.active + [direction === "first" ? "prevAll" : "nextAll"](".ui-menu-item") + .eq(-1); + } else { + next = this.active + [direction + "All"](".ui-menu-item") + .eq(0); + } + } + if (!next || !next.length || !this.active) { + next = this.activeMenu.children(".ui-menu-item")[filter](); + } + + this.focus(event, next); + }, + + nextPage: function (event) { + var item, base, height; + + if (!this.active) { + this.next(event); + return; + } + if (this.isLastItem()) { + return; + } + if (this._hasScroll()) { + base = this.active.offset().top; + height = this.element.height(); + this.active.nextAll(".ui-menu-item").each(function () { + item = $(this); + return item.offset().top - base - height < 0; + }); + + this.focus(event, item); + } else { + this.focus(event, this.activeMenu.children(".ui-menu-item") + [!this.active ? "first" : "last"]()); + } + }, + + previousPage: function (event) { + var item, base, height; + if (!this.active) { + this.next(event); + return; + } + if (this.isFirstItem()) { + return; + } + if (this._hasScroll()) { + base = this.active.offset().top; + height = this.element.height(); + this.active.prevAll(".ui-menu-item").each(function () { + item = $(this); + return item.offset().top - base + height > 0; + }); + + this.focus(event, item); + } else { + this.focus(event, this.activeMenu.children(".ui-menu-item").first()); + } + }, + + _hasScroll: function () { + return this.element.outerHeight() < this.element.prop("scrollHeight"); + }, + + select: function (event) { + // TODO: It should never be possible to not have an active item at this + // point, but the tests don't trigger mouseenter before click. + this.active = this.active || $(event.target).closest(".ui-menu-item"); + var ui = {item: this.active}; + if (!this.active.has(".ui-menu").length) { + this.collapseAll(event, true); + } + this._trigger("select", event, ui); + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/mouse.js b/lib/web/jquery/ui-modules/mouse.js new file mode 100644 index 000000000000..aefa64a5ea0a --- /dev/null +++ b/lib/web/jquery/ui-modules/mouse.js @@ -0,0 +1,177 @@ +/*! + * jQuery UI Mouse - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/mouse/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/widget' +], function ($, undefined) { + + var mouseHandled = false; + $(document).mouseup(function () { + mouseHandled = false; + }); + + $.widget("ui.mouse", { + version: "1.10.4", + options: { + cancel: "input,textarea,button,select,option", + distance: 1, + delay: 0 + }, + _mouseInit: function () { + var that = this; + + this.element + .bind("mousedown." + this.widgetName, function (event) { + return that._mouseDown(event); + }) + .bind("click." + this.widgetName, function (event) { + if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { + $.removeData(event.target, that.widgetName + ".preventClickEvent"); + event.stopImmediatePropagation(); + return false; + } + }); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function () { + this.element.unbind("." + this.widgetName); + if (this._mouseMoveDelegate) { + $(document) + .unbind("mousemove." + this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup." + this.widgetName, this._mouseUpDelegate); + } + }, + + _mouseDown: function (event) { + // don't let more than one widget handle mouseStart + if (mouseHandled) { + return; + } + + // we may have missed mouseup (out of window) + (this._mouseStarted && this._mouseUp(event)); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = (event.which === 1), + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); + if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if (!this.mouseDelayMet) { + this._mouseDelayTimer = setTimeout(function () { + that.mouseDelayMet = true; + }, this.options.delay); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = (this._mouseStart(event) !== false); + if (!this._mouseStarted) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { + $.removeData(event.target, this.widgetName + ".preventClickEvent"); + } + + // these delegates are required to keep context + this._mouseMoveDelegate = function (event) { + return that._mouseMove(event); + }; + this._mouseUpDelegate = function (event) { + return that._mouseUp(event); + }; + $(document) + .bind("mousemove." + this.widgetName, this._mouseMoveDelegate) + .bind("mouseup." + this.widgetName, this._mouseUpDelegate); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function (event) { + // IE mouseup check - mouseup happened when mouse was out of window + if ($.ui.ie && (!document.documentMode || document.documentMode < 9) && !event.button) { + return this._mouseUp(event); + } + + if (this._mouseStarted) { + this._mouseDrag(event); + return event.preventDefault(); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = + (this._mouseStart(this._mouseDownEvent, event) !== false); + (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); + } + + return !this._mouseStarted; + }, + + _mouseUp: function (event) { + $(document) + .unbind("mousemove." + this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup." + this.widgetName, this._mouseUpDelegate); + + if (this._mouseStarted) { + this._mouseStarted = false; + + if (event.target === this._mouseDownEvent.target) { + $.data(event.target, this.widgetName + ".preventClickEvent", true); + } + + this._mouseStop(event); + } + + return false; + }, + + _mouseDistanceMet: function (event) { + return (Math.max( + Math.abs(this._mouseDownEvent.pageX - event.pageX), + Math.abs(this._mouseDownEvent.pageY - event.pageY) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function (/* event */) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overridden by extending plugin + _mouseStart: function (/* event */) { + }, + _mouseDrag: function (/* event */) { + }, + _mouseStop: function (/* event */) { + }, + _mouseCapture: function (/* event */) { + return true; + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/position.js b/lib/web/jquery/ui-modules/position.js new file mode 100644 index 000000000000..6a3ffb151622 --- /dev/null +++ b/lib/web/jquery/ui-modules/position.js @@ -0,0 +1,500 @@ +/*! + * jQuery UI Position - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +define([ + 'jquery' +], function ($, undefined) { + + $.ui = $.ui || {}; + + var cachedScrollbarWidth, + max = Math.max, + abs = Math.abs, + round = Math.round, + rhorizontal = /left|center|right/, + rvertical = /top|center|bottom/, + roffset = /[\+\-]\d+(\.[\d]+)?%?/, + rposition = /^\w+/, + rpercent = /%$/, + _position = $.fn.position; + + function getOffsets(offsets, width, height) { + return [ + parseFloat(offsets[0]) * (rpercent.test(offsets[0]) ? width / 100 : 1), + parseFloat(offsets[1]) * (rpercent.test(offsets[1]) ? height / 100 : 1) + ]; + } + + function parseCss(element, property) { + return parseInt($.css(element, property), 10) || 0; + } + + function getDimensions(elem) { + var raw = elem[0]; + if (raw.nodeType === 9) { + return { + width: elem.width(), + height: elem.height(), + offset: {top: 0, left: 0} + }; + } + if ($.isWindow(raw)) { + return { + width: elem.width(), + height: elem.height(), + offset: {top: elem.scrollTop(), left: elem.scrollLeft()} + }; + } + if (raw.preventDefault) { + return { + width: 0, + height: 0, + offset: {top: raw.pageY, left: raw.pageX} + }; + } + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset() + }; + } + + $.position = { + scrollbarWidth: function () { + if (cachedScrollbarWidth !== undefined) { + return cachedScrollbarWidth; + } + var w1, w2, + div = $("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"), + innerDiv = div.children()[0]; + + $("body").append(div); + w1 = innerDiv.offsetWidth; + div.css("overflow", "scroll"); + + w2 = innerDiv.offsetWidth; + + if (w1 === w2) { + w2 = div[0].clientWidth; + } + + div.remove(); + + return (cachedScrollbarWidth = w1 - w2); + }, + getScrollInfo: function (within) { + var overflowX = within.isWindow || within.isDocument ? "" : + within.element.css("overflow-x"), + overflowY = within.isWindow || within.isDocument ? "" : + within.element.css("overflow-y"), + hasOverflowX = overflowX === "scroll" || + (overflowX === "auto" && within.width < within.element[0].scrollWidth), + hasOverflowY = overflowY === "scroll" || + (overflowY === "auto" && within.height < within.element[0].scrollHeight); + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function (element) { + var withinElement = $(element || window), + isWindow = $.isWindow(withinElement[0]), + isDocument = !!withinElement[0] && withinElement[0].nodeType === 9; + return { + element: withinElement, + isWindow: isWindow, + isDocument: isDocument, + offset: withinElement.offset() || {left: 0, top: 0}, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: isWindow ? withinElement.width() : withinElement.outerWidth(), + height: isWindow ? withinElement.height() : withinElement.outerHeight() + }; + } + }; + + $.fn.position = function (options) { + if (!options || !options.of) { + return _position.apply(this, arguments); + } + + // make a copy, we don't want to modify arguments + options = $.extend({}, options); + + var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, + target = $(options.of), + within = $.position.getWithinInfo(options.within), + scrollInfo = $.position.getScrollInfo(within), + collision = (options.collision || "flip").split(" "), + offsets = {}; + + dimensions = getDimensions(target); + if (target[0].preventDefault) { + // force left top to allow flipping + options.at = "left top"; + } + targetWidth = dimensions.width; + targetHeight = dimensions.height; + targetOffset = dimensions.offset; + // clone to reuse original targetOffset later + basePosition = $.extend({}, targetOffset); + + // force my and at to have valid horizontal and vertical positions + // if a value is missing or invalid, it will be converted to center + $.each(["my", "at"], function () { + var pos = (options[this] || "").split(" "), + horizontalOffset, + verticalOffset; + + if (pos.length === 1) { + pos = rhorizontal.test(pos[0]) ? + pos.concat(["center"]) : + rvertical.test(pos[0]) ? + ["center"].concat(pos) : + ["center", "center"]; + } + pos[0] = rhorizontal.test(pos[0]) ? pos[0] : "center"; + pos[1] = rvertical.test(pos[1]) ? pos[1] : "center"; + + // calculate offsets + horizontalOffset = roffset.exec(pos[0]); + verticalOffset = roffset.exec(pos[1]); + offsets[this] = [ + horizontalOffset ? horizontalOffset[0] : 0, + verticalOffset ? verticalOffset[0] : 0 + ]; + + // reduce to just the positions without the offsets + options[this] = [ + rposition.exec(pos[0])[0], + rposition.exec(pos[1])[0] + ]; + }); + + // normalize collision option + if (collision.length === 1) { + collision[1] = collision[0]; + } + + if (options.at[0] === "right") { + basePosition.left += targetWidth; + } else if (options.at[0] === "center") { + basePosition.left += targetWidth / 2; + } + + if (options.at[1] === "bottom") { + basePosition.top += targetHeight; + } else if (options.at[1] === "center") { + basePosition.top += targetHeight / 2; + } + + atOffset = getOffsets(offsets.at, targetWidth, targetHeight); + basePosition.left += atOffset[0]; + basePosition.top += atOffset[1]; + + return this.each(function () { + var collisionPosition, using, + elem = $(this), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseCss(this, "marginLeft"), + marginTop = parseCss(this, "marginTop"), + collisionWidth = elemWidth + marginLeft + parseCss(this, "marginRight") + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss(this, "marginBottom") + scrollInfo.height, + position = $.extend({}, basePosition), + myOffset = getOffsets(offsets.my, elem.outerWidth(), elem.outerHeight()); + + if (options.my[0] === "right") { + position.left -= elemWidth; + } else if (options.my[0] === "center") { + position.left -= elemWidth / 2; + } + + if (options.my[1] === "bottom") { + position.top -= elemHeight; + } else if (options.my[1] === "center") { + position.top -= elemHeight / 2; + } + + position.left += myOffset[0]; + position.top += myOffset[1]; + + // if the browser doesn't support fractions, then round for consistent results + if (!$.support.offsetFractions) { + position.left = round(position.left); + position.top = round(position.top); + } + + collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + + $.each(["left", "top"], function (i, dir) { + if ($.ui.position[collision[i]]) { + $.ui.position[collision[i]][dir](position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [atOffset[0] + myOffset[0], atOffset [1] + myOffset[1]], + my: options.my, + at: options.at, + within: within, + elem: elem + }); + } + }); + + if (options.using) { + // adds feedback as second argument to using callback, if present + using = function (props) { + var left = targetOffset.left - position.left, + right = left + targetWidth - elemWidth, + top = targetOffset.top - position.top, + bottom = top + targetHeight - elemHeight, + feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", + vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" + }; + if (targetWidth < elemWidth && abs(left + right) < targetWidth) { + feedback.horizontal = "center"; + } + if (targetHeight < elemHeight && abs(top + bottom) < targetHeight) { + feedback.vertical = "middle"; + } + if (max(abs(left), abs(right)) > max(abs(top), abs(bottom))) { + feedback.important = "horizontal"; + } else { + feedback.important = "vertical"; + } + options.using.call(this, props, feedback); + }; + } + + elem.offset($.extend(position, {using: using})); + }); + }; + + $.ui.position = { + fit: { + left: function (position, data) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, + outerWidth = within.width, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = withinOffset - collisionPosLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, + newOverRight; + + // element is wider than within + if (data.collisionWidth > outerWidth) { + // element is initially over the left side of within + if (overLeft > 0 && overRight <= 0) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; + position.left += overLeft - newOverRight; + // element is initially over right side of within + } else if (overRight > 0 && overLeft <= 0) { + position.left = withinOffset; + // element is initially over both left and right sides of within + } else { + if (overLeft > overRight) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } + // too far left -> align with left edge + } else if (overLeft > 0) { + position.left += overLeft; + // too far right -> align with right edge + } else if (overRight > 0) { + position.left -= overRight; + // adjust based on position and margin + } else { + position.left = max(position.left - collisionPosLeft, position.left); + } + }, + top: function (position, data) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollTop : within.offset.top, + outerHeight = data.within.height, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = withinOffset - collisionPosTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, + newOverBottom; + + // element is taller than within + if (data.collisionHeight > outerHeight) { + // element is initially over the top of within + if (overTop > 0 && overBottom <= 0) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; + position.top += overTop - newOverBottom; + // element is initially over bottom of within + } else if (overBottom > 0 && overTop <= 0) { + position.top = withinOffset; + // element is initially over both top and bottom of within + } else { + if (overTop > overBottom) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } + // too far up -> align with top + } else if (overTop > 0) { + position.top += overTop; + // too far down -> align with bottom edge + } else if (overBottom > 0) { + position.top -= overBottom; + // adjust based on position and margin + } else { + position.top = max(position.top - collisionPosTop, position.top); + } + } + }, + flip: { + left: function (position, data) { + var within = data.within, + withinOffset = within.offset.left + within.scrollLeft, + outerWidth = within.width, + offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = collisionPosLeft - offsetLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, + myOffset = data.my[0] === "left" ? + -data.elemWidth : + data.my[0] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[0] === "left" ? + data.targetWidth : + data.at[0] === "right" ? + -data.targetWidth : + 0, + offset = -2 * data.offset[0], + newOverRight, + newOverLeft; + + if (overLeft < 0) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; + if (newOverRight < 0 || newOverRight < abs(overLeft)) { + position.left += myOffset + atOffset + offset; + } + } else if (overRight > 0) { + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft; + if (newOverLeft > 0 || abs(newOverLeft) < overRight) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function (position, data) { + var within = data.within, + withinOffset = within.offset.top + within.scrollTop, + outerHeight = within.height, + offsetTop = within.isWindow ? within.scrollTop : within.offset.top, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = collisionPosTop - offsetTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, + top = data.my[1] === "top", + myOffset = top ? + -data.elemHeight : + data.my[1] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[1] === "top" ? + data.targetHeight : + data.at[1] === "bottom" ? + -data.targetHeight : + 0, + offset = -2 * data.offset[1], + newOverTop, + newOverBottom; + if (overTop < 0) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; + if ((position.top + myOffset + atOffset + offset) > overTop && (newOverBottom < 0 || newOverBottom < abs(overTop))) { + position.top += myOffset + atOffset + offset; + } + } else if (overBottom > 0) { + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop; + if ((position.top + myOffset + atOffset + offset) > overBottom && (newOverTop > 0 || abs(newOverTop) < overBottom)) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function () { + $.ui.position.flip.left.apply(this, arguments); + $.ui.position.fit.left.apply(this, arguments); + }, + top: function () { + $.ui.position.flip.top.apply(this, arguments); + $.ui.position.fit.top.apply(this, arguments); + } + } + }; + +// fraction support test + (function () { + var testElement, testElementParent, testElementStyle, offsetLeft, i, + body = document.getElementsByTagName("body")[0], + div = document.createElement("div"); + + //Create a "fake body" for testing based on method used in jQuery.support + testElement = document.createElement(body ? "div" : "body"); + testElementStyle = { + visibility: "hidden", + width: 0, + height: 0, + border: 0, + margin: 0, + background: "none" + }; + if (body) { + $.extend(testElementStyle, { + position: "absolute", + left: "-1000px", + top: "-1000px" + }); + } + for (i in testElementStyle) { + testElement.style[i] = testElementStyle[i]; + } + testElement.appendChild(div); + testElementParent = body || document.documentElement; + testElementParent.insertBefore(testElement, testElementParent.firstChild); + + div.style.cssText = "position: absolute; left: 10.7432222px;"; + + offsetLeft = $(div).offset().left; + $.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11; + + testElement.innerHTML = ""; + testElementParent.removeChild(testElement); + })(); + +}); diff --git a/lib/web/jquery/ui-modules/progressbar.js b/lib/web/jquery/ui-modules/progressbar.js new file mode 100644 index 000000000000..f38ece369e87 --- /dev/null +++ b/lib/web/jquery/ui-modules/progressbar.js @@ -0,0 +1,146 @@ +/*! + * jQuery UI Progressbar - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/progressbar/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/widget' +], function ($, undefined) { + + $.widget("ui.progressbar", { + version: "1.10.4", + options: { + max: 100, + value: 0, + + change: null, + complete: null + }, + + min: 0, + + _create: function () { + // Constrain initial value + this.oldValue = this.options.value = this._constrainedValue(); + + this.element + .addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all") + .attr({ + // Only set static values, aria-valuenow and aria-valuemax are + // set inside _refreshValue() + role: "progressbar", + "aria-valuemin": this.min + }); + + this.valueDiv = $("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>") + .appendTo(this.element); + + this._refreshValue(); + }, + + _destroy: function () { + this.element + .removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all") + .removeAttr("role") + .removeAttr("aria-valuemin") + .removeAttr("aria-valuemax") + .removeAttr("aria-valuenow"); + + this.valueDiv.remove(); + }, + + value: function (newValue) { + if (newValue === undefined) { + return this.options.value; + } + + this.options.value = this._constrainedValue(newValue); + this._refreshValue(); + }, + + _constrainedValue: function (newValue) { + if (newValue === undefined) { + newValue = this.options.value; + } + + this.indeterminate = newValue === false; + + // sanitize value + if (typeof newValue !== "number") { + newValue = 0; + } + + return this.indeterminate ? false : + Math.min(this.options.max, Math.max(this.min, newValue)); + }, + + _setOptions: function (options) { + // Ensure "value" option is set after other values (like max) + var value = options.value; + delete options.value; + + this._super(options); + + this.options.value = this._constrainedValue(value); + this._refreshValue(); + }, + + _setOption: function (key, value) { + if (key === "max") { + // Don't allow a max less than min + value = Math.max(this.min, value); + } + + this._super(key, value); + }, + + _percentage: function () { + return this.indeterminate ? 100 : 100 * (this.options.value - this.min) / (this.options.max - this.min); + }, + + _refreshValue: function () { + var value = this.options.value, + percentage = this._percentage(); + + this.valueDiv + .toggle(this.indeterminate || value > this.min) + .toggleClass("ui-corner-right", value === this.options.max) + .width(percentage.toFixed(0) + "%"); + + this.element.toggleClass("ui-progressbar-indeterminate", this.indeterminate); + + if (this.indeterminate) { + this.element.removeAttr("aria-valuenow"); + if (!this.overlayDiv) { + this.overlayDiv = $("<div class='ui-progressbar-overlay'></div>").appendTo(this.valueDiv); + } + } else { + this.element.attr({ + "aria-valuemax": this.options.max, + "aria-valuenow": value + }); + if (this.overlayDiv) { + this.overlayDiv.remove(); + this.overlayDiv = null; + } + } + + if (this.oldValue !== value) { + this.oldValue = value; + this._trigger("change"); + } + if (value === this.options.max) { + this._trigger("complete"); + } + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/resizable.js b/lib/web/jquery/ui-modules/resizable.js new file mode 100644 index 000000000000..aa3aa205673f --- /dev/null +++ b/lib/web/jquery/ui-modules/resizable.js @@ -0,0 +1,1023 @@ +/*! + * jQuery UI Resizable - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/resizable/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/mouse' +], function ($, undefined) { + + function num(v) { + return parseInt(v, 10) || 0; + } + + function isNumber(value) { + return !isNaN(parseInt(value, 10)); + } + + $.widget("ui.resizable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "resize", + options: { + alsoResize: false, + animate: false, + animateDuration: "slow", + animateEasing: "swing", + aspectRatio: false, + autoHide: false, + containment: false, + ghost: false, + grid: false, + handles: "e,s,se", + helper: false, + maxHeight: null, + maxWidth: null, + minHeight: 10, + minWidth: 10, + // See #7960 + zIndex: 90, + + // callbacks + resize: null, + start: null, + stop: null + }, + _create: function () { + + var n, i, handle, axis, hname, + that = this, + o = this.options; + this.element.addClass("ui-resizable"); + + $.extend(this, { + _aspectRatio: !!(o.aspectRatio), + aspectRatio: o.aspectRatio, + originalElement: this.element, + _proportionallyResizeElements: [], + _helper: o.helper || o.ghost || o.animate ? o.helper || "ui-resizable-helper" : null + }); + + //Wrap the element if it cannot hold child nodes + if (this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)) { + + //Create a wrapper element and set the wrapper to the new current internal element + this.element.wrap( + $("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({ + position: this.element.css("position"), + width: this.element.outerWidth(), + height: this.element.outerHeight(), + top: this.element.css("top"), + left: this.element.css("left") + }) + ); + + //Overwrite the original this.element + this.element = this.element.parent().data( + "ui-resizable", this.element.data("ui-resizable") + ); + + this.elementIsWrapper = true; + + //Move margins to the wrapper + this.element.css({ + marginLeft: this.originalElement.css("marginLeft"), + marginTop: this.originalElement.css("marginTop"), + marginRight: this.originalElement.css("marginRight"), + marginBottom: this.originalElement.css("marginBottom") + }); + this.originalElement.css({marginLeft: 0, marginTop: 0, marginRight: 0, marginBottom: 0}); + + //Prevent Safari textarea resize + this.originalResizeStyle = this.originalElement.css("resize"); + this.originalElement.css("resize", "none"); + + //Push the actual element to our proportionallyResize internal array + this._proportionallyResizeElements.push(this.originalElement.css({ + position: "static", + zoom: 1, + display: "block" + })); + + // avoid IE jump (hard set the margin) + this.originalElement.css({margin: this.originalElement.css("margin")}); + + // fix handlers offset + this._proportionallyResize(); + + } + + this.handles = o.handles || (!$(".ui-resizable-handle", this.element).length ? "e,s,se" : { + n: ".ui-resizable-n", + e: ".ui-resizable-e", + s: ".ui-resizable-s", + w: ".ui-resizable-w", + se: ".ui-resizable-se", + sw: ".ui-resizable-sw", + ne: ".ui-resizable-ne", + nw: ".ui-resizable-nw" + }); + if (this.handles.constructor === String) { + + if (this.handles === "all") { + this.handles = "n,e,s,w,se,sw,ne,nw"; + } + + n = this.handles.split(","); + this.handles = {}; + + for (i = 0; i < n.length; i++) { + + handle = $.trim(n[i]); + hname = "ui-resizable-" + handle; + axis = $("<div class='ui-resizable-handle " + hname + "'></div>"); + + // Apply zIndex to all handles - see #7960 + axis.css({zIndex: o.zIndex}); + + //TODO : What's going on here? + if ("se" === handle) { + axis.addClass("ui-icon ui-icon-gripsmall-diagonal-se"); + } + + //Insert into internal handles object and append to element + this.handles[handle] = ".ui-resizable-" + handle; + this.element.append(axis); + } + + } + + this._renderAxis = function (target) { + + var i, axis, padPos, padWrapper; + + target = target || this.element; + + for (i in this.handles) { + + if (this.handles[i].constructor === String) { + this.handles[i] = $(this.handles[i], this.element).show(); + } + + //Apply pad to wrapper element, needed to fix axis position (textarea, inputs, scrolls) + if (this.elementIsWrapper && this.originalElement[0].nodeName.match(/textarea|input|select|button/i)) { + + axis = $(this.handles[i], this.element); + + //Checking the correct pad and border + padWrapper = /sw|ne|nw|se|n|s/.test(i) ? axis.outerHeight() : axis.outerWidth(); + + //The padding type i have to apply... + padPos = ["padding", + /ne|nw|n/.test(i) ? "Top" : + /se|sw|s/.test(i) ? "Bottom" : + /^e$/.test(i) ? "Right" : "Left"].join(""); + + target.css(padPos, padWrapper); + + this._proportionallyResize(); + + } + + //TODO: What's that good for? There's not anything to be executed left + if (!$(this.handles[i]).length) { + continue; + } + } + }; + + //TODO: make renderAxis a prototype function + this._renderAxis(this.element); + + this._handles = $(".ui-resizable-handle", this.element) + .disableSelection(); + + //Matching axis name + this._handles.mouseover(function () { + if (!that.resizing) { + if (this.className) { + axis = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i); + } + //Axis, default = se + that.axis = axis && axis[1] ? axis[1] : "se"; + } + }); + + //If we want to auto hide the elements + if (o.autoHide) { + this._handles.hide(); + $(this.element) + .addClass("ui-resizable-autohide") + .mouseenter(function () { + if (o.disabled) { + return; + } + $(this).removeClass("ui-resizable-autohide"); + that._handles.show(); + }) + .mouseleave(function () { + if (o.disabled) { + return; + } + if (!that.resizing) { + $(this).addClass("ui-resizable-autohide"); + that._handles.hide(); + } + }); + } + + //Initialize the mouse interaction + this._mouseInit(); + + }, + + _destroy: function () { + + this._mouseDestroy(); + + var wrapper, + _destroy = function (exp) { + $(exp).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing") + .removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove(); + }; + + //TODO: Unwrap at same DOM position + if (this.elementIsWrapper) { + _destroy(this.element); + wrapper = this.element; + this.originalElement.css({ + position: wrapper.css("position"), + width: wrapper.outerWidth(), + height: wrapper.outerHeight(), + top: wrapper.css("top"), + left: wrapper.css("left") + }).insertAfter(wrapper); + wrapper.remove(); + } + + this.originalElement.css("resize", this.originalResizeStyle); + _destroy(this.originalElement); + + return this; + }, + + _mouseCapture: function (event) { + var i, handle, + capture = false; + + for (i in this.handles) { + handle = $(this.handles[i])[0]; + if (handle === event.target || $.contains(handle, event.target)) { + capture = true; + } + } + + return !this.options.disabled && capture; + }, + + _mouseStart: function (event) { + + var curleft, curtop, cursor, + o = this.options, + iniPos = this.element.position(), + el = this.element; + + this.resizing = true; + + // bugfix for http://dev.jquery.com/ticket/1749 + if ((/absolute/).test(el.css("position"))) { + el.css({position: "absolute", top: el.css("top"), left: el.css("left")}); + } else if (el.is(".ui-draggable")) { + el.css({position: "absolute", top: iniPos.top, left: iniPos.left}); + } + + this._renderProxy(); + + curleft = num(this.helper.css("left")); + curtop = num(this.helper.css("top")); + + if (o.containment) { + curleft += $(o.containment).scrollLeft() || 0; + curtop += $(o.containment).scrollTop() || 0; + } + + //Store needed variables + this.offset = this.helper.offset(); + this.position = {left: curleft, top: curtop}; + this.size = this._helper ? {width: this.helper.width(), height: this.helper.height()} : { + width: el.width(), + height: el.height() + }; + this.originalSize = this._helper ? {width: el.outerWidth(), height: el.outerHeight()} : { + width: el.width(), + height: el.height() + }; + this.originalPosition = {left: curleft, top: curtop}; + this.sizeDiff = {width: el.outerWidth() - el.width(), height: el.outerHeight() - el.height()}; + this.originalMousePosition = {left: event.pageX, top: event.pageY}; + + //Aspect Ratio + this.aspectRatio = (typeof o.aspectRatio === "number") ? o.aspectRatio : ((this.originalSize.width / this.originalSize.height) || 1); + + cursor = $(".ui-resizable-" + this.axis).css("cursor"); + $("body").css("cursor", cursor === "auto" ? this.axis + "-resize" : cursor); + + el.addClass("ui-resizable-resizing"); + this._propagate("start", event); + return true; + }, + + _mouseDrag: function (event) { + + //Increase performance, avoid regex + var data, + el = this.helper, props = {}, + smp = this.originalMousePosition, + a = this.axis, + prevTop = this.position.top, + prevLeft = this.position.left, + prevWidth = this.size.width, + prevHeight = this.size.height, + dx = (event.pageX - smp.left) || 0, + dy = (event.pageY - smp.top) || 0, + trigger = this._change[a]; + + if (!trigger) { + return false; + } + + // Calculate the attrs that will be change + data = trigger.apply(this, [event, dx, dy]); + + // Put this in the mouseDrag handler since the user can start pressing shift while resizing + this._updateVirtualBoundaries(event.shiftKey); + if (this._aspectRatio || event.shiftKey) { + data = this._updateRatio(data, event); + } + + data = this._respectSize(data, event); + + this._updateCache(data); + + // plugins callbacks need to be called first + this._propagate("resize", event); + + if (this.position.top !== prevTop) { + props.top = this.position.top + "px"; + } + if (this.position.left !== prevLeft) { + props.left = this.position.left + "px"; + } + if (this.size.width !== prevWidth) { + props.width = this.size.width + "px"; + } + if (this.size.height !== prevHeight) { + props.height = this.size.height + "px"; + } + el.css(props); + + if (!this._helper && this._proportionallyResizeElements.length) { + this._proportionallyResize(); + } + + // Call the user callback if the element was resized + if (!$.isEmptyObject(props)) { + this._trigger("resize", event, this.ui()); + } + + return false; + }, + + _mouseStop: function (event) { + + this.resizing = false; + var pr, ista, soffseth, soffsetw, s, left, top, + o = this.options, that = this; + + if (this._helper) { + + pr = this._proportionallyResizeElements; + ista = pr.length && (/textarea/i).test(pr[0].nodeName); + soffseth = ista && $.ui.hasScroll(pr[0], "left") /* TODO - jump height */ ? 0 : that.sizeDiff.height; + soffsetw = ista ? 0 : that.sizeDiff.width; + + s = {width: (that.helper.width() - soffsetw), height: (that.helper.height() - soffseth)}; + left = (parseInt(that.element.css("left"), 10) + (that.position.left - that.originalPosition.left)) || null; + top = (parseInt(that.element.css("top"), 10) + (that.position.top - that.originalPosition.top)) || null; + + if (!o.animate) { + this.element.css($.extend(s, {top: top, left: left})); + } + + that.helper.height(that.size.height); + that.helper.width(that.size.width); + + if (this._helper && !o.animate) { + this._proportionallyResize(); + } + } + + $("body").css("cursor", "auto"); + + this.element.removeClass("ui-resizable-resizing"); + + this._propagate("stop", event); + + if (this._helper) { + this.helper.remove(); + } + + return false; + + }, + + _updateVirtualBoundaries: function (forceAspectRatio) { + var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b, + o = this.options; + + b = { + minWidth: isNumber(o.minWidth) ? o.minWidth : 0, + maxWidth: isNumber(o.maxWidth) ? o.maxWidth : Infinity, + minHeight: isNumber(o.minHeight) ? o.minHeight : 0, + maxHeight: isNumber(o.maxHeight) ? o.maxHeight : Infinity + }; + + if (this._aspectRatio || forceAspectRatio) { + // We want to create an enclosing box whose aspect ration is the requested one + // First, compute the "projected" size for each dimension based on the aspect ratio and other dimension + pMinWidth = b.minHeight * this.aspectRatio; + pMinHeight = b.minWidth / this.aspectRatio; + pMaxWidth = b.maxHeight * this.aspectRatio; + pMaxHeight = b.maxWidth / this.aspectRatio; + + if (pMinWidth > b.minWidth) { + b.minWidth = pMinWidth; + } + if (pMinHeight > b.minHeight) { + b.minHeight = pMinHeight; + } + if (pMaxWidth < b.maxWidth) { + b.maxWidth = pMaxWidth; + } + if (pMaxHeight < b.maxHeight) { + b.maxHeight = pMaxHeight; + } + } + this._vBoundaries = b; + }, + + _updateCache: function (data) { + this.offset = this.helper.offset(); + if (isNumber(data.left)) { + this.position.left = data.left; + } + if (isNumber(data.top)) { + this.position.top = data.top; + } + if (isNumber(data.height)) { + this.size.height = data.height; + } + if (isNumber(data.width)) { + this.size.width = data.width; + } + }, + + _updateRatio: function (data) { + + var cpos = this.position, + csize = this.size, + a = this.axis; + + if (isNumber(data.height)) { + data.width = (data.height * this.aspectRatio); + } else if (isNumber(data.width)) { + data.height = (data.width / this.aspectRatio); + } + + if (a === "sw") { + data.left = cpos.left + (csize.width - data.width); + data.top = null; + } + if (a === "nw") { + data.top = cpos.top + (csize.height - data.height); + data.left = cpos.left + (csize.width - data.width); + } + + return data; + }, + + _respectSize: function (data) { + + var o = this._vBoundaries, + a = this.axis, + ismaxw = isNumber(data.width) && o.maxWidth && (o.maxWidth < data.width), + ismaxh = isNumber(data.height) && o.maxHeight && (o.maxHeight < data.height), + isminw = isNumber(data.width) && o.minWidth && (o.minWidth > data.width), + isminh = isNumber(data.height) && o.minHeight && (o.minHeight > data.height), + dw = this.originalPosition.left + this.originalSize.width, + dh = this.position.top + this.size.height, + cw = /sw|nw|w/.test(a), ch = /nw|ne|n/.test(a); + if (isminw) { + data.width = o.minWidth; + } + if (isminh) { + data.height = o.minHeight; + } + if (ismaxw) { + data.width = o.maxWidth; + } + if (ismaxh) { + data.height = o.maxHeight; + } + + if (isminw && cw) { + data.left = dw - o.minWidth; + } + if (ismaxw && cw) { + data.left = dw - o.maxWidth; + } + if (isminh && ch) { + data.top = dh - o.minHeight; + } + if (ismaxh && ch) { + data.top = dh - o.maxHeight; + } + + // fixing jump error on top/left - bug #2330 + if (!data.width && !data.height && !data.left && data.top) { + data.top = null; + } else if (!data.width && !data.height && !data.top && data.left) { + data.left = null; + } + + return data; + }, + + _proportionallyResize: function () { + + if (!this._proportionallyResizeElements.length) { + return; + } + + var i, j, borders, paddings, prel, + element = this.helper || this.element; + + for (i = 0; i < this._proportionallyResizeElements.length; i++) { + + prel = this._proportionallyResizeElements[i]; + + if (!this.borderDif) { + this.borderDif = []; + borders = [prel.css("borderTopWidth"), prel.css("borderRightWidth"), prel.css("borderBottomWidth"), prel.css("borderLeftWidth")]; + paddings = [prel.css("paddingTop"), prel.css("paddingRight"), prel.css("paddingBottom"), prel.css("paddingLeft")]; + + for (j = 0; j < borders.length; j++) { + this.borderDif[j] = (parseInt(borders[j], 10) || 0) + (parseInt(paddings[j], 10) || 0); + } + } + + prel.css({ + height: (element.height() - this.borderDif[0] - this.borderDif[2]) || 0, + width: (element.width() - this.borderDif[1] - this.borderDif[3]) || 0 + }); + + } + + }, + + _renderProxy: function () { + + var el = this.element, o = this.options; + this.elementOffset = el.offset(); + + if (this._helper) { + + this.helper = this.helper || $("<div style='overflow:hidden;'></div>"); + + this.helper.addClass(this._helper).css({ + width: this.element.outerWidth() - 1, + height: this.element.outerHeight() - 1, + position: "absolute", + left: this.elementOffset.left + "px", + top: this.elementOffset.top + "px", + zIndex: ++o.zIndex //TODO: Don't modify option + }); + + this.helper + .appendTo("body") + .disableSelection(); + + } else { + this.helper = this.element; + } + + }, + + _change: { + e: function (event, dx) { + return {width: this.originalSize.width + dx}; + }, + w: function (event, dx) { + var cs = this.originalSize, sp = this.originalPosition; + return {left: sp.left + dx, width: cs.width - dx}; + }, + n: function (event, dx, dy) { + var cs = this.originalSize, sp = this.originalPosition; + return {top: sp.top + dy, height: cs.height - dy}; + }, + s: function (event, dx, dy) { + return {height: this.originalSize.height + dy}; + }, + se: function (event, dx, dy) { + return $.extend(this._change.s.apply(this, arguments), this._change.e.apply(this, [event, dx, dy])); + }, + sw: function (event, dx, dy) { + return $.extend(this._change.s.apply(this, arguments), this._change.w.apply(this, [event, dx, dy])); + }, + ne: function (event, dx, dy) { + return $.extend(this._change.n.apply(this, arguments), this._change.e.apply(this, [event, dx, dy])); + }, + nw: function (event, dx, dy) { + return $.extend(this._change.n.apply(this, arguments), this._change.w.apply(this, [event, dx, dy])); + } + }, + + _propagate: function (n, event) { + $.ui.plugin.call(this, n, [event, this.ui()]); + (n !== "resize" && this._trigger(n, event, this.ui())); + }, + + plugins: {}, + + ui: function () { + return { + originalElement: this.originalElement, + element: this.element, + helper: this.helper, + position: this.position, + size: this.size, + originalSize: this.originalSize, + originalPosition: this.originalPosition + }; + } + + }); + + /* + * Resizable Extensions + */ + + $.ui.plugin.add("resizable", "animate", { + + stop: function (event) { + var that = $(this).data("ui-resizable"), + o = that.options, + pr = that._proportionallyResizeElements, + ista = pr.length && (/textarea/i).test(pr[0].nodeName), + soffseth = ista && $.ui.hasScroll(pr[0], "left") /* TODO - jump height */ ? 0 : that.sizeDiff.height, + soffsetw = ista ? 0 : that.sizeDiff.width, + style = {width: (that.size.width - soffsetw), height: (that.size.height - soffseth)}, + left = (parseInt(that.element.css("left"), 10) + (that.position.left - that.originalPosition.left)) || null, + top = (parseInt(that.element.css("top"), 10) + (that.position.top - that.originalPosition.top)) || null; + + that.element.animate( + $.extend(style, top && left ? {top: top, left: left} : {}), { + duration: o.animateDuration, + easing: o.animateEasing, + step: function () { + + var data = { + width: parseInt(that.element.css("width"), 10), + height: parseInt(that.element.css("height"), 10), + top: parseInt(that.element.css("top"), 10), + left: parseInt(that.element.css("left"), 10) + }; + + if (pr && pr.length) { + $(pr[0]).css({width: data.width, height: data.height}); + } + + // propagating resize, and updating values for each animation step + that._updateCache(data); + that._propagate("resize", event); + + } + } + ); + } + + }); + + $.ui.plugin.add("resizable", "containment", { + + start: function () { + var element, p, co, ch, cw, width, height, + that = $(this).data("ui-resizable"), + o = that.options, + el = that.element, + oc = o.containment, + ce = (oc instanceof $) ? oc.get(0) : (/parent/.test(oc)) ? el.parent().get(0) : oc; + + if (!ce) { + return; + } + + that.containerElement = $(ce); + + if (/document/.test(oc) || oc === document) { + that.containerOffset = {left: 0, top: 0}; + that.containerPosition = {left: 0, top: 0}; + + that.parentData = { + element: $(document), left: 0, top: 0, + width: $(document).width(), height: $(document).height() || document.body.parentNode.scrollHeight + }; + } + + // i'm a node, so compute top, left, right, bottom + else { + element = $(ce); + p = []; + $(["Top", "Right", "Left", "Bottom"]).each(function (i, name) { + p[i] = num(element.css("padding" + name)); + }); + + that.containerOffset = element.offset(); + that.containerPosition = element.position(); + that.containerSize = {height: (element.innerHeight() - p[3]), width: (element.innerWidth() - p[1])}; + + co = that.containerOffset; + ch = that.containerSize.height; + cw = that.containerSize.width; + width = ($.ui.hasScroll(ce, "left") ? ce.scrollWidth : cw); + height = ($.ui.hasScroll(ce) ? ce.scrollHeight : ch); + + that.parentData = { + element: ce, left: co.left, top: co.top, width: width, height: height + }; + } + }, + + resize: function (event) { + var woset, hoset, isParent, isOffsetRelative, + that = $(this).data("ui-resizable"), + o = that.options, + co = that.containerOffset, cp = that.position, + pRatio = that._aspectRatio || event.shiftKey, + cop = {top: 0, left: 0}, ce = that.containerElement; + + if (ce[0] !== document && (/static/).test(ce.css("position"))) { + cop = co; + } + + if (cp.left < (that._helper ? co.left : 0)) { + that.size.width = that.size.width + (that._helper ? (that.position.left - co.left) : (that.position.left - cop.left)); + if (pRatio) { + that.size.height = that.size.width / that.aspectRatio; + } + that.position.left = o.helper ? co.left : 0; + } + + if (cp.top < (that._helper ? co.top : 0)) { + that.size.height = that.size.height + (that._helper ? (that.position.top - co.top) : that.position.top); + if (pRatio) { + that.size.width = that.size.height * that.aspectRatio; + } + that.position.top = that._helper ? co.top : 0; + } + + that.offset.left = that.parentData.left + that.position.left; + that.offset.top = that.parentData.top + that.position.top; + + woset = Math.abs((that._helper ? that.offset.left - cop.left : (that.offset.left - cop.left)) + that.sizeDiff.width); + hoset = Math.abs((that._helper ? that.offset.top - cop.top : (that.offset.top - co.top)) + that.sizeDiff.height); + + isParent = that.containerElement.get(0) === that.element.parent().get(0); + isOffsetRelative = /relative|absolute/.test(that.containerElement.css("position")); + + if (isParent && isOffsetRelative) { + woset -= Math.abs(that.parentData.left); + } + + if (woset + that.size.width >= that.parentData.width) { + that.size.width = that.parentData.width - woset; + if (pRatio) { + that.size.height = that.size.width / that.aspectRatio; + } + } + + if (hoset + that.size.height >= that.parentData.height) { + that.size.height = that.parentData.height - hoset; + if (pRatio) { + that.size.width = that.size.height * that.aspectRatio; + } + } + }, + + stop: function () { + var that = $(this).data("ui-resizable"), + o = that.options, + co = that.containerOffset, + cop = that.containerPosition, + ce = that.containerElement, + helper = $(that.helper), + ho = helper.offset(), + w = helper.outerWidth() - that.sizeDiff.width, + h = helper.outerHeight() - that.sizeDiff.height; + + if (that._helper && !o.animate && (/relative/).test(ce.css("position"))) { + $(this).css({left: ho.left - cop.left - co.left, width: w, height: h}); + } + + if (that._helper && !o.animate && (/static/).test(ce.css("position"))) { + $(this).css({left: ho.left - cop.left - co.left, width: w, height: h}); + } + + } + }); + + $.ui.plugin.add("resizable", "alsoResize", { + + start: function () { + var that = $(this).data("ui-resizable"), + o = that.options, + _store = function (exp) { + $(exp).each(function () { + var el = $(this); + el.data("ui-resizable-alsoresize", { + width: parseInt(el.width(), 10), height: parseInt(el.height(), 10), + left: parseInt(el.css("left"), 10), top: parseInt(el.css("top"), 10) + }); + }); + }; + + if (typeof (o.alsoResize) === "object" && !o.alsoResize.parentNode) { + if (o.alsoResize.length) { + o.alsoResize = o.alsoResize[0]; + _store(o.alsoResize); + } else { + $.each(o.alsoResize, function (exp) { + _store(exp); + }); + } + } else { + _store(o.alsoResize); + } + }, + + resize: function (event, ui) { + var that = $(this).data("ui-resizable"), + o = that.options, + os = that.originalSize, + op = that.originalPosition, + delta = { + height: (that.size.height - os.height) || 0, width: (that.size.width - os.width) || 0, + top: (that.position.top - op.top) || 0, left: (that.position.left - op.left) || 0 + }, + + _alsoResize = function (exp, c) { + $(exp).each(function () { + var el = $(this), start = $(this).data("ui-resizable-alsoresize"), style = {}, + css = c && c.length ? c : el.parents(ui.originalElement[0]).length ? ["width", "height"] : ["width", "height", "top", "left"]; + + $.each(css, function (i, prop) { + var sum = (start[prop] || 0) + (delta[prop] || 0); + if (sum && sum >= 0) { + style[prop] = sum || null; + } + }); + + el.css(style); + }); + }; + + if (typeof (o.alsoResize) === "object" && !o.alsoResize.nodeType) { + $.each(o.alsoResize, function (exp, c) { + _alsoResize(exp, c); + }); + } else { + _alsoResize(o.alsoResize); + } + }, + + stop: function () { + $(this).removeData("resizable-alsoresize"); + } + }); + + $.ui.plugin.add("resizable", "ghost", { + + start: function () { + + var that = $(this).data("ui-resizable"), o = that.options, cs = that.size; + + that.ghost = that.originalElement.clone(); + that.ghost + .css({ + opacity: 0.25, + display: "block", + position: "relative", + height: cs.height, + width: cs.width, + margin: 0, + left: 0, + top: 0 + }) + .addClass("ui-resizable-ghost") + .addClass(typeof o.ghost === "string" ? o.ghost : ""); + + that.ghost.appendTo(that.helper); + + }, + + resize: function () { + var that = $(this).data("ui-resizable"); + if (that.ghost) { + that.ghost.css({position: "relative", height: that.size.height, width: that.size.width}); + } + }, + + stop: function () { + var that = $(this).data("ui-resizable"); + if (that.ghost && that.helper) { + that.helper.get(0).removeChild(that.ghost.get(0)); + } + } + + }); + + $.ui.plugin.add("resizable", "grid", { + + resize: function () { + var that = $(this).data("ui-resizable"), + o = that.options, + cs = that.size, + os = that.originalSize, + op = that.originalPosition, + a = that.axis, + grid = typeof o.grid === "number" ? [o.grid, o.grid] : o.grid, + gridX = (grid[0] || 1), + gridY = (grid[1] || 1), + ox = Math.round((cs.width - os.width) / gridX) * gridX, + oy = Math.round((cs.height - os.height) / gridY) * gridY, + newWidth = os.width + ox, + newHeight = os.height + oy, + isMaxWidth = o.maxWidth && (o.maxWidth < newWidth), + isMaxHeight = o.maxHeight && (o.maxHeight < newHeight), + isMinWidth = o.minWidth && (o.minWidth > newWidth), + isMinHeight = o.minHeight && (o.minHeight > newHeight); + + o.grid = grid; + + if (isMinWidth) { + newWidth = newWidth + gridX; + } + if (isMinHeight) { + newHeight = newHeight + gridY; + } + if (isMaxWidth) { + newWidth = newWidth - gridX; + } + if (isMaxHeight) { + newHeight = newHeight - gridY; + } + + if (/^(se|s|e)$/.test(a)) { + that.size.width = newWidth; + that.size.height = newHeight; + } else if (/^(ne)$/.test(a)) { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.top = op.top - oy; + } else if (/^(sw)$/.test(a)) { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.left = op.left - ox; + } else { + if (newHeight - gridY > 0) { + that.size.height = newHeight; + that.position.top = op.top - oy; + } else { + that.size.height = gridY; + that.position.top = op.top + os.height - gridY; + } + if (newWidth - gridX > 0) { + that.size.width = newWidth; + that.position.left = op.left - ox; + } else { + that.size.width = gridX; + that.position.left = op.left + os.width - gridX; + } + } + } + + }); + +}); diff --git a/lib/web/jquery/ui-modules/selectable.js b/lib/web/jquery/ui-modules/selectable.js new file mode 100644 index 000000000000..e63d3848bb13 --- /dev/null +++ b/lib/web/jquery/ui-modules/selectable.js @@ -0,0 +1,285 @@ +/*! + * jQuery UI Selectable - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/selectable/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/mouse' +], function ($, undefined) { + + $.widget("ui.selectable", $.ui.mouse, { + version: "1.10.4", + options: { + appendTo: "body", + autoRefresh: true, + distance: 0, + filter: "*", + tolerance: "touch", + + // callbacks + selected: null, + selecting: null, + start: null, + stop: null, + unselected: null, + unselecting: null + }, + _create: function () { + var selectees, + that = this; + + this.element.addClass("ui-selectable"); + + this.dragged = false; + + // cache selectee children based on filter + this.refresh = function () { + selectees = $(that.options.filter, that.element[0]); + selectees.addClass("ui-selectee"); + selectees.each(function () { + var $this = $(this), + pos = $this.offset(); + $.data(this, "selectable-item", { + element: this, + $element: $this, + left: pos.left, + top: pos.top, + right: pos.left + $this.outerWidth(), + bottom: pos.top + $this.outerHeight(), + startselected: false, + selected: $this.hasClass("ui-selected"), + selecting: $this.hasClass("ui-selecting"), + unselecting: $this.hasClass("ui-unselecting") + }); + }); + }; + this.refresh(); + + this.selectees = selectees.addClass("ui-selectee"); + + this._mouseInit(); + + this.helper = $("<div class='ui-selectable-helper'></div>"); + }, + + _destroy: function () { + this.selectees + .removeClass("ui-selectee") + .removeData("selectable-item"); + this.element + .removeClass("ui-selectable ui-selectable-disabled"); + this._mouseDestroy(); + }, + + _mouseStart: function (event) { + var that = this, + options = this.options; + + this.opos = [event.pageX, event.pageY]; + + if (this.options.disabled) { + return; + } + + this.selectees = $(options.filter, this.element[0]); + + this._trigger("start", event); + + $(options.appendTo).append(this.helper); + // position helper (lasso) + this.helper.css({ + "left": event.pageX, + "top": event.pageY, + "width": 0, + "height": 0 + }); + + if (options.autoRefresh) { + this.refresh(); + } + + this.selectees.filter(".ui-selected").each(function () { + var selectee = $.data(this, "selectable-item"); + selectee.startselected = true; + if (!event.metaKey && !event.ctrlKey) { + selectee.$element.removeClass("ui-selected"); + selectee.selected = false; + selectee.$element.addClass("ui-unselecting"); + selectee.unselecting = true; + // selectable UNSELECTING callback + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + }); + + $(event.target).parents().addBack().each(function () { + var doSelect, + selectee = $.data(this, "selectable-item"); + if (selectee) { + doSelect = (!event.metaKey && !event.ctrlKey) || !selectee.$element.hasClass("ui-selected"); + selectee.$element + .removeClass(doSelect ? "ui-unselecting" : "ui-selected") + .addClass(doSelect ? "ui-selecting" : "ui-unselecting"); + selectee.unselecting = !doSelect; + selectee.selecting = doSelect; + selectee.selected = doSelect; + // selectable (UN)SELECTING callback + if (doSelect) { + that._trigger("selecting", event, { + selecting: selectee.element + }); + } else { + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + return false; + } + }); + + }, + + _mouseDrag: function (event) { + + this.dragged = true; + + if (this.options.disabled) { + return; + } + + var tmp, + that = this, + options = this.options, + x1 = this.opos[0], + y1 = this.opos[1], + x2 = event.pageX, + y2 = event.pageY; + + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + this.helper.css({left: x1, top: y1, width: x2 - x1, height: y2 - y1}); + + this.selectees.each(function () { + var selectee = $.data(this, "selectable-item"), + hit = false; + + //prevent helper from being selected if appendTo: selectable + if (!selectee || selectee.element === that.element[0]) { + return; + } + + if (options.tolerance === "touch") { + hit = (!(selectee.left > x2 || selectee.right < x1 || selectee.top > y2 || selectee.bottom < y1)); + } else if (options.tolerance === "fit") { + hit = (selectee.left > x1 && selectee.right < x2 && selectee.top > y1 && selectee.bottom < y2); + } + + if (hit) { + // SELECT + if (selectee.selected) { + selectee.$element.removeClass("ui-selected"); + selectee.selected = false; + } + if (selectee.unselecting) { + selectee.$element.removeClass("ui-unselecting"); + selectee.unselecting = false; + } + if (!selectee.selecting) { + selectee.$element.addClass("ui-selecting"); + selectee.selecting = true; + // selectable SELECTING callback + that._trigger("selecting", event, { + selecting: selectee.element + }); + } + } else { + // UNSELECT + if (selectee.selecting) { + if ((event.metaKey || event.ctrlKey) && selectee.startselected) { + selectee.$element.removeClass("ui-selecting"); + selectee.selecting = false; + selectee.$element.addClass("ui-selected"); + selectee.selected = true; + } else { + selectee.$element.removeClass("ui-selecting"); + selectee.selecting = false; + if (selectee.startselected) { + selectee.$element.addClass("ui-unselecting"); + selectee.unselecting = true; + } + // selectable UNSELECTING callback + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + } + if (selectee.selected) { + if (!event.metaKey && !event.ctrlKey && !selectee.startselected) { + selectee.$element.removeClass("ui-selected"); + selectee.selected = false; + + selectee.$element.addClass("ui-unselecting"); + selectee.unselecting = true; + // selectable UNSELECTING callback + that._trigger("unselecting", event, { + unselecting: selectee.element + }); + } + } + } + }); + + return false; + }, + + _mouseStop: function (event) { + var that = this; + + this.dragged = false; + + $(".ui-unselecting", this.element[0]).each(function () { + var selectee = $.data(this, "selectable-item"); + selectee.$element.removeClass("ui-unselecting"); + selectee.unselecting = false; + selectee.startselected = false; + that._trigger("unselected", event, { + unselected: selectee.element + }); + }); + $(".ui-selecting", this.element[0]).each(function () { + var selectee = $.data(this, "selectable-item"); + selectee.$element.removeClass("ui-selecting").addClass("ui-selected"); + selectee.selecting = false; + selectee.selected = true; + selectee.startselected = true; + that._trigger("selected", event, { + selected: selectee.element + }); + }); + this._trigger("stop", event); + + this.helper.remove(); + + return false; + } + + }); + +}); diff --git a/lib/web/jquery/ui-modules/slider.js b/lib/web/jquery/ui-modules/slider.js new file mode 100644 index 000000000000..74dad5902379 --- /dev/null +++ b/lib/web/jquery/ui-modules/slider.js @@ -0,0 +1,688 @@ +/*! + * jQuery UI Slider - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/slider/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/mouse' +], function ($, undefined) { + +// number of pages in a slider +// (how many times can you page up/down to go through the whole range) + var numPages = 5; + + $.widget("ui.slider", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "slide", + + options: { + animate: false, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null, + + // callbacks + change: null, + slide: null, + start: null, + stop: null + }, + + _create: function () { + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + + this.element + .addClass("ui-slider" + + " ui-slider-" + this.orientation + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all"); + + this._refresh(); + this._setOption("disabled", this.options.disabled); + + this._animateOff = false; + }, + + _refresh: function () { + this._createRange(); + this._createHandles(); + this._setupEvents(); + this._refreshValue(); + }, + + _createHandles: function () { + var i, handleCount, + options = this.options, + existingHandles = this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"), + handle = "<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>", + handles = []; + + handleCount = (options.values && options.values.length) || 1; + + if (existingHandles.length > handleCount) { + existingHandles.slice(handleCount).remove(); + existingHandles = existingHandles.slice(0, handleCount); + } + + for (i = existingHandles.length; i < handleCount; i++) { + handles.push(handle); + } + + this.handles = existingHandles.add($(handles.join("")).appendTo(this.element)); + + this.handle = this.handles.eq(0); + + this.handles.each(function (i) { + $(this).data("ui-slider-handle-index", i); + }); + }, + + _createRange: function () { + var options = this.options, + classes = ""; + + if (options.range) { + if (options.range === true) { + if (!options.values) { + options.values = [this._valueMin(), this._valueMin()]; + } else if (options.values.length && options.values.length !== 2) { + options.values = [options.values[0], options.values[0]]; + } else if ($.isArray(options.values)) { + options.values = options.values.slice(0); + } + } + + if (!this.range || !this.range.length) { + this.range = $("<div></div>") + .appendTo(this.element); + + classes = "ui-slider-range" + + // note: this isn't the most fittingly semantic framework class for this element, + // but worked best visually with a variety of themes + " ui-widget-header ui-corner-all"; + } else { + this.range.removeClass("ui-slider-range-min ui-slider-range-max") + // Handle range switching from true to min/max + .css({ + "left": "", + "bottom": "" + }); + } + + this.range.addClass(classes + + ((options.range === "min" || options.range === "max") ? " ui-slider-range-" + options.range : "")); + } else { + if (this.range) { + this.range.remove(); + } + this.range = null; + } + }, + + _setupEvents: function () { + var elements = this.handles.add(this.range).filter("a"); + this._off(elements); + this._on(elements, this._handleEvents); + this._hoverable(elements); + this._focusable(elements); + }, + + _destroy: function () { + this.handles.remove(); + if (this.range) { + this.range.remove(); + } + + this.element + .removeClass("ui-slider" + + " ui-slider-horizontal" + + " ui-slider-vertical" + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all"); + + this._mouseDestroy(); + }, + + _mouseCapture: function (event) { + var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, + that = this, + o = this.options; + + if (o.disabled) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = {x: event.pageX, y: event.pageY}; + normValue = this._normValueFromMouse(position); + distance = this._valueMax() - this._valueMin() + 1; + this.handles.each(function (i) { + var thisDistance = Math.abs(normValue - that.values(i)); + if ((distance > thisDistance) || + (distance === thisDistance && + (i === that._lastChangedValue || that.values(i) === o.min))) { + distance = thisDistance; + closestHandle = $(this); + index = i; + } + }); + + allowed = this._start(event, index); + if (allowed === false) { + return false; + } + this._mouseSliding = true; + + this._handleIndex = index; + + closestHandle + .addClass("ui-state-active") + .focus(); + + offset = closestHandle.offset(); + mouseOverHandle = !$(event.target).parents().addBack().is(".ui-slider-handle"); + this._clickOffset = mouseOverHandle ? {left: 0, top: 0} : { + left: event.pageX - offset.left - (closestHandle.width() / 2), + top: event.pageY - offset.top - + (closestHandle.height() / 2) - + (parseInt(closestHandle.css("borderTopWidth"), 10) || 0) - + (parseInt(closestHandle.css("borderBottomWidth"), 10) || 0) + + (parseInt(closestHandle.css("marginTop"), 10) || 0) + }; + + if (!this.handles.hasClass("ui-state-hover")) { + this._slide(event, index, normValue); + } + this._animateOff = true; + return true; + }, + + _mouseStart: function () { + return true; + }, + + _mouseDrag: function (event) { + var position = {x: event.pageX, y: event.pageY}, + normValue = this._normValueFromMouse(position); + + this._slide(event, this._handleIndex, normValue); + + return false; + }, + + _mouseStop: function (event) { + this.handles.removeClass("ui-state-active"); + this._mouseSliding = false; + + this._stop(event, this._handleIndex); + this._change(event, this._handleIndex); + + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; + + return false; + }, + + _detectOrientation: function () { + this.orientation = (this.options.orientation === "vertical") ? "vertical" : "horizontal"; + }, + + _normValueFromMouse: function (position) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if (this.orientation === "horizontal") { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - (this._clickOffset ? this._clickOffset.left : 0); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - (this._clickOffset ? this._clickOffset.top : 0); + } + + percentMouse = (pixelMouse / pixelTotal); + if (percentMouse > 1) { + percentMouse = 1; + } + if (percentMouse < 0) { + percentMouse = 0; + } + if (this.orientation === "vertical") { + percentMouse = 1 - percentMouse; + } + + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; + + return this._trimAlignValue(valueMouse); + }, + + _start: function (event, index) { + var uiHash = { + handle: this.handles[index], + value: this.value() + }; + if (this.options.values && this.options.values.length) { + uiHash.value = this.values(index); + uiHash.values = this.values(); + } + return this._trigger("start", event, uiHash); + }, + + _slide: function (event, index, newVal) { + var otherVal, + newValues, + allowed; + + if (this.options.values && this.options.values.length) { + otherVal = this.values(index ? 0 : 1); + + if ((this.options.values.length === 2 && this.options.range === true) && + ((index === 0 && newVal > otherVal) || (index === 1 && newVal < otherVal)) + ) { + newVal = otherVal; + } + + if (newVal !== this.values(index)) { + newValues = this.values(); + newValues[index] = newVal; + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger("slide", event, { + handle: this.handles[index], + value: newVal, + values: newValues + }); + otherVal = this.values(index ? 0 : 1); + if (allowed !== false) { + this.values(index, newVal); + } + } + } else { + if (newVal !== this.value()) { + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger("slide", event, { + handle: this.handles[index], + value: newVal + }); + if (allowed !== false) { + this.value(newVal); + } + } + } + }, + + _stop: function (event, index) { + var uiHash = { + handle: this.handles[index], + value: this.value() + }; + if (this.options.values && this.options.values.length) { + uiHash.value = this.values(index); + uiHash.values = this.values(); + } + + this._trigger("stop", event, uiHash); + }, + + _change: function (event, index) { + if (!this._keySliding && !this._mouseSliding) { + var uiHash = { + handle: this.handles[index], + value: this.value() + }; + if (this.options.values && this.options.values.length) { + uiHash.value = this.values(index); + uiHash.values = this.values(); + } + + //store the last changed value index for reference when handles overlap + this._lastChangedValue = index; + + this._trigger("change", event, uiHash); + } + }, + + value: function (newValue) { + if (arguments.length) { + this.options.value = this._trimAlignValue(newValue); + this._refreshValue(); + this._change(null, 0); + return; + } + + return this._value(); + }, + + values: function (index, newValue) { + var vals, + newValues, + i; + + if (arguments.length > 1) { + this.options.values[index] = this._trimAlignValue(newValue); + this._refreshValue(); + this._change(null, index); + return; + } + + if (arguments.length) { + if ($.isArray(arguments[0])) { + vals = this.options.values; + newValues = arguments[0]; + for (i = 0; i < vals.length; i += 1) { + vals[i] = this._trimAlignValue(newValues[i]); + this._change(null, i); + } + this._refreshValue(); + } else { + if (this.options.values && this.options.values.length) { + return this._values(index); + } else { + return this.value(); + } + } + } else { + return this._values(); + } + }, + + _setOption: function (key, value) { + var i, + valsLength = 0; + + if (key === "range" && this.options.range === true) { + if (value === "min") { + this.options.value = this._values(0); + this.options.values = null; + } else if (value === "max") { + this.options.value = this._values(this.options.values.length - 1); + this.options.values = null; + } + } + + if ($.isArray(this.options.values)) { + valsLength = this.options.values.length; + } + + $.Widget.prototype._setOption.apply(this, arguments); + + switch (key) { + case "orientation": + this._detectOrientation(); + this.element + .removeClass("ui-slider-horizontal ui-slider-vertical") + .addClass("ui-slider-" + this.orientation); + this._refreshValue(); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change(null, 0); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + for (i = 0; i < valsLength; i += 1) { + this._change(null, i); + } + this._animateOff = false; + break; + case "min": + case "max": + this._animateOff = true; + this._refreshValue(); + this._animateOff = false; + break; + case "range": + this._animateOff = true; + this._refresh(); + this._animateOff = false; + break; + } + }, + + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function () { + var val = this.options.value; + val = this._trimAlignValue(val); + + return val; + }, + + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function (index) { + var val, + vals, + i; + + if (arguments.length) { + val = this.options.values[index]; + val = this._trimAlignValue(val); + + return val; + } else if (this.options.values && this.options.values.length) { + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for (i = 0; i < vals.length; i += 1) { + vals[i] = this._trimAlignValue(vals[i]); + } + + return vals; + } else { + return []; + } + }, + + // returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function (val) { + if (val <= this._valueMin()) { + return this._valueMin(); + } + if (val >= this._valueMax()) { + return this._valueMax(); + } + var step = (this.options.step > 0) ? this.options.step : 1, + valModStep = (val - this._valueMin()) % step, + alignValue = val - valModStep; + + if (Math.abs(valModStep) * 2 >= step) { + alignValue += (valModStep > 0) ? step : (-step); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat(alignValue.toFixed(5)); + }, + + _valueMin: function () { + return this.options.min; + }, + + _valueMax: function () { + return this.options.max; + }, + + _refreshValue: function () { + var lastValPercent, valPercent, value, valueMin, valueMax, + oRange = this.options.range, + o = this.options, + that = this, + animate = (!this._animateOff) ? o.animate : false, + _set = {}; + + if (this.options.values && this.options.values.length) { + this.handles.each(function (i) { + valPercent = (that.values(i) - that._valueMin()) / (that._valueMax() - that._valueMin()) * 100; + _set[that.orientation === "horizontal" ? "left" : "bottom"] = valPercent + "%"; + $(this).stop(1, 1)[animate ? "animate" : "css"](_set, o.animate); + if (that.options.range === true) { + if (that.orientation === "horizontal") { + if (i === 0) { + that.range.stop(1, 1)[animate ? "animate" : "css"]({left: valPercent + "%"}, o.animate); + } + if (i === 1) { + that.range[animate ? "animate" : "css"]({width: (valPercent - lastValPercent) + "%"}, { + queue: false, + duration: o.animate + }); + } + } else { + if (i === 0) { + that.range.stop(1, 1)[animate ? "animate" : "css"]({bottom: (valPercent) + "%"}, o.animate); + } + if (i === 1) { + that.range[animate ? "animate" : "css"]({height: (valPercent - lastValPercent) + "%"}, { + queue: false, + duration: o.animate + }); + } + } + } + lastValPercent = valPercent; + }); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = (valueMax !== valueMin) ? + (value - valueMin) / (valueMax - valueMin) * 100 : + 0; + _set[this.orientation === "horizontal" ? "left" : "bottom"] = valPercent + "%"; + this.handle.stop(1, 1)[animate ? "animate" : "css"](_set, o.animate); + + if (oRange === "min" && this.orientation === "horizontal") { + this.range.stop(1, 1)[animate ? "animate" : "css"]({width: valPercent + "%"}, o.animate); + } + if (oRange === "max" && this.orientation === "horizontal") { + this.range[animate ? "animate" : "css"]({width: (100 - valPercent) + "%"}, { + queue: false, + duration: o.animate + }); + } + if (oRange === "min" && this.orientation === "vertical") { + this.range.stop(1, 1)[animate ? "animate" : "css"]({height: valPercent + "%"}, o.animate); + } + if (oRange === "max" && this.orientation === "vertical") { + this.range[animate ? "animate" : "css"]({height: (100 - valPercent) + "%"}, { + queue: false, + duration: o.animate + }); + } + } + }, + + _handleEvents: { + keydown: function (event) { + var allowed, curVal, newVal, step, + index = $(event.target).data("ui-slider-handle-index"); + + switch (event.keyCode) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if (!this._keySliding) { + this._keySliding = true; + $(event.target).addClass("ui-state-active"); + allowed = this._start(event, index); + if (allowed === false) { + return; + } + } + break; + } + + step = this.options.step; + if (this.options.values && this.options.values.length) { + curVal = newVal = this.values(index); + } else { + curVal = newVal = this.value(); + } + + switch (event.keyCode) { + case $.ui.keyCode.HOME: + newVal = this._valueMin(); + break; + case $.ui.keyCode.END: + newVal = this._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = this._trimAlignValue(curVal + ((this._valueMax() - this._valueMin()) / numPages)); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = this._trimAlignValue(curVal - ((this._valueMax() - this._valueMin()) / numPages)); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if (curVal === this._valueMax()) { + return; + } + newVal = this._trimAlignValue(curVal + step); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if (curVal === this._valueMin()) { + return; + } + newVal = this._trimAlignValue(curVal - step); + break; + } + + this._slide(event, index, newVal); + }, + click: function (event) { + event.preventDefault(); + }, + keyup: function (event) { + var index = $(event.target).data("ui-slider-handle-index"); + + if (this._keySliding) { + this._keySliding = false; + this._stop(event, index); + this._change(event, index); + $(event.target).removeClass("ui-state-active"); + } + } + } + + }); + +}); diff --git a/lib/web/jquery/ui-modules/sortable.js b/lib/web/jquery/ui-modules/sortable.js new file mode 100644 index 000000000000..9c55486f6711 --- /dev/null +++ b/lib/web/jquery/ui-modules/sortable.js @@ -0,0 +1,1322 @@ +/*! + * jQuery UI Sortable - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/sortable/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/mouse' +], function ($, undefined) { + + function isOverAxis(x, reference, size) { + return (x > reference) && (x < (reference + size)); + } + + function isFloating(item) { + return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); + } + + $.widget("ui.sortable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "sort", + ready: false, + options: { + appendTo: "parent", + axis: false, + connectWith: false, + containment: false, + cursor: "auto", + cursorAt: false, + dropOnEmpty: true, + forcePlaceholderSize: false, + forceHelperSize: false, + grid: false, + handle: false, + helper: "original", + items: "> *", + opacity: false, + placeholder: false, + revert: false, + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + scope: "default", + tolerance: "intersect", + zIndex: 1000, + + // callbacks + activate: null, + beforeStop: null, + change: null, + deactivate: null, + out: null, + over: null, + receive: null, + remove: null, + sort: null, + start: null, + stop: null, + update: null + }, + _create: function () { + + var o = this.options; + this.containerCache = {}; + this.element.addClass("ui-sortable"); + + //Get the items + this.refresh(); + + //Let's determine if the items are being displayed horizontally + this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; + + //Let's determine the parent's offset + this.offset = this.element.offset(); + + //Initialize mouse events for interaction + this._mouseInit(); + + //We're ready to go + this.ready = true; + + }, + + _destroy: function () { + this.element + .removeClass("ui-sortable ui-sortable-disabled"); + this._mouseDestroy(); + + for (var i = this.items.length - 1; i >= 0; i--) { + this.items[i].item.removeData(this.widgetName + "-item"); + } + + return this; + }, + + _setOption: function (key, value) { + if (key === "disabled") { + this.options[key] = value; + + this.widget().toggleClass("ui-sortable-disabled", !!value); + } else { + // Don't call widget base _setOption for disable as it adds ui-state-disabled class + $.Widget.prototype._setOption.apply(this, arguments); + } + }, + + _mouseCapture: function (event, overrideHandle) { + var currentItem = null, + validHandle = false, + that = this; + + if (this.reverting) { + return false; + } + + if (this.options.disabled || this.options.type === "static") { + return false; + } + + //We have to refresh the items data once first + this._refreshItems(event); + + //Find out if the clicked node (or one of its parents) is a actual item in this.items + $(event.target).parents().each(function () { + if ($.data(this, that.widgetName + "-item") === that) { + currentItem = $(this); + return false; + } + }); + if ($.data(event.target, that.widgetName + "-item") === that) { + currentItem = $(event.target); + } + + if (!currentItem) { + return false; + } + if (this.options.handle && !overrideHandle) { + $(this.options.handle, currentItem).find("*").addBack().each(function () { + if (this === event.target) { + validHandle = true; + } + }); + if (!validHandle) { + return false; + } + } + + this.currentItem = currentItem; + this._removeCurrentsFromItems(); + return true; + + }, + + _mouseStart: function (event, overrideHandle, noActivation) { + + var i, body, + o = this.options; + + this.currentContainer = this; + + //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture + this.refreshPositions(); + + //Create and append the visible helper + this.helper = this._createHelper(event); + + //Cache the helper size + this._cacheHelperProportions(); + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Get the next scrolling parent + this.scrollParent = this.helper.scrollParent(); + + //The element's absolute position on the page minus margins + this.offset = this.currentItem.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + // Only after we got the offset, we can change the helper's position to absolute + // TODO: Still need to figure out a way to make relative sorting possible + this.helper.css("position", "absolute"); + this.cssPosition = this.helper.css("position"); + + //Generate the original position + this.originalPosition = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Cache the former DOM position + this.domPosition = {prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0]}; + + //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way + if (this.helper[0] !== this.currentItem[0]) { + this.currentItem.hide(); + } + + //Create the placeholder + this._createPlaceholder(); + + //Set a containment if given in the options + if (o.containment) { + this._setContainment(); + } + + if (o.cursor && o.cursor !== "auto") { // cursor option + body = this.document.find("body"); + + // support: IE + this.storedCursor = body.css("cursor"); + body.css("cursor", o.cursor); + + this.storedStylesheet = $("<style>*{ cursor: " + o.cursor + " !important; }</style>").appendTo(body); + } + + if (o.opacity) { // opacity option + if (this.helper.css("opacity")) { + this._storedOpacity = this.helper.css("opacity"); + } + this.helper.css("opacity", o.opacity); + } + + if (o.zIndex) { // zIndex option + if (this.helper.css("zIndex")) { + this._storedZIndex = this.helper.css("zIndex"); + } + this.helper.css("zIndex", o.zIndex); + } + + //Prepare scrolling + if (this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + this.overflowOffset = this.scrollParent.offset(); + } + + //Call callbacks + this._trigger("start", event, this._uiHash()); + + //Recache the helper size + if (!this._preserveHelperProportions) { + this._cacheHelperProportions(); + } + + + //Post "activate" events to possible containers + if (!noActivation) { + for (i = this.containers.length - 1; i >= 0; i--) { + this.containers[i]._trigger("activate", event, this._uiHash(this)); + } + } + + //Prepare possible droppables + if ($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + this.dragging = true; + + this.helper.addClass("ui-sortable-helper"); + this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position + return true; + + }, + + _mouseDrag: function (event) { + var i, item, itemElement, intersection, + o = this.options, + scrolled = false; + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + if (!this.lastPositionAbs) { + this.lastPositionAbs = this.positionAbs; + } + + //Do scrolling + if (this.options.scroll) { + if (this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + + if ((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; + } else if (event.pageY - this.overflowOffset.top < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; + } + + if ((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if (event.pageX - this.overflowOffset.left < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; + } + + } else { + + if (event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if ($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + + if (event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if ($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + + } + + if (scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + } + + //Regenerate the absolute position used for position checks + this.positionAbs = this._convertPositionTo("absolute"); + + //Set the helper position + if (!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left + "px"; + } + if (!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top + "px"; + } + + //Rearrange + for (i = this.items.length - 1; i >= 0; i--) { + + //Cache variables and intersection, continue if no intersection + item = this.items[i]; + itemElement = item.item[0]; + intersection = this._intersectsWithPointer(item); + if (!intersection) { + continue; + } + + // Only put the placeholder inside the current Container, skip all + // items from other containers. This works because when moving + // an item from one container to another the + // currentContainer is switched before the placeholder is moved. + // + // Without this, moving items in "sub-sortables" can cause + // the placeholder to jitter beetween the outer and inner container. + if (item.instance !== this.currentContainer) { + continue; + } + + // cannot intersect with itself + // no useless actions that have been done before + // no action if the item moved is the parent of the item checked + if (itemElement !== this.currentItem[0] && + this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && + !$.contains(this.placeholder[0], itemElement) && + (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) + ) { + + this.direction = intersection === 1 ? "down" : "up"; + + if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { + this._rearrange(event, item); + } else { + break; + } + + this._trigger("change", event, this._uiHash()); + break; + } + } + + //Post events to containers + this._contactContainers(event); + + //Interconnect with droppables + if ($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + //Call callbacks + this._trigger("sort", event, this._uiHash()); + + this.lastPositionAbs = this.positionAbs; + return false; + + }, + + _mouseStop: function (event, noPropagation) { + + if (!event) { + return; + } + + //If we are using droppables, inform the manager about the drop + if ($.ui.ddmanager && !this.options.dropBehaviour) { + $.ui.ddmanager.drop(this, event); + } + + if (this.options.revert) { + var that = this, + cur = this.placeholder.offset(), + axis = this.options.axis, + animation = {}; + + if (!axis || axis === "x") { + animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); + } + if (!axis || axis === "y") { + animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); + } + this.reverting = true; + $(this.helper).animate(animation, parseInt(this.options.revert, 10) || 500, function () { + that._clear(event); + }); + } else { + this._clear(event, noPropagation); + } + + return false; + + }, + + cancel: function () { + + if (this.dragging) { + + this._mouseUp({target: null}); + + if (this.options.helper === "original") { + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + //Post deactivating events to containers + for (var i = this.containers.length - 1; i >= 0; i--) { + this.containers[i]._trigger("deactivate", null, this._uiHash(this)); + if (this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", null, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + if (this.placeholder) { + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + if (this.placeholder[0].parentNode) { + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + } + if (this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { + this.helper.remove(); + } + + $.extend(this, { + helper: null, + dragging: false, + reverting: false, + _noFinalSort: null + }); + + if (this.domPosition.prev) { + $(this.domPosition.prev).after(this.currentItem); + } else { + $(this.domPosition.parent).prepend(this.currentItem); + } + } + + return this; + + }, + + serialize: function (o) { + + var items = this._getItemsAsjQuery(o && o.connected), + str = []; + o = o || {}; + + $(items).each(function () { + var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); + if (res) { + str.push((o.key || res[1] + "[]") + "=" + (o.key && o.expression ? res[1] : res[2])); + } + }); + + if (!str.length && o.key) { + str.push(o.key + "="); + } + + return str.join("&"); + + }, + + toArray: function (o) { + + var items = this._getItemsAsjQuery(o && o.connected), + ret = []; + + o = o || {}; + + items.each(function () { + ret.push($(o.item || this).attr(o.attribute || "id") || ""); + }); + return ret; + + }, + + /* Be careful with the following core functions */ + _intersectsWith: function (item) { + + var x1 = this.positionAbs.left, + x2 = x1 + this.helperProportions.width, + y1 = this.positionAbs.top, + y2 = y1 + this.helperProportions.height, + l = item.left, + r = l + item.width, + t = item.top, + b = t + item.height, + dyClick = this.offset.click.top, + dxClick = this.offset.click.left, + isOverElementHeight = (this.options.axis === "x") || ((y1 + dyClick) > t && (y1 + dyClick) < b), + isOverElementWidth = (this.options.axis === "y") || ((x1 + dxClick) > l && (x1 + dxClick) < r), + isOverElement = isOverElementHeight && isOverElementWidth; + + if (this.options.tolerance === "pointer" || + this.options.forcePointerForContainers || + (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) + ) { + return isOverElement; + } else { + + return (l < x1 + (this.helperProportions.width / 2) && // Right Half + x2 - (this.helperProportions.width / 2) < r && // Left Half + t < y1 + (this.helperProportions.height / 2) && // Bottom Half + y2 - (this.helperProportions.height / 2) < b); // Top Half + + } + }, + + _intersectsWithPointer: function (item) { + + var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), + isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), + isOverElement = isOverElementHeight && isOverElementWidth, + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (!isOverElement) { + return false; + } + + return this.floating ? + (((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1) + : (verticalDirection && (verticalDirection === "down" ? 2 : 1)); + + }, + + _intersectsWithSides: function (item) { + + var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height / 2), item.height), + isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width / 2), item.width), + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (this.floating && horizontalDirection) { + return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); + } else { + return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); + } + + }, + + _getDragVerticalDirection: function () { + var delta = this.positionAbs.top - this.lastPositionAbs.top; + return delta !== 0 && (delta > 0 ? "down" : "up"); + }, + + _getDragHorizontalDirection: function () { + var delta = this.positionAbs.left - this.lastPositionAbs.left; + return delta !== 0 && (delta > 0 ? "right" : "left"); + }, + + refresh: function (event) { + this._refreshItems(event); + this.refreshPositions(); + return this; + }, + + _connectWith: function () { + var options = this.options; + return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; + }, + + _getItemsAsjQuery: function (connected) { + + var i, j, cur, inst, + items = [], + queries = [], + connectWith = this._connectWith(); + + if (connectWith && connected) { + for (i = connectWith.length - 1; i >= 0; i--) { + cur = $(connectWith[i]); + for (j = cur.length - 1; j >= 0; j--) { + inst = $.data(cur[j], this.widgetFullName); + if (inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); + } + } + } + } + + queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { + options: this.options, + item: this.currentItem + }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); + + function addItems() { + items.push(this); + } + + for (i = queries.length - 1; i >= 0; i--) { + queries[i][0].each(addItems); + } + + return $(items); + + }, + + _removeCurrentsFromItems: function () { + + var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); + + this.items = $.grep(this.items, function (item) { + for (var j = 0; j < list.length; j++) { + if (list[j] === item.item[0]) { + return false; + } + } + return true; + }); + + }, + + _refreshItems: function (event) { + + this.items = []; + this.containers = [this]; + + var i, j, cur, inst, targetData, _queries, item, queriesLength, + items = this.items, + queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, {item: this.currentItem}) : $(this.options.items, this.element), this]], + connectWith = this._connectWith(); + + if (connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down + for (i = connectWith.length - 1; i >= 0; i--) { + cur = $(connectWith[i]); + for (j = cur.length - 1; j >= 0; j--) { + inst = $.data(cur[j], this.widgetFullName); + if (inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, {item: this.currentItem}) : $(inst.options.items, inst.element), inst]); + this.containers.push(inst); + } + } + } + } + + for (i = queries.length - 1; i >= 0; i--) { + targetData = queries[i][1]; + _queries = queries[i][0]; + + for (j = 0, queriesLength = _queries.length; j < queriesLength; j++) { + item = $(_queries[j]); + + item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) + + items.push({ + item: item, + instance: targetData, + width: 0, height: 0, + left: 0, top: 0 + }); + } + } + + }, + + refreshPositions: function (fast) { + + //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change + if (this.offsetParent && this.helper) { + this.offset.parent = this._getParentOffset(); + } + + var i, item, t, p; + + for (i = this.items.length - 1; i >= 0; i--) { + item = this.items[i]; + + //We ignore calculating positions of all connected containers when we're not over them + if (item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { + continue; + } + + t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; + + if (!fast) { + item.width = t.outerWidth(); + item.height = t.outerHeight(); + } + + p = t.offset(); + item.left = p.left; + item.top = p.top; + } + + if (this.options.custom && this.options.custom.refreshContainers) { + this.options.custom.refreshContainers.call(this); + } else { + for (i = this.containers.length - 1; i >= 0; i--) { + p = this.containers[i].element.offset(); + this.containers[i].containerCache.left = p.left; + this.containers[i].containerCache.top = p.top; + this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); + this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); + } + } + + return this; + }, + + _createPlaceholder: function (that) { + that = that || this; + var className, + o = that.options; + + if (!o.placeholder || o.placeholder.constructor === String) { + className = o.placeholder; + o.placeholder = { + element: function () { + + var nodeName = that.currentItem[0].nodeName.toLowerCase(), + element = $("<" + nodeName + ">", that.document[0]) + .addClass(className || that.currentItem[0].className + " ui-sortable-placeholder") + .removeClass("ui-sortable-helper"); + + if (nodeName === "tr") { + that.currentItem.children().each(function () { + $("<td> </td>", that.document[0]) + .attr("colspan", $(this).attr("colspan") || 1) + .appendTo(element); + }); + } else if (nodeName === "img") { + element.attr("src", that.currentItem.attr("src")); + } + + if (!className) { + element.css("visibility", "hidden"); + } + + return element; + }, + update: function (container, p) { + + // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that + // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified + if (className && !o.forcePlaceholderSize) { + return; + } + + //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item + if (!p.height()) { + p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop") || 0, 10) - parseInt(that.currentItem.css("paddingBottom") || 0, 10)); + } + if (!p.width()) { + p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft") || 0, 10) - parseInt(that.currentItem.css("paddingRight") || 0, 10)); + } + } + }; + } + + //Create the placeholder + that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); + + //Append it after the actual current item + that.currentItem.after(that.placeholder); + + //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) + o.placeholder.update(that, that.placeholder); + + }, + + _contactContainers: function (event) { + var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, + innermostContainer = null, + innermostIndex = null; + + // get innermost container that intersects with item + for (i = this.containers.length - 1; i >= 0; i--) { + + // never consider a container that's located within the item itself + if ($.contains(this.currentItem[0], this.containers[i].element[0])) { + continue; + } + + if (this._intersectsWith(this.containers[i].containerCache)) { + + // if we've already found a container and it's more "inner" than this, then continue + if (innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { + continue; + } + + innermostContainer = this.containers[i]; + innermostIndex = i; + + } else { + // container doesn't intersect. trigger "out" event if necessary + if (this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", event, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + // if no intersecting containers found, return + if (!innermostContainer) { + return; + } + + // move the item into the container if it's not there already + if (this.containers.length === 1) { + if (!this.containers[innermostIndex].containerCache.over) { + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + } else { + + //When entering a new container, we will find the item with the least distance and append our item near it + dist = 10000; + itemWithLeastDistance = null; + floating = innermostContainer.floating || isFloating(this.currentItem); + posProperty = floating ? "left" : "top"; + sizeProperty = floating ? "width" : "height"; + base = this.positionAbs[posProperty] + this.offset.click[posProperty]; + for (j = this.items.length - 1; j >= 0; j--) { + if (!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { + continue; + } + if (this.items[j].item[0] === this.currentItem[0]) { + continue; + } + if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { + continue; + } + cur = this.items[j].item.offset()[posProperty]; + nearBottom = false; + if (Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)) { + nearBottom = true; + cur += this.items[j][sizeProperty]; + } + + if (Math.abs(cur - base) < dist) { + dist = Math.abs(cur - base); + itemWithLeastDistance = this.items[j]; + this.direction = nearBottom ? "up" : "down"; + } + } + + //Check if dropOnEmpty is enabled + if (!itemWithLeastDistance && !this.options.dropOnEmpty) { + return; + } + + if (this.currentContainer === this.containers[innermostIndex]) { + return; + } + + itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); + this._trigger("change", event, this._uiHash()); + this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); + this.currentContainer = this.containers[innermostIndex]; + + //Update the placeholder + this.options.placeholder.update(this.currentContainer, this.placeholder); + + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + + + }, + + _createHelper: function (event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); + + //Add the helper to the DOM if that didn't happen already + if (!helper.parents("body").length) { + $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); + } + + if (helper[0] === this.currentItem[0]) { + this._storedCSS = { + width: this.currentItem[0].style.width, + height: this.currentItem[0].style.height, + position: this.currentItem.css("position"), + top: this.currentItem.css("top"), + left: this.currentItem.css("left") + }; + } + + if (!helper[0].style.width || o.forceHelperSize) { + helper.width(this.currentItem.width()); + } + if (!helper[0].style.height || o.forceHelperSize) { + helper.height(this.currentItem.height()); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function (obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function () { + + + //Get the offsetParent and cache its position + this.offsetParent = this.helper.offsetParent(); + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if (this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + // This needs to be actually done for all browsers, since pageX/pageY includes this information + // with an ugly IE fix + if (this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = {top: 0, left: 0}; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"), 10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"), 10) || 0) + }; + + }, + + _getRelativeOffset: function () { + + if (this.cssPosition === "relative") { + var p = this.currentItem.position(); + return { + top: p.top - (parseInt(this.helper.css("top"), 10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"), 10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return {top: 0, left: 0}; + } + + }, + + _cacheMargins: function () { + this.margins = { + left: (parseInt(this.currentItem.css("marginLeft"), 10) || 0), + top: (parseInt(this.currentItem.css("marginTop"), 10) || 0) + }; + }, + + _cacheHelperProportions: function () { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function () { + + var ce, co, over, + o = this.options; + if (o.containment === "parent") { + o.containment = this.helper[0].parentNode; + } + if (o.containment === "document" || o.containment === "window") { + this.containment = [ + 0 - this.offset.relative.left - this.offset.parent.left, + 0 - this.offset.relative.top - this.offset.parent.top, + $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, + ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top + ]; + } + + if (!(/^(document|window|parent)$/).test(o.containment)) { + ce = $(o.containment)[0]; + co = $(o.containment).offset(); + over = ($(ce).css("overflow") !== "hidden"); + + this.containment = [ + co.left + (parseInt($(ce).css("borderLeftWidth"), 10) || 0) + (parseInt($(ce).css("paddingLeft"), 10) || 0) - this.margins.left, + co.top + (parseInt($(ce).css("borderTopWidth"), 10) || 0) + (parseInt($(ce).css("paddingTop"), 10) || 0) - this.margins.top, + co.left + (over ? Math.max(ce.scrollWidth, ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"), 10) || 0) - (parseInt($(ce).css("paddingRight"), 10) || 0) - this.helperProportions.width - this.margins.left, + co.top + (over ? Math.max(ce.scrollHeight, ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"), 10) || 0) - (parseInt($(ce).css("paddingBottom"), 10) || 0) - this.helperProportions.height - this.margins.top + ]; + } + + }, + + _convertPositionTo: function (d, pos) { + + if (!pos) { + pos = this.position; + } + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, + scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ((this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : (scrollIsRootNode ? 0 : scroll.scrollTop())) * mod) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ((this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft()) * mod) + ) + }; + + }, + + _generatePosition: function (event) { + + var top, left, + o = this.options, + pageX = event.pageX, + pageY = event.pageY, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, + scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + // This is another very weird special case that only happens for relative elements: + // 1. If the css position is relative + // 2. and the scroll parent is the document or similar to the offset parent + // we have to refresh the relative offset during the scroll so there are no jumps + if (this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { + this.offset.relative = this._getRelativeOffset(); + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + if (this.originalPosition) { //If we are not dragging yet, we won't check for options + + if (this.containment) { + if (event.pageX - this.offset.click.left < this.containment[0]) { + pageX = this.containment[0] + this.offset.click.left; + } + if (event.pageY - this.offset.click.top < this.containment[1]) { + pageY = this.containment[1] + this.offset.click.top; + } + if (event.pageX - this.offset.click.left > this.containment[2]) { + pageX = this.containment[2] + this.offset.click.left; + } + if (event.pageY - this.offset.click.top > this.containment[3]) { + pageY = this.containment[3] + this.offset.click.top; + } + } + + if (o.grid) { + top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; + pageY = this.containment ? ((top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; + pageX = this.containment ? ((left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ((this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : (scrollIsRootNode ? 0 : scroll.scrollTop()))) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ((this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft())) + ) + }; + + }, + + _rearrange: function (event, i, a, hardRefresh) { + + a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); + + //Various things done here to improve the performance: + // 1. we create a setTimeout, that calls refreshPositions + // 2. on the instance, we have a counter variable, that get's higher after every append + // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same + // 4. this lets only the last addition to the timeout stack through + this.counter = this.counter ? ++this.counter : 1; + var counter = this.counter; + + this._delay(function () { + if (counter === this.counter) { + this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove + } + }); + + }, + + _clear: function (event, noPropagation) { + + this.reverting = false; + // We delay all events that have to be triggered to after the point where the placeholder has been removed and + // everything else normalized again + var i, + delayedTriggers = []; + + // We first have to update the dom position of the actual currentItem + // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) + if (!this._noFinalSort && this.currentItem.parent().length) { + this.placeholder.before(this.currentItem); + } + this._noFinalSort = null; + + if (this.helper[0] === this.currentItem[0]) { + for (i in this._storedCSS) { + if (this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { + this._storedCSS[i] = ""; + } + } + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + if (this.fromOutside && !noPropagation) { + delayedTriggers.push(function (event) { + this._trigger("receive", event, this._uiHash(this.fromOutside)); + }); + } + if ((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { + delayedTriggers.push(function (event) { + this._trigger("update", event, this._uiHash()); + }); //Trigger update callback if the DOM position has changed + } + + // Check if the items Container has Changed and trigger appropriate + // events. + if (this !== this.currentContainer) { + if (!noPropagation) { + delayedTriggers.push(function (event) { + this._trigger("remove", event, this._uiHash()); + }); + delayedTriggers.push((function (c) { + return function (event) { + c._trigger("receive", event, this._uiHash(this)); + }; + }).call(this, this.currentContainer)); + delayedTriggers.push((function (c) { + return function (event) { + c._trigger("update", event, this._uiHash(this)); + }; + }).call(this, this.currentContainer)); + } + } + + + //Post events to containers + function delayEvent(type, instance, container) { + return function (event) { + container._trigger(type, event, instance._uiHash(instance)); + }; + } + + for (i = this.containers.length - 1; i >= 0; i--) { + if (!noPropagation) { + delayedTriggers.push(delayEvent("deactivate", this, this.containers[i])); + } + if (this.containers[i].containerCache.over) { + delayedTriggers.push(delayEvent("out", this, this.containers[i])); + this.containers[i].containerCache.over = 0; + } + } + + //Do what was originally in plugins + if (this.storedCursor) { + this.document.find("body").css("cursor", this.storedCursor); + this.storedStylesheet.remove(); + } + if (this._storedOpacity) { + this.helper.css("opacity", this._storedOpacity); + } + if (this._storedZIndex) { + this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); + } + + this.dragging = false; + if (this.cancelHelperRemoval) { + if (!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + for (i = 0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return false; + } + + if (!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + } + + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + + if (this.helper[0] !== this.currentItem[0]) { + this.helper.remove(); + } + this.helper = null; + + if (!noPropagation) { + for (i = 0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return true; + + }, + + _trigger: function () { + if ($.Widget.prototype._trigger.apply(this, arguments) === false) { + this.cancel(); + } + }, + + _uiHash: function (_inst) { + var inst = _inst || this; + return { + helper: inst.helper, + placeholder: inst.placeholder || $([]), + position: inst.position, + originalPosition: inst.originalPosition, + offset: inst.positionAbs, + item: inst.currentItem, + sender: _inst ? _inst.element : null + }; + } + + }); + +}); diff --git a/lib/web/jquery/ui-modules/spinner.js b/lib/web/jquery/ui-modules/spinner.js new file mode 100644 index 000000000000..f18621e39dd9 --- /dev/null +++ b/lib/web/jquery/ui-modules/spinner.js @@ -0,0 +1,497 @@ +/*! + * jQuery UI Spinner - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/spinner/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/button' +], function ($) { + + function modifier(fn) { + return function () { + var previous = this.element.val(); + fn.apply(this, arguments); + this._refresh(); + if (previous !== this.element.val()) { + this._trigger("change"); + } + }; + } + + $.widget("ui.spinner", { + version: "1.10.4", + defaultElement: "<input>", + widgetEventPrefix: "spin", + options: { + culture: null, + icons: { + down: "ui-icon-triangle-1-s", + up: "ui-icon-triangle-1-n" + }, + incremental: true, + max: null, + min: null, + numberFormat: null, + page: 10, + step: 1, + + change: null, + spin: null, + start: null, + stop: null + }, + + _create: function () { + // handle string values that need to be parsed + this._setOption("max", this.options.max); + this._setOption("min", this.options.min); + this._setOption("step", this.options.step); + + // Only format if there is a value, prevents the field from being marked + // as invalid in Firefox, see #9573. + if (this.value() !== "") { + // Format the value, but don't constrain. + this._value(this.element.val(), true); + } + + this._draw(); + this._on(this._events); + this._refresh(); + + // turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on(this.window, { + beforeunload: function () { + this.element.removeAttr("autocomplete"); + } + }); + }, + + _getCreateOptions: function () { + var options = {}, + element = this.element; + + $.each(["min", "max", "step"], function (i, option) { + var value = element.attr(option); + if (value !== undefined && value.length) { + options[option] = value; + } + }); + + return options; + }, + + _events: { + keydown: function (event) { + if (this._start(event) && this._keydown(event)) { + event.preventDefault(); + } + }, + keyup: "_stop", + focus: function () { + this.previous = this.element.val(); + }, + blur: function (event) { + if (this.cancelBlur) { + delete this.cancelBlur; + return; + } + + this._stop(); + this._refresh(); + if (this.previous !== this.element.val()) { + this._trigger("change", event); + } + }, + mousewheel: function (event, delta) { + if (!delta) { + return; + } + if (!this.spinning && !this._start(event)) { + return false; + } + + this._spin((delta > 0 ? 1 : -1) * this.options.step, event); + clearTimeout(this.mousewheelTimer); + this.mousewheelTimer = this._delay(function () { + if (this.spinning) { + this._stop(event); + } + }, 100); + event.preventDefault(); + }, + "mousedown .ui-spinner-button": function (event) { + var previous; + + // We never want the buttons to have focus; whenever the user is + // interacting with the spinner, the focus should be on the input. + // If the input is focused then this.previous is properly set from + // when the input first received focus. If the input is not focused + // then we need to set this.previous based on the value before spinning. + previous = this.element[0] === this.document[0].activeElement ? + this.previous : this.element.val(); + + function checkFocus() { + var isActive = this.element[0] === this.document[0].activeElement; + if (!isActive) { + this.element.focus(); + this.previous = previous; + // support: IE + // IE sets focus asynchronously, so we need to check if focus + // moved off of the input because the user clicked on the button. + this._delay(function () { + this.previous = previous; + }); + } + } + + // ensure focus is on (or stays on) the text field + event.preventDefault(); + checkFocus.call(this); + + // support: IE + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + // and check (again) if focus moved off of the input. + this.cancelBlur = true; + this._delay(function () { + delete this.cancelBlur; + checkFocus.call(this); + }); + + if (this._start(event) === false) { + return; + } + + this._repeat(null, $(event.currentTarget).hasClass("ui-spinner-up") ? 1 : -1, event); + }, + "mouseup .ui-spinner-button": "_stop", + "mouseenter .ui-spinner-button": function (event) { + // button will add ui-state-active if mouse was down while mouseleave and kept down + if (!$(event.currentTarget).hasClass("ui-state-active")) { + return; + } + + if (this._start(event) === false) { + return false; + } + this._repeat(null, $(event.currentTarget).hasClass("ui-spinner-up") ? 1 : -1, event); + }, + // TODO: do we really want to consider this a stop? + // shouldn't we just stop the repeater and wait until mouseup before + // we trigger the stop event? + "mouseleave .ui-spinner-button": "_stop" + }, + + _draw: function () { + var uiSpinner = this.uiSpinner = this.element + .addClass("ui-spinner-input") + .attr("autocomplete", "off") + .wrap(this._uiSpinnerHtml()) + .parent() + // add buttons + .append(this._buttonHtml()); + + this.element.attr("role", "spinbutton"); + + // button bindings + this.buttons = uiSpinner.find(".ui-spinner-button") + .attr("tabIndex", -1) + .button() + .removeClass("ui-corner-all"); + + // IE 6 doesn't understand height: 50% for the buttons + // unless the wrapper has an explicit height + if (this.buttons.height() > Math.ceil(uiSpinner.height() * 0.5) && + uiSpinner.height() > 0) { + uiSpinner.height(uiSpinner.height()); + } + + // disable spinner if element was already disabled + if (this.options.disabled) { + this.disable(); + } + }, + + _keydown: function (event) { + var options = this.options, + keyCode = $.ui.keyCode; + + switch (event.keyCode) { + case keyCode.UP: + this._repeat(null, 1, event); + return true; + case keyCode.DOWN: + this._repeat(null, -1, event); + return true; + case keyCode.PAGE_UP: + this._repeat(null, options.page, event); + return true; + case keyCode.PAGE_DOWN: + this._repeat(null, -options.page, event); + return true; + } + + return false; + }, + + _uiSpinnerHtml: function () { + return "<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"; + }, + + _buttonHtml: function () { + return "" + + "<a class='ui-spinner-button ui-spinner-up ui-corner-tr'>" + + "<span class='ui-icon " + this.options.icons.up + "'>▲</span>" + + "</a>" + + "<a class='ui-spinner-button ui-spinner-down ui-corner-br'>" + + "<span class='ui-icon " + this.options.icons.down + "'>▼</span>" + + "</a>"; + }, + + _start: function (event) { + if (!this.spinning && this._trigger("start", event) === false) { + return false; + } + + if (!this.counter) { + this.counter = 1; + } + this.spinning = true; + return true; + }, + + _repeat: function (i, steps, event) { + i = i || 500; + + clearTimeout(this.timer); + this.timer = this._delay(function () { + this._repeat(40, steps, event); + }, i); + + this._spin(steps * this.options.step, event); + }, + + _spin: function (step, event) { + var value = this.value() || 0; + + if (!this.counter) { + this.counter = 1; + } + + value = this._adjustValue(value + step * this._increment(this.counter)); + + if (!this.spinning || this._trigger("spin", event, {value: value}) !== false) { + this._value(value); + this.counter++; + } + }, + + _increment: function (i) { + var incremental = this.options.incremental; + + if (incremental) { + return $.isFunction(incremental) ? + incremental(i) : + Math.floor(i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1); + } + + return 1; + }, + + _precision: function () { + var precision = this._precisionOf(this.options.step); + if (this.options.min !== null) { + precision = Math.max(precision, this._precisionOf(this.options.min)); + } + return precision; + }, + + _precisionOf: function (num) { + var str = num.toString(), + decimal = str.indexOf("."); + return decimal === -1 ? 0 : str.length - decimal - 1; + }, + + _adjustValue: function (value) { + var base, aboveMin, + options = this.options; + + // make sure we're at a valid step + // - find out where we are relative to the base (min or 0) + base = options.min !== null ? options.min : 0; + aboveMin = value - base; + // - round to the nearest step + aboveMin = Math.round(aboveMin / options.step) * options.step; + // - rounding is based on 0, so adjust back to our base + value = base + aboveMin; + + // fix precision from bad JS floating point math + value = parseFloat(value.toFixed(this._precision())); + + // clamp the value + if (options.max !== null && value > options.max) { + return options.max; + } + if (options.min !== null && value < options.min) { + return options.min; + } + + return value; + }, + + _stop: function (event) { + if (!this.spinning) { + return; + } + + clearTimeout(this.timer); + clearTimeout(this.mousewheelTimer); + this.counter = 0; + this.spinning = false; + this._trigger("stop", event); + }, + + _setOption: function (key, value) { + if (key === "culture" || key === "numberFormat") { + var prevValue = this._parse(this.element.val()); + this.options[key] = value; + this.element.val(this._format(prevValue)); + return; + } + + if (key === "max" || key === "min" || key === "step") { + if (typeof value === "string") { + value = this._parse(value); + } + } + if (key === "icons") { + this.buttons.first().find(".ui-icon") + .removeClass(this.options.icons.up) + .addClass(value.up); + this.buttons.last().find(".ui-icon") + .removeClass(this.options.icons.down) + .addClass(value.down); + } + + this._super(key, value); + + if (key === "disabled") { + if (value) { + this.element.prop("disabled", true); + this.buttons.button("disable"); + } else { + this.element.prop("disabled", false); + this.buttons.button("enable"); + } + } + }, + + _setOptions: modifier(function (options) { + this._super(options); + this._value(this.element.val()); + }), + + _parse: function (val) { + if (typeof val === "string" && val !== "") { + val = window.Globalize && this.options.numberFormat ? + Globalize.parseFloat(val, 10, this.options.culture) : +val; + } + return val === "" || isNaN(val) ? null : val; + }, + + _format: function (value) { + if (value === "") { + return ""; + } + return window.Globalize && this.options.numberFormat ? + Globalize.format(value, this.options.numberFormat, this.options.culture) : + value; + }, + + _refresh: function () { + this.element.attr({ + "aria-valuemin": this.options.min, + "aria-valuemax": this.options.max, + // TODO: what should we do with values that can't be parsed? + "aria-valuenow": this._parse(this.element.val()) + }); + }, + + // update the value without triggering change + _value: function (value, allowAny) { + var parsed; + if (value !== "") { + parsed = this._parse(value); + if (parsed !== null) { + if (!allowAny) { + parsed = this._adjustValue(parsed); + } + value = this._format(parsed); + } + } + this.element.val(value); + this._refresh(); + }, + + _destroy: function () { + this.element + .removeClass("ui-spinner-input") + .prop("disabled", false) + .removeAttr("autocomplete") + .removeAttr("role") + .removeAttr("aria-valuemin") + .removeAttr("aria-valuemax") + .removeAttr("aria-valuenow"); + this.uiSpinner.replaceWith(this.element); + }, + + stepUp: modifier(function (steps) { + this._stepUp(steps); + }), + _stepUp: function (steps) { + if (this._start()) { + this._spin((steps || 1) * this.options.step); + this._stop(); + } + }, + + stepDown: modifier(function (steps) { + this._stepDown(steps); + }), + _stepDown: function (steps) { + if (this._start()) { + this._spin((steps || 1) * -this.options.step); + this._stop(); + } + }, + + pageUp: modifier(function (pages) { + this._stepUp((pages || 1) * this.options.page); + }), + + pageDown: modifier(function (pages) { + this._stepDown((pages || 1) * this.options.page); + }), + + value: function (newVal) { + if (!arguments.length) { + return this._parse(this.element.val()); + } + modifier(this._value).call(this, newVal); + }, + + widget: function () { + return this.uiSpinner; + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/tabs.js b/lib/web/jquery/ui-modules/tabs.js new file mode 100644 index 000000000000..32c592de959e --- /dev/null +++ b/lib/web/jquery/ui-modules/tabs.js @@ -0,0 +1,850 @@ +/*! + * jQuery UI Tabs - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/tabs/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/widget' +], function ($, undefined) { + + var tabId = 0, + rhash = /#.*$/; + + function getNextTabId() { + return ++tabId; + } + + function isLocal(anchor) { + // support: IE7 + // IE7 doesn't normalize the href property when set via script (#9317) + anchor = anchor.cloneNode(false); + + return anchor.hash.length > 1 && + decodeURIComponent(anchor.href.replace(rhash, "")) === + decodeURIComponent(location.href.replace(rhash, "")); + } + + $.widget("ui.tabs", { + version: "1.10.4", + delay: 300, + options: { + active: null, + collapsible: false, + event: "click", + heightStyle: "content", + hide: null, + show: null, + + // callbacks + activate: null, + beforeActivate: null, + beforeLoad: null, + load: null + }, + + _create: function () { + var that = this, + options = this.options; + + this.running = false; + + this.element + .addClass("ui-tabs ui-widget ui-widget-content ui-corner-all") + .toggleClass("ui-tabs-collapsible", options.collapsible) + // Prevent users from focusing disabled tabs via click + .delegate(".ui-tabs-nav > li", "mousedown" + this.eventNamespace, function (event) { + if ($(this).is(".ui-state-disabled")) { + event.preventDefault(); + } + }) + // support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .delegate(".ui-tabs-anchor", "focus" + this.eventNamespace, function () { + if ($(this).closest("li").is(".ui-state-disabled")) { + this.blur(); + } + }); + + this._processTabs(); + options.active = this._initialActive(); + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ($.isArray(options.disabled)) { + options.disabled = $.unique(options.disabled.concat( + $.map(this.tabs.filter(".ui-state-disabled"), function (li) { + return that.tabs.index(li); + }) + )).sort(); + } + + // check for length avoids error when initializing empty list + if (this.options.active !== false && this.anchors.length) { + this.active = this._findActive(options.active); + } else { + this.active = $(); + } + + this._refresh(); + + if (this.active.length) { + this.load(options.active); + } + }, + + _initialActive: function () { + var active = this.options.active, + collapsible = this.options.collapsible, + locationHash = location.hash.substring(1); + + if (active === null) { + // check the fragment identifier in the URL + if (locationHash) { + this.tabs.each(function (i, tab) { + if ($(tab).attr("aria-controls") === locationHash) { + active = i; + return false; + } + }); + } + + // check for a tab marked active via a class + if (active === null) { + active = this.tabs.index(this.tabs.filter(".ui-tabs-active")); + } + + // no active tab, set to false + if (active === null || active === -1) { + active = this.tabs.length ? 0 : false; + } + } + + // handle numbers: negative, out of range + if (active !== false) { + active = this.tabs.index(this.tabs.eq(active)); + if (active === -1) { + active = collapsible ? false : 0; + } + } + + // don't allow collapsible: false and active: false + if (!collapsible && active === false && this.anchors.length) { + active = 0; + } + + return active; + }, + + _getCreateEventData: function () { + return { + tab: this.active, + panel: !this.active.length ? $() : this._getPanelForTab(this.active) + }; + }, + + _tabKeydown: function (event) { + var focusedTab = $(this.document[0].activeElement).closest("li"), + selectedIndex = this.tabs.index(focusedTab), + goingForward = true; + + if (this._handlePageNav(event)) { + return; + } + + switch (event.keyCode) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = this.anchors.length - 1; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: + // Activate only, no collapsing + event.preventDefault(); + clearTimeout(this.activating); + this._activate(selectedIndex); + return; + case $.ui.keyCode.ENTER: + // Toggle (cancel delayed activation, allow collapsing) + event.preventDefault(); + clearTimeout(this.activating); + // Determine if we should collapse or activate + this._activate(selectedIndex === this.options.active ? false : selectedIndex); + return; + default: + return; + } + + // Focus the appropriate tab, based on which key was pressed + event.preventDefault(); + clearTimeout(this.activating); + selectedIndex = this._focusNextTab(selectedIndex, goingForward); + + // Navigating with control key will prevent automatic activation + if (!event.ctrlKey) { + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab, + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr("aria-selected", "false"); + this.tabs.eq(selectedIndex).attr("aria-selected", "true"); + + this.activating = this._delay(function () { + this.option("active", selectedIndex); + }, this.delay); + } + }, + + _panelKeydown: function (event) { + if (this._handlePageNav(event)) { + return; + } + + // Ctrl+up moves focus to the current tab + if (event.ctrlKey && event.keyCode === $.ui.keyCode.UP) { + event.preventDefault(); + this.active.focus(); + } + }, + + // Alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function (event) { + if (event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP) { + this._activate(this._focusNextTab(this.options.active - 1, false)); + return true; + } + if (event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN) { + this._activate(this._focusNextTab(this.options.active + 1, true)); + return true; + } + }, + + _findNextTab: function (index, goingForward) { + var lastTabIndex = this.tabs.length - 1; + + function constrain() { + if (index > lastTabIndex) { + index = 0; + } + if (index < 0) { + index = lastTabIndex; + } + return index; + } + + while ($.inArray(constrain(), this.options.disabled) !== -1) { + index = goingForward ? index + 1 : index - 1; + } + + return index; + }, + + _focusNextTab: function (index, goingForward) { + index = this._findNextTab(index, goingForward); + this.tabs.eq(index).focus(); + return index; + }, + + _setOption: function (key, value) { + if (key === "active") { + // _activate() will handle invalid values and update this.options + this._activate(value); + return; + } + + if (key === "disabled") { + // don't use the widget factory's disabled handling + this._setupDisabled(value); + return; + } + + this._super(key, value); + + if (key === "collapsible") { + this.element.toggleClass("ui-tabs-collapsible", value); + // Setting collapsible: false while collapsed; open first panel + if (!value && this.options.active === false) { + this._activate(0); + } + } + + if (key === "event") { + this._setupEvents(value); + } + + if (key === "heightStyle") { + this._setupHeightStyle(value); + } + }, + + _tabId: function (tab) { + return tab.attr("aria-controls") || "ui-tabs-" + getNextTabId(); + }, + + _sanitizeSelector: function (hash) { + return hash ? hash.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&") : ""; + }, + + refresh: function () { + var options = this.options, + lis = this.tablist.children(":has(a[href])"); + + // get disabled tabs from class attribute from HTML + // this will get converted to a boolean if needed in _refresh() + options.disabled = $.map(lis.filter(".ui-state-disabled"), function (tab) { + return lis.index(tab); + }); + + this._processTabs(); + + // was collapsed or no tabs + if (options.active === false || !this.anchors.length) { + options.active = false; + this.active = $(); + // was active, but active tab is gone + } else if (this.active.length && !$.contains(this.tablist[0], this.active[0])) { + // all remaining tabs are disabled + if (this.tabs.length === options.disabled.length) { + options.active = false; + this.active = $(); + // activate previous tab + } else { + this._activate(this._findNextTab(Math.max(0, options.active - 1), false)); + } + // was active, active tab still exists + } else { + // make sure active index is correct + options.active = this.tabs.index(this.active); + } + + this._refresh(); + }, + + _refresh: function () { + this._setupDisabled(this.options.disabled); + this._setupEvents(this.options.event); + this._setupHeightStyle(this.options.heightStyle); + + this.tabs.not(this.active).attr({ + "aria-selected": "false", + tabIndex: -1 + }); + this.panels.not(this._getPanelForTab(this.active)) + .hide() + .attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + + // Make sure one tab is in the tab order + if (!this.active.length) { + this.tabs.eq(0).attr("tabIndex", 0); + } else { + this.active + .addClass("ui-tabs-active ui-state-active") + .attr({ + "aria-selected": "true", + tabIndex: 0 + }); + this._getPanelForTab(this.active) + .show() + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + } + }, + + _processTabs: function () { + var that = this; + + this.tablist = this._getList() + .addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all") + .attr("role", "tablist"); + + this.tabs = this.tablist.find("> li:has(a[href])") + .addClass("ui-state-default ui-corner-top") + .attr({ + role: "tab", + tabIndex: -1 + }); + + this.anchors = this.tabs.map(function () { + return $("a", this)[0]; + }) + .addClass("ui-tabs-anchor") + .attr({ + role: "presentation", + tabIndex: -1 + }); + + this.panels = $(); + + this.anchors.each(function (i, anchor) { + var selector, panel, panelId, + anchorId = $(anchor).uniqueId().attr("id"), + tab = $(anchor).closest("li"), + originalAriaControls = tab.attr("aria-controls"); + + // inline tab + if (isLocal(anchor)) { + selector = anchor.hash; + panel = that.element.find(that._sanitizeSelector(selector)); + // remote tab + } else { + panelId = that._tabId(tab); + selector = "#" + panelId; + panel = that.element.find(selector); + if (!panel.length) { + panel = that._createPanel(panelId); + panel.insertAfter(that.panels[i - 1] || that.tablist); + } + panel.attr("aria-live", "polite"); + } + + if (panel.length) { + that.panels = that.panels.add(panel); + } + if (originalAriaControls) { + tab.data("ui-tabs-aria-controls", originalAriaControls); + } + tab.attr({ + "aria-controls": selector.substring(1), + "aria-labelledby": anchorId + }); + panel.attr("aria-labelledby", anchorId); + }); + + this.panels + .addClass("ui-tabs-panel ui-widget-content ui-corner-bottom") + .attr("role", "tabpanel"); + }, + + // allow overriding how to find the list for rare usage scenarios (#7715) + _getList: function () { + return this.tablist || this.element.find("ol,ul").eq(0); + }, + + _createPanel: function (id) { + return $("<div>") + .attr("id", id) + .addClass("ui-tabs-panel ui-widget-content ui-corner-bottom") + .data("ui-tabs-destroy", true); + }, + + _setupDisabled: function (disabled) { + if ($.isArray(disabled)) { + if (!disabled.length) { + disabled = false; + } else if (disabled.length === this.anchors.length) { + disabled = true; + } + } + + // disable tabs + for (var i = 0, li; (li = this.tabs[i]); i++) { + if (disabled === true || $.inArray(i, disabled) !== -1) { + $(li) + .addClass("ui-state-disabled") + .attr("aria-disabled", "true"); + } else { + $(li) + .removeClass("ui-state-disabled") + .removeAttr("aria-disabled"); + } + } + + this.options.disabled = disabled; + }, + + _setupEvents: function (event) { + var events = { + click: function (event) { + event.preventDefault(); + } + }; + if (event) { + $.each(event.split(" "), function (index, eventName) { + events[eventName] = "_eventHandler"; + }); + } + + this._off(this.anchors.add(this.tabs).add(this.panels)); + this._on(this.anchors, events); + this._on(this.tabs, {keydown: "_tabKeydown"}); + this._on(this.panels, {keydown: "_panelKeydown"}); + + this._focusable(this.tabs); + this._hoverable(this.tabs); + }, + + _setupHeightStyle: function (heightStyle) { + var maxHeight, + parent = this.element.parent(); + + if (heightStyle === "fill") { + maxHeight = parent.height(); + maxHeight -= this.element.outerHeight() - this.element.height(); + + this.element.siblings(":visible").each(function () { + var elem = $(this), + position = elem.css("position"); + + if (position === "absolute" || position === "fixed") { + return; + } + maxHeight -= elem.outerHeight(true); + }); + + this.element.children().not(this.panels).each(function () { + maxHeight -= $(this).outerHeight(true); + }); + + this.panels.each(function () { + $(this).height(Math.max(0, maxHeight - + $(this).innerHeight() + $(this).height())); + }) + .css("overflow", "auto"); + } else if (heightStyle === "auto") { + maxHeight = 0; + this.panels.each(function () { + maxHeight = Math.max(maxHeight, $(this).height("").height()); + }).height(maxHeight); + } + }, + + _eventHandler: function (event) { + var options = this.options, + active = this.active, + anchor = $(event.currentTarget), + tab = anchor.closest("li"), + clickedIsActive = tab[0] === active[0], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : this._getPanelForTab(tab), + toHide = !active.length ? $() : this._getPanelForTab(active), + eventData = { + oldTab: active, + oldPanel: toHide, + newTab: collapsing ? $() : tab, + newPanel: toShow + }; + + event.preventDefault(); + + if (tab.hasClass("ui-state-disabled") || + // tab is already loading + tab.hasClass("ui-tabs-loading") || + // can't switch durning an animation + this.running || + // click on active header, but not collapsible + (clickedIsActive && !options.collapsible) || + // allow canceling activation + (this._trigger("beforeActivate", event, eventData) === false)) { + return; + } + + options.active = collapsing ? false : this.tabs.index(tab); + + this.active = clickedIsActive ? $() : tab; + if (this.xhr) { + this.xhr.abort(); + } + + if (!toHide.length && !toShow.length) { + $.error("jQuery UI Tabs: Mismatching fragment identifier."); + } + + if (toShow.length) { + this.load(this.tabs.index(tab), event); + } + this._toggle(event, eventData); + }, + + // handles show/hide for selecting tabs + _toggle: function (event, eventData) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel; + + this.running = true; + + function complete() { + that.running = false; + that._trigger("activate", event, eventData); + } + + function show() { + eventData.newTab.closest("li").addClass("ui-tabs-active ui-state-active"); + + if (toShow.length && that.options.show) { + that._show(toShow, that.options.show, complete); + } else { + toShow.show(); + complete(); + } + } + + // start out by hiding, then showing, then completing + if (toHide.length && this.options.hide) { + this._hide(toHide, this.options.hide, function () { + eventData.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"); + show(); + }); + } else { + eventData.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"); + toHide.hide(); + show(); + } + + toHide.attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + eventData.oldTab.attr("aria-selected", "false"); + // If we're switching tabs, remove the old tab from the tab order. + // If we're opening from collapsed state, remove the previous tab from the tab order. + // If we're collapsing, then keep the collapsing tab in the tab order. + if (toShow.length && toHide.length) { + eventData.oldTab.attr("tabIndex", -1); + } else if (toShow.length) { + this.tabs.filter(function () { + return $(this).attr("tabIndex") === 0; + }) + .attr("tabIndex", -1); + } + + toShow.attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + eventData.newTab.attr({ + "aria-selected": "true", + tabIndex: 0 + }); + }, + + _activate: function (index) { + var anchor, + active = this._findActive(index); + + // trying to activate the already active panel + if (active[0] === this.active[0]) { + return; + } + + // trying to collapse, simulate a click on the current active header + if (!active.length) { + active = this.active; + } + + anchor = active.find(".ui-tabs-anchor")[0]; + this._eventHandler({ + target: anchor, + currentTarget: anchor, + preventDefault: $.noop + }); + }, + + _findActive: function (index) { + return index === false ? $() : this.tabs.eq(index); + }, + + _getIndex: function (index) { + // meta-function to give users option to provide a href string instead of a numerical index. + if (typeof index === "string") { + index = this.anchors.index(this.anchors.filter("[href$='" + index + "']")); + } + + return index; + }, + + _destroy: function () { + if (this.xhr) { + this.xhr.abort(); + } + + this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"); + + this.tablist + .removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all") + .removeAttr("role"); + + this.anchors + .removeClass("ui-tabs-anchor") + .removeAttr("role") + .removeAttr("tabIndex") + .removeUniqueId(); + + this.tabs.add(this.panels).each(function () { + if ($.data(this, "ui-tabs-destroy")) { + $(this).remove(); + } else { + $(this) + .removeClass("ui-state-default ui-state-active ui-state-disabled " + + "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel") + .removeAttr("tabIndex") + .removeAttr("aria-live") + .removeAttr("aria-busy") + .removeAttr("aria-selected") + .removeAttr("aria-labelledby") + .removeAttr("aria-hidden") + .removeAttr("aria-expanded") + .removeAttr("role"); + } + }); + + this.tabs.each(function () { + var li = $(this), + prev = li.data("ui-tabs-aria-controls"); + if (prev) { + li + .attr("aria-controls", prev) + .removeData("ui-tabs-aria-controls"); + } else { + li.removeAttr("aria-controls"); + } + }); + + this.panels.show(); + + if (this.options.heightStyle !== "content") { + this.panels.css("height", ""); + } + }, + + enable: function (index) { + var disabled = this.options.disabled; + if (disabled === false) { + return; + } + + if (index === undefined) { + disabled = false; + } else { + index = this._getIndex(index); + if ($.isArray(disabled)) { + disabled = $.map(disabled, function (num) { + return num !== index ? num : null; + }); + } else { + disabled = $.map(this.tabs, function (li, num) { + return num !== index ? num : null; + }); + } + } + this._setupDisabled(disabled); + }, + + disable: function (index) { + var disabled = this.options.disabled; + if (disabled === true) { + return; + } + + if (index === undefined) { + disabled = true; + } else { + index = this._getIndex(index); + if ($.inArray(index, disabled) !== -1) { + return; + } + if ($.isArray(disabled)) { + disabled = $.merge([index], disabled).sort(); + } else { + disabled = [index]; + } + } + this._setupDisabled(disabled); + }, + + load: function (index, event) { + index = this._getIndex(index); + var that = this, + tab = this.tabs.eq(index), + anchor = tab.find(".ui-tabs-anchor"), + panel = this._getPanelForTab(tab), + eventData = { + tab: tab, + panel: panel + }; + + // not remote + if (isLocal(anchor[0])) { + return; + } + + this.xhr = $.ajax(this._ajaxSettings(anchor, event, eventData)); + + // support: jQuery <1.8 + // jQuery <1.8 returns false if the request is canceled in beforeSend, + // but as of 1.8, $.ajax() always returns a jqXHR object. + if (this.xhr && this.xhr.statusText !== "canceled") { + tab.addClass("ui-tabs-loading"); + panel.attr("aria-busy", "true"); + + this.xhr + .success(function (response) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout(function () { + panel.html(response); + that._trigger("load", event, eventData); + }, 1); + }) + .complete(function (jqXHR, status) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout(function () { + if (status === "abort") { + that.panels.stop(false, true); + } + + tab.removeClass("ui-tabs-loading"); + panel.removeAttr("aria-busy"); + + if (jqXHR === that.xhr) { + delete that.xhr; + } + }, 1); + }); + } + }, + + _ajaxSettings: function (anchor, event, eventData) { + var that = this; + return { + url: anchor.attr("href"), + beforeSend: function (jqXHR, settings) { + return that._trigger("beforeLoad", event, + $.extend({jqXHR: jqXHR, ajaxSettings: settings}, eventData)); + } + }; + }, + + _getPanelForTab: function (tab) { + var id = $(tab).attr("aria-controls"); + return this.element.find(this._sanitizeSelector("#" + id)); + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/timepicker.js b/lib/web/jquery/ui-modules/timepicker.js new file mode 100644 index 000000000000..a488e46f5666 --- /dev/null +++ b/lib/web/jquery/ui-modules/timepicker.js @@ -0,0 +1,2170 @@ +/*! jQuery Timepicker Addon - v1.4.3 - 2013-11-30 +* http://trentrichardson.com/examples/timepicker +* Copyright (c) 2013 Trent Richardson; Licensed MIT */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define([ + "jquery", + "jquery-ui-modules/datepicker", + "jquery-ui-modules/slider" + ], factory); + } else { + factory(jQuery); + } +}(function ($) { + + /* + * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" + */ + $.ui.timepicker = $.ui.timepicker || {}; + if ($.ui.timepicker.version) { + return; + } + + /* + * Extend jQueryUI, get it started with our version number + */ + $.extend($.ui, { + timepicker: { + version: "1.4.3" + } + }); + + /* + * Timepicker manager. + * Use the singleton instance of this class, $.timepicker, to interact with the time picker. + * Settings for (groups of) time pickers are maintained in an instance object, + * allowing multiple different settings on the same page. + */ + var Timepicker = function () { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'HH:mm', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + microsecText: 'Microsecond', + timezoneText: 'Time Zone', + isRTL: false + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + showHour: null, + showMinute: null, + showSecond: null, + showMillisec: null, + showMicrosec: null, + showTimezone: null, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + stepMicrosec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + microsecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + microsecMax: 999, + minDateTime: null, + maxDateTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + microsecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + altTimeFormat: null, + altSeparator: null, + altTimeSuffix: null, + pickerTimeFormat: null, + pickerTimeSuffix: null, + showTimepicker: true, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null, + controlType: 'slider', + defaultValue: null, + parse: 'strict' + }; + $.extend(this._defaults, this.regional['']); + }; + + $.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + microsec_slider: null, + timezone_select: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + microsecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + microsecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + units: ['hour', 'minute', 'second', 'millisec', 'microsec'], + support: {}, + control: null, + + /* + * Override the default settings for all instances of the time picker. + * @param {Object} settings object - the new settings to use as defaults (anonymous object) + * @return {Object} the manager object + */ + setDefaults: function (settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* + * Create a new Timepicker instance + */ + _newInst: function ($input, opts) { + var tp_inst = new Timepicker(), + inlineSettings = {}, + fns = {}, + overrides, i; + + for (var attrName in this._defaults) { + if (this._defaults.hasOwnProperty(attrName)) { + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + + overrides = { + beforeShow: function (input, dp_inst) { + if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) { + return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); + } + }, + onChangeMonthYear: function (year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + tp_inst._updateDateTime(dp_inst); + if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) { + tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + } + }, + onClose: function (dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() !== '') { + tp_inst._updateDateTime(dp_inst); + } + if ($.isFunction(tp_inst._defaults.evnts.onClose)) { + tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); + } + } + }; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + fns[i] = opts[i] || null; + } + } + + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, opts, overrides, { + evnts: fns, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function (val) { + return val.toUpperCase(); + }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function (val) { + return val.toUpperCase(); + }); + + // detect which units are supported + tp_inst.support = detectSupport( + tp_inst._defaults.timeFormat + + (tp_inst._defaults.pickerTimeFormat ? tp_inst._defaults.pickerTimeFormat : '') + + (tp_inst._defaults.altTimeFormat ? tp_inst._defaults.altTimeFormat : '')); + + // controlType is string - key to our this._controls + if (typeof (tp_inst._defaults.controlType) === 'string') { + if (tp_inst._defaults.controlType === 'slider' && typeof ($.ui.slider) === 'undefined') { + tp_inst._defaults.controlType = 'select'; + } + tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; + } + // controlType is an object and must implement create, options, value methods + else { + tp_inst.control = tp_inst._defaults.controlType; + } + + // prep the timezone options + var timezoneList = [-720, -660, -600, -570, -540, -480, -420, -360, -300, -270, -240, -210, -180, -120, -60, + 0, 60, 120, 180, 210, 240, 270, 300, 330, 345, 360, 390, 420, 480, 525, 540, 570, 600, 630, 660, 690, 720, 765, 780, 840]; + if (tp_inst._defaults.timezoneList !== null) { + timezoneList = tp_inst._defaults.timezoneList; + } + var tzl = timezoneList.length, tzi = 0, tzv = null; + if (tzl > 0 && typeof timezoneList[0] !== 'object') { + for (; tzi < tzl; tzi++) { + tzv = timezoneList[tzi]; + timezoneList[tzi] = { + value: tzv, + label: $.timepicker.timezoneOffsetString(tzv, tp_inst.support.iso8601) + }; + } + } + tp_inst._defaults.timezoneList = timezoneList; + + // set the default units + tp_inst.timezone = tp_inst._defaults.timezone !== null ? $.timepicker.timezoneOffsetNumber(tp_inst._defaults.timezone) : + ((new Date()).getTimezoneOffset() * -1); + tp_inst.hour = tp_inst._defaults.hour < tp_inst._defaults.hourMin ? tp_inst._defaults.hourMin : + tp_inst._defaults.hour > tp_inst._defaults.hourMax ? tp_inst._defaults.hourMax : tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin ? tp_inst._defaults.minuteMin : + tp_inst._defaults.minute > tp_inst._defaults.minuteMax ? tp_inst._defaults.minuteMax : tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin ? tp_inst._defaults.secondMin : + tp_inst._defaults.second > tp_inst._defaults.secondMax ? tp_inst._defaults.secondMax : tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin ? tp_inst._defaults.millisecMin : + tp_inst._defaults.millisec > tp_inst._defaults.millisecMax ? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec; + tp_inst.microsec = tp_inst._defaults.microsec < tp_inst._defaults.microsecMin ? tp_inst._defaults.microsecMin : + tp_inst._defaults.microsec > tp_inst._defaults.microsecMax ? tp_inst._defaults.microsecMax : tp_inst._defaults.microsec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (tp_inst._defaults.altField) { + tp_inst.$altInput = $(tp_inst._defaults.altField).css({ + cursor: 'pointer' + }).focus(function () { + $input.trigger("focus"); + }); + } + + if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { + tp_inst._defaults.minDate = new Date(); + } + if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { + tp_inst._defaults.maxDate = new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + } + if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + } + if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + } + if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + } + tp_inst.$input.bind('focus', function () { + tp_inst._onFocus(); + }); + + return tp_inst; + }, + + /* + * add our sliders to the calendar + */ + _addTimePicker: function (dp_inst) { + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val(); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + }, + + /* + * parse the time string from input value or _setTime + */ + _parseTime: function (timeString, withDate) { + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + if (withDate || !this._defaults.timeOnly) { + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + try { + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); + if (!parseRes.timeObj) { + return false; + } + $.extend(this, parseRes.timeObj); + } catch (err) { + $.timepicker.log("Error parsing the date/time string: " + err + + "\ndate/time string = " + timeString + + "\ntimeFormat = " + this._defaults.timeFormat + + "\ndateFormat = " + dp_dateFormat); + return false; + } + return true; + } else { + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); + if (!timeObj) { + return false; + } + $.extend(this, timeObj); + return true; + } + }, + + /* + * generate and inject html for timepicker into ui datepicker + */ + _injectTimePicker: function () { + var $dp = this.inst.dpDiv, + o = this.inst.settings, + tp_inst = this, + litem = '', + uitem = '', + show = null, + max = {}, + gridSize = {}, + size = null, + i = 0, + l = 0; + + // Prevent displaying twice + if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { + var noDisplay = ' style="display:none;"', + html = '<div class="ui-timepicker-div' + (o.isRTL ? ' ui-timepicker-rtl' : '') + '"><dl>' + '<dt class="ui_tpicker_time_label"' + ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' + + '<dd class="ui_tpicker_time"' + ((o.showTime) ? '' : noDisplay) + '></dd>'; + + // Create the markup + for (i = 0, l = this.units.length; i < l; i++) { + litem = this.units[i]; + uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); + show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; + + // Added by Peter Medeiros: + // - Figure out what the hour/minute/second max should be based on the step values. + // - Example: if stepMinute is 15, then minMax is 45. + max[litem] = parseInt((o[litem + 'Max'] - ((o[litem + 'Max'] - o[litem + 'Min']) % o['step' + uitem])), 10); + gridSize[litem] = 0; + + html += '<dt class="ui_tpicker_' + litem + '_label"' + (show ? '' : noDisplay) + '>' + o[litem + 'Text'] + '</dt>' + + '<dd class="ui_tpicker_' + litem + '"><div class="ui_tpicker_' + litem + '_slider"' + (show ? '' : noDisplay) + '></div>'; + + if (show && o[litem + 'Grid'] > 0) { + html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>'; + + if (litem === 'hour') { + for (var h = o[litem + 'Min']; h <= max[litem]; h += parseInt(o[litem + 'Grid'], 10)) { + gridSize[litem]++; + var tmph = $.datepicker.formatTime(this.support.ampm ? 'hht' : 'HH', {hour: h}, o); + html += '<td data-for="' + litem + '">' + tmph + '</td>'; + } + } else { + for (var m = o[litem + 'Min']; m <= max[litem]; m += parseInt(o[litem + 'Grid'], 10)) { + gridSize[litem]++; + html += '<td data-for="' + litem + '">' + ((m < 10) ? '0' : '') + m + '</td>'; + } + } + + html += '</tr></table></div>'; + } + html += '</dd>'; + } + + // Timezone + var showTz = o.showTimezone !== null ? o.showTimezone : this.support.timezone; + html += '<dt class="ui_tpicker_timezone_label"' + (showTz ? '' : noDisplay) + '>' + o.timezoneText + '</dt>'; + html += '<dd class="ui_tpicker_timezone" ' + (showTz ? '' : noDisplay) + '></dd>'; + + // Create the elements from string + html += '</dl></div>'; + var $tp = $(html); + + // if we only want time picker... + if (o.timeOnly === true) { + $tp.prepend('<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' + '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' + '</div>'); + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); + } + + // add sliders, adjust grids, add events + for (i = 0, l = tp_inst.units.length; i < l; i++) { + litem = tp_inst.units[i]; + uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); + show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; + + // add the slider + tp_inst[litem + '_slider'] = tp_inst.control.create(tp_inst, $tp.find('.ui_tpicker_' + litem + '_slider'), litem, tp_inst[litem], o[litem + 'Min'], max[litem], o['step' + uitem]); + + // adjust the grid and add click event + if (show && o[litem + 'Grid'] > 0) { + size = 100 * gridSize[litem] * o[litem + 'Grid'] / (max[litem] - o[litem + 'Min']); + $tp.find('.ui_tpicker_' + litem + ' table').css({ + width: size + "%", + marginLeft: o.isRTL ? '0' : ((size / (-2 * gridSize[litem])) + "%"), + marginRight: o.isRTL ? ((size / (-2 * gridSize[litem])) + "%") : '0', + borderCollapse: 'collapse' + }).find("td").click(function (e) { + var $t = $(this), + h = $t.html(), + n = parseInt(h.replace(/[^0-9]/g), 10), + ap = h.replace(/[^apm]/ig), + f = $t.data('for'); // loses scope, so we use data-for + + if (f === 'hour') { + if (ap.indexOf('p') !== -1 && n < 12) { + n += 12; + } else { + if (ap.indexOf('a') !== -1 && n === 12) { + n = 0; + } + } + } + + tp_inst.control.value(tp_inst, tp_inst[f + '_slider'], litem, n); + + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }).css({ + cursor: 'pointer', + width: (100 / gridSize[litem]) + '%', + textAlign: 'center', + overflow: 'hidden' + }); + } // end if grid > 0 + } // end for loop + + // Add timezone options + this.timezone_select = $tp.find('.ui_tpicker_timezone').append('<select></select>').find("select"); + $.fn.append.apply(this.timezone_select, + $.map(o.timezoneList, function (val, idx) { + return $("<option />").val(typeof val === "object" ? val.value : val).text(typeof val === "object" ? val.label : val); + })); + if (typeof (this.timezone) !== "undefined" && this.timezone !== null && this.timezone !== "") { + var local_timezone = (new Date(this.inst.selectedYear, this.inst.selectedMonth, this.inst.selectedDay, 12)).getTimezoneOffset() * -1; + if (local_timezone === this.timezone) { + selectLocalTimezone(tp_inst); + } else { + this.timezone_select.val(this.timezone); + } + } else { + if (typeof (this.hour) !== "undefined" && this.hour !== null && this.hour !== "") { + this.timezone_select.val(o.timezone); + } else { + selectLocalTimezone(tp_inst); + } + } + this.timezone_select.change(function () { + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }); + // End timezone options + + // inject timepicker into datepicker + var $buttonPanel = $dp.find('.ui-datepicker-buttonpane'); + if ($buttonPanel.length) { + $buttonPanel.before($tp); + } else { + $dp.append($tp); + } + + this.$timeObj = $tp.find('.ui_tpicker_time'); + + if (this.inst !== null) { + var timeDefined = this.timeDefined; + this._onTimeChange(); + this.timeDefined = timeDefined; + } + + // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/ + if (this._defaults.addSliderAccess) { + var sliderAccessArgs = this._defaults.sliderAccessArgs, + rtl = this._defaults.isRTL; + sliderAccessArgs.isRTL = rtl; + + setTimeout(function () { // fix for inline mode + if ($tp.find('.ui-slider-access').length === 0) { + $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs); + + // fix any grids since sliders are shorter + var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true); + if (sliderAccessWidth) { + $tp.find('table:visible').each(function () { + var $g = $(this), + oldWidth = $g.outerWidth(), + oldMarginLeft = $g.css(rtl ? 'marginRight' : 'marginLeft').toString().replace('%', ''), + newWidth = oldWidth - sliderAccessWidth, + newMarginLeft = ((oldMarginLeft * newWidth) / oldWidth) + '%', + css = {width: newWidth, marginRight: 0, marginLeft: 0}; + css[rtl ? 'marginRight' : 'marginLeft'] = newMarginLeft; + $g.css(css); + }); + } + } + }, 10); + } + // end slideAccess integration + + tp_inst._limitMinMaxDateTime(this.inst, true); + } + }, + + /* + * This function tries to limit the ability to go outside the + * min/max date range + */ + _limitMinMaxDateTime: function (dp_inst, adjustSliders) { + var o = this._defaults, + dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay); + + if (!this._defaults.showTimepicker) { + return; + } // No time so nothing to check here + + if ($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date) { + var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'), + minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0); + + if (this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null || this.microsecMinOriginal === null) { + this.hourMinOriginal = o.hourMin; + this.minuteMinOriginal = o.minuteMin; + this.secondMinOriginal = o.secondMin; + this.millisecMinOriginal = o.millisecMin; + this.microsecMinOriginal = o.microsecMin; + } + + if (dp_inst.settings.timeOnly || minDateTimeDate.getTime() === dp_date.getTime()) { + this._defaults.hourMin = minDateTime.getHours(); + if (this.hour <= this._defaults.hourMin) { + this.hour = this._defaults.hourMin; + this._defaults.minuteMin = minDateTime.getMinutes(); + if (this.minute <= this._defaults.minuteMin) { + this.minute = this._defaults.minuteMin; + this._defaults.secondMin = minDateTime.getSeconds(); + if (this.second <= this._defaults.secondMin) { + this.second = this._defaults.secondMin; + this._defaults.millisecMin = minDateTime.getMilliseconds(); + if (this.millisec <= this._defaults.millisecMin) { + this.millisec = this._defaults.millisecMin; + this._defaults.microsecMin = minDateTime.getMicroseconds(); + } else { + if (this.microsec < this._defaults.microsecMin) { + this.microsec = this._defaults.microsecMin; + } + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.minuteMin = this.minuteMinOriginal; + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.hourMin = this.hourMinOriginal; + this._defaults.minuteMin = this.minuteMinOriginal; + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } + + if ($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date) { + var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'), + maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0); + + if (this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null || this.millisecMaxOriginal === null) { + this.hourMaxOriginal = o.hourMax; + this.minuteMaxOriginal = o.minuteMax; + this.secondMaxOriginal = o.secondMax; + this.millisecMaxOriginal = o.millisecMax; + this.microsecMaxOriginal = o.microsecMax; + } + + if (dp_inst.settings.timeOnly || maxDateTimeDate.getTime() === dp_date.getTime()) { + this._defaults.hourMax = maxDateTime.getHours(); + if (this.hour >= this._defaults.hourMax) { + this.hour = this._defaults.hourMax; + this._defaults.minuteMax = maxDateTime.getMinutes(); + if (this.minute >= this._defaults.minuteMax) { + this.minute = this._defaults.minuteMax; + this._defaults.secondMax = maxDateTime.getSeconds(); + if (this.second >= this._defaults.secondMax) { + this.second = this._defaults.secondMax; + this._defaults.millisecMax = maxDateTime.getMilliseconds(); + if (this.millisec >= this._defaults.millisecMax) { + this.millisec = this._defaults.millisecMax; + this._defaults.microsecMax = maxDateTime.getMicroseconds(); + } else { + if (this.microsec > this._defaults.microsecMax) { + this.microsec = this._defaults.microsecMax; + } + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.minuteMax = this.minuteMaxOriginal; + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.hourMax = this.hourMaxOriginal; + this._defaults.minuteMax = this.minuteMaxOriginal; + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } + + if (adjustSliders !== undefined && adjustSliders === true) { + var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)), 10), + minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)), 10), + secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)), 10), + millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)), 10), + microsecMax = parseInt((this._defaults.microsecMax - ((this._defaults.microsecMax - this._defaults.microsecMin) % this._defaults.stepMicrosec)), 10); + + if (this.hour_slider) { + this.control.options(this, this.hour_slider, 'hour', {min: this._defaults.hourMin, max: hourMax}); + this.control.value(this, this.hour_slider, 'hour', this.hour - (this.hour % this._defaults.stepHour)); + } + if (this.minute_slider) { + this.control.options(this, this.minute_slider, 'minute', { + min: this._defaults.minuteMin, + max: minMax + }); + this.control.value(this, this.minute_slider, 'minute', this.minute - (this.minute % this._defaults.stepMinute)); + } + if (this.second_slider) { + this.control.options(this, this.second_slider, 'second', { + min: this._defaults.secondMin, + max: secMax + }); + this.control.value(this, this.second_slider, 'second', this.second - (this.second % this._defaults.stepSecond)); + } + if (this.millisec_slider) { + this.control.options(this, this.millisec_slider, 'millisec', { + min: this._defaults.millisecMin, + max: millisecMax + }); + this.control.value(this, this.millisec_slider, 'millisec', this.millisec - (this.millisec % this._defaults.stepMillisec)); + } + if (this.microsec_slider) { + this.control.options(this, this.microsec_slider, 'microsec', { + min: this._defaults.microsecMin, + max: microsecMax + }); + this.control.value(this, this.microsec_slider, 'microsec', this.microsec - (this.microsec % this._defaults.stepMicrosec)); + } + } + + }, + + /* + * when a slider moves, set the internal time... + * on time change is also called when the time is updated in the text field + */ + _onTimeChange: function () { + if (!this._defaults.showTimepicker) { + return; + } + var hour = (this.hour_slider) ? this.control.value(this, this.hour_slider, 'hour') : false, + minute = (this.minute_slider) ? this.control.value(this, this.minute_slider, 'minute') : false, + second = (this.second_slider) ? this.control.value(this, this.second_slider, 'second') : false, + millisec = (this.millisec_slider) ? this.control.value(this, this.millisec_slider, 'millisec') : false, + microsec = (this.microsec_slider) ? this.control.value(this, this.microsec_slider, 'microsec') : false, + timezone = (this.timezone_select) ? this.timezone_select.val() : false, + o = this._defaults, + pickerTimeFormat = o.pickerTimeFormat || o.timeFormat, + pickerTimeSuffix = o.pickerTimeSuffix || o.timeSuffix; + + if (typeof (hour) === 'object') { + hour = false; + } + if (typeof (minute) === 'object') { + minute = false; + } + if (typeof (second) === 'object') { + second = false; + } + if (typeof (millisec) === 'object') { + millisec = false; + } + if (typeof (microsec) === 'object') { + microsec = false; + } + if (typeof (timezone) === 'object') { + timezone = false; + } + + if (hour !== false) { + hour = parseInt(hour, 10); + } + if (minute !== false) { + minute = parseInt(minute, 10); + } + if (second !== false) { + second = parseInt(second, 10); + } + if (millisec !== false) { + millisec = parseInt(millisec, 10); + } + if (microsec !== false) { + microsec = parseInt(microsec, 10); + } + if (timezone !== false) { + timezone = timezone.toString(); + } + + var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0]; + + // If the update was done in the input field, the input field should not be updated. + // If the update was done using the sliders, update the input field. + var hasChanged = ( + hour !== parseInt(this.hour, 10) || // sliders should all be numeric + minute !== parseInt(this.minute, 10) || + second !== parseInt(this.second, 10) || + millisec !== parseInt(this.millisec, 10) || + microsec !== parseInt(this.microsec, 10) || + (this.ampm.length > 0 && (hour < 12) !== ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1)) || + (this.timezone !== null && timezone !== this.timezone.toString()) // could be numeric or "EST" format, so use toString() + ); + + if (hasChanged) { + + if (hour !== false) { + this.hour = hour; + } + if (minute !== false) { + this.minute = minute; + } + if (second !== false) { + this.second = second; + } + if (millisec !== false) { + this.millisec = millisec; + } + if (microsec !== false) { + this.microsec = microsec; + } + if (timezone !== false) { + this.timezone = timezone; + } + + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + this._limitMinMaxDateTime(this.inst, true); + } + if (this.support.ampm) { + this.ampm = ampm; + } + + // Updates the time within the timepicker + this.formattedTime = $.datepicker.formatTime(o.timeFormat, this, o); + if (this.$timeObj) { + if (pickerTimeFormat === o.timeFormat) { + this.$timeObj.text(this.formattedTime + pickerTimeSuffix); + } else { + this.$timeObj.text($.datepicker.formatTime(pickerTimeFormat, this, o) + pickerTimeSuffix); + } + } + + this.timeDefined = true; + if (hasChanged) { + this._updateDateTime(); + this.$input.focus(); + } + }, + + /* + * call custom onSelect. + * bind to sliders slidestop, and grid click. + */ + _onSelectHandler: function () { + var onSelect = this._defaults.onSelect || this.inst.settings.onSelect; + var inputEl = this.$input ? this.$input[0] : null; + if (onSelect && inputEl) { + onSelect.apply(inputEl, [this.formattedDateTime, this]); + } + }, + + /* + * update our input with the new date time.. + */ + _updateDateTime: function (dp_inst) { + dp_inst = this.inst || dp_inst; + var dtTmp = (dp_inst.currentYear > 0 ? + new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay) : + new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), + dt = $.datepicker._daylightSavingAdjust(dtTmp), + //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), + //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay)), + dateFmt = $.datepicker._get(dp_inst, 'dateFormat'), + formatCfg = $.datepicker._getFormatConfig(dp_inst), + timeAvailable = dt !== null && this.timeDefined; + this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg); + var formattedDateTime = this.formattedDate; + + // if a slider was changed but datepicker doesn't have a value yet, set it + if (dp_inst.lastVal === "") { + dp_inst.currentYear = dp_inst.selectedYear; + dp_inst.currentMonth = dp_inst.selectedMonth; + dp_inst.currentDay = dp_inst.selectedDay; + } + + /* + * remove following lines to force every changes in date picker to change the input value + * Bug descriptions: when an input field has a default value, and click on the field to pop up the date picker. + * If the user manually empty the value in the input field, the date picker will never change selected value. + */ + //if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0)) { + // return; + //} + + if (this._defaults.timeOnly === true) { + formattedDateTime = this.formattedTime; + } else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) { + formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix; + } + + this.formattedDateTime = formattedDateTime; + + if (!this._defaults.showTimepicker) { + this.$input.val(this.formattedDate); + } else if (this.$altInput && this._defaults.timeOnly === false && this._defaults.altFieldTimeOnly === true) { + this.$altInput.val(this.formattedTime); + this.$input.val(this.formattedDate); + } else if (this.$altInput) { + this.$input.val(formattedDateTime); + var altFormattedDateTime = '', + altSeparator = this._defaults.altSeparator ? this._defaults.altSeparator : this._defaults.separator, + altTimeSuffix = this._defaults.altTimeSuffix ? this._defaults.altTimeSuffix : this._defaults.timeSuffix; + + if (!this._defaults.timeOnly) { + if (this._defaults.altFormat) { + altFormattedDateTime = $.datepicker.formatDate(this._defaults.altFormat, (dt === null ? new Date() : dt), formatCfg); + } else { + altFormattedDateTime = this.formattedDate; + } + + if (altFormattedDateTime) { + altFormattedDateTime += altSeparator; + } + } + + if (this._defaults.altTimeFormat) { + altFormattedDateTime += $.datepicker.formatTime(this._defaults.altTimeFormat, this, this._defaults) + altTimeSuffix; + } else { + altFormattedDateTime += this.formattedTime + altTimeSuffix; + } + this.$altInput.val(altFormattedDateTime); + } else { + this.$input.val(formattedDateTime); + } + + this.$input.trigger("change"); + }, + + _onFocus: function () { + if (!this.$input.val() && this._defaults.defaultValue) { + this.$input.val(this._defaults.defaultValue); + var inst = $.datepicker._getInst(this.$input.get(0)), + tp_inst = $.datepicker._get(inst, 'timepicker'); + if (tp_inst) { + if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) { + try { + $.datepicker._updateDatepicker(inst); + } catch (err) { + $.timepicker.log(err); + } + } + } + } + }, + + /* + * Small abstraction to control types + * We can add more, just be sure to follow the pattern: create, options, value + */ + _controls: { + // slider methods + slider: { + create: function (tp_inst, obj, unit, val, min, max, step) { + var rtl = tp_inst._defaults.isRTL; // if rtl go -60->0 instead of 0->60 + return obj.prop('slide', null).slider({ + orientation: "horizontal", + value: rtl ? val * -1 : val, + min: rtl ? max * -1 : min, + max: rtl ? min * -1 : max, + step: step, + slide: function (event, ui) { + tp_inst.control.value(tp_inst, $(this), unit, rtl ? ui.value * -1 : ui.value); + tp_inst._onTimeChange(); + }, + stop: function (event, ui) { + tp_inst._onSelectHandler(); + } + }); + }, + options: function (tp_inst, obj, unit, opts, val) { + if (tp_inst._defaults.isRTL) { + if (typeof (opts) === 'string') { + if (opts === 'min' || opts === 'max') { + if (val !== undefined) { + return obj.slider(opts, val * -1); + } + return Math.abs(obj.slider(opts)); + } + return obj.slider(opts); + } + var min = opts.min, + max = opts.max; + opts.min = opts.max = null; + if (min !== undefined) { + opts.max = min * -1; + } + if (max !== undefined) { + opts.min = max * -1; + } + return obj.slider(opts); + } + if (typeof (opts) === 'string' && val !== undefined) { + return obj.slider(opts, val); + } + return obj.slider(opts); + }, + value: function (tp_inst, obj, unit, val) { + if (tp_inst._defaults.isRTL) { + if (val !== undefined) { + return obj.slider('value', val * -1); + } + return Math.abs(obj.slider('value')); + } + if (val !== undefined) { + return obj.slider('value', val); + } + return obj.slider('value'); + } + }, + // select methods + select: { + create: function (tp_inst, obj, unit, val, min, max, step) { + var sel = '<select class="ui-timepicker-select" data-unit="' + unit + '" data-min="' + min + '" data-max="' + max + '" data-step="' + step + '">', + format = tp_inst._defaults.pickerTimeFormat || tp_inst._defaults.timeFormat; + + for (var i = min; i <= max; i += step) { + sel += '<option value="' + i + '"' + (i === val ? ' selected' : '') + '>'; + if (unit === 'hour') { + sel += $.datepicker.formatTime($.trim(format.replace(/[^ht ]/ig, '')), {hour: i}, tp_inst._defaults); + } else if (unit === 'millisec' || unit === 'microsec' || i >= 10) { + sel += i; + } else { + sel += '0' + i.toString(); + } + sel += '</option>'; + } + sel += '</select>'; + + obj.children('select').remove(); + + $(sel).appendTo(obj).change(function (e) { + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }); + + return obj; + }, + options: function (tp_inst, obj, unit, opts, val) { + var o = {}, + $t = obj.children('select'); + if (typeof (opts) === 'string') { + if (val === undefined) { + return $t.data(opts); + } + o[opts] = val; + } else { + o = opts; + } + return tp_inst.control.create(tp_inst, obj, $t.data('unit'), $t.val(), o.min || $t.data('min'), o.max || $t.data('max'), o.step || $t.data('step')); + }, + value: function (tp_inst, obj, unit, val) { + var $t = obj.children('select'); + if (val !== undefined) { + return $t.val(val); + } + return $t.val(); + } + } + } // end _controls + + }); + + $.fn.extend({ + /* + * shorthand just to use timepicker. + */ + timepicker: function (o) { + o = o || {}; + var tmp_args = Array.prototype.slice.call(arguments); + + if (typeof o === 'object') { + tmp_args[0] = $.extend(o, { + timeOnly: true + }); + } + + return $(this).each(function () { + $.fn.datetimepicker.apply($(this), tmp_args); + }); + }, + + /* + * extend timepicker to datepicker + */ + datetimepicker: function (o) { + o = o || {}; + var tmp_args = arguments; + + if (typeof (o) === 'string') { + if (o === 'getDate') { + return $.fn.datepicker.apply($(this[0]), tmp_args); + } else { + return this.each(function () { + var $t = $(this); + $t.datepicker.apply($t, tmp_args); + }); + } + } else { + return this.each(function () { + var $t = $(this); + $t.datepicker($.timepicker._newInst($t, o)._defaults); + }); + } + } + }); + + /* + * Public Utility to parse date and time + */ + $.datepicker.parseDateTime = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { + var parseRes = parseDateTimeInternal(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings); + if (parseRes.timeObj) { + var t = parseRes.timeObj; + parseRes.date.setHours(t.hour, t.minute, t.second, t.millisec); + parseRes.date.setMicroseconds(t.microsec); + } + + return parseRes.date; + }; + + /* + * Public utility to parse time + */ + $.datepicker.parseTime = function (timeFormat, timeString, options) { + var o = extendRemove(extendRemove({}, $.timepicker._defaults), options || {}), + iso8601 = (timeFormat.replace(/\'.*?\'/g, '').indexOf('Z') !== -1); + + // Strict parse requires the timeString to match the timeFormat exactly + var strictParse = function (f, s, o) { + + // pattern for standard and localized AM/PM markers + var getPatternAmpm = function (amNames, pmNames) { + var markers = []; + if (amNames) { + $.merge(markers, amNames); + } + if (pmNames) { + $.merge(markers, pmNames); + } + markers = $.map(markers, function (val) { + return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); + }); + return '(' + markers.join('|') + ')?'; + }; + + // figure out position of time elements.. cause js cant do named captures + var getFormatPositions = function (timeFormat) { + var finds = timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|c{1}|t{1,2}|z|'.*?')/g), + orders = { + h: -1, + m: -1, + s: -1, + l: -1, + c: -1, + t: -1, + z: -1 + }; + + if (finds) { + for (var i = 0; i < finds.length; i++) { + if (orders[finds[i].toString().charAt(0)] === -1) { + orders[finds[i].toString().charAt(0)] = i + 1; + } + } + } + return orders; + }; + + var regstr = '^' + f.toString() + .replace(/([hH]{1,2}|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) { + var ml = match.length; + switch (match.charAt(0).toLowerCase()) { + case 'h': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 'm': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 's': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 'l': + return '(\\d?\\d?\\d)'; + case 'c': + return '(\\d?\\d?\\d)'; + case 'z': + return '(z|[-+]\\d\\d:?\\d\\d|\\S+)?'; + case 't': + return getPatternAmpm(o.amNames, o.pmNames); + default: // literal escaped in quotes + return '(' + match.replace(/\'/g, "").replace(/(\.|\$|\^|\\|\/|\(|\)|\[|\]|\?|\+|\*)/g, function (m) { + return "\\" + m; + }) + ')?'; + } + }) + .replace(/\s/g, '\\s?') + + o.timeSuffix + '$', + order = getFormatPositions(f), + ampm = '', + treg; + + treg = s.match(new RegExp(regstr, 'i')); + + var resTime = { + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0 + }; + + if (treg) { + if (order.t !== -1) { + if (treg[order.t] === undefined || treg[order.t].length === 0) { + ampm = ''; + resTime.ampm = ''; + } else { + ampm = $.inArray(treg[order.t].toUpperCase(), o.amNames) !== -1 ? 'AM' : 'PM'; + resTime.ampm = o[ampm === 'AM' ? 'amNames' : 'pmNames'][0]; + } + } + + if (order.h !== -1) { + if (ampm === 'AM' && treg[order.h] === '12') { + resTime.hour = 0; // 12am = 0 hour + } else { + if (ampm === 'PM' && treg[order.h] !== '12') { + resTime.hour = parseInt(treg[order.h], 10) + 12; // 12pm = 12 hour, any other pm = hour + 12 + } else { + resTime.hour = Number(treg[order.h]); + } + } + } + + if (order.m !== -1) { + resTime.minute = Number(treg[order.m]); + } + if (order.s !== -1) { + resTime.second = Number(treg[order.s]); + } + if (order.l !== -1) { + resTime.millisec = Number(treg[order.l]); + } + if (order.c !== -1) { + resTime.microsec = Number(treg[order.c]); + } + if (order.z !== -1 && treg[order.z] !== undefined) { + resTime.timezone = $.timepicker.timezoneOffsetNumber(treg[order.z]); + } + + + return resTime; + } + return false; + };// end strictParse + + // First try JS Date, if that fails, use strictParse + var looseParse = function (f, s, o) { + try { + var d = new Date('2012-01-01 ' + s); + if (isNaN(d.getTime())) { + d = new Date('2012-01-01T' + s); + if (isNaN(d.getTime())) { + d = new Date('01/01/2012 ' + s); + if (isNaN(d.getTime())) { + throw "Unable to parse time with native Date: " + s; + } + } + } + + return { + hour: d.getHours(), + minute: d.getMinutes(), + second: d.getSeconds(), + millisec: d.getMilliseconds(), + microsec: d.getMicroseconds(), + timezone: d.getTimezoneOffset() * -1 + }; + } catch (err) { + try { + return strictParse(f, s, o); + } catch (err2) { + $.timepicker.log("Unable to parse \ntimeString: " + s + "\ntimeFormat: " + f); + } + } + return false; + }; // end looseParse + + if (typeof o.parse === "function") { + return o.parse(timeFormat, timeString, o); + } + if (o.parse === 'loose') { + return looseParse(timeFormat, timeString, o); + } + return strictParse(timeFormat, timeString, o); + }; + + /** + * Public utility to format the time + * @param {string} format format of the time + * @param {Object} time Object not a Date for timezones + * @param {Object} [options] essentially the regional[].. amNames, pmNames, ampm + * @returns {string} the formatted time + */ + $.datepicker.formatTime = function (format, time, options) { + options = options || {}; + options = $.extend({}, $.timepicker._defaults, options); + time = $.extend({ + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null + }, time); + + var tmptime = format, + ampmName = options.amNames[0], + hour = parseInt(time.hour, 10); + + if (hour > 11) { + ampmName = options.pmNames[0]; + } + + tmptime = tmptime.replace(/(?:HH?|hh?|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) { + switch (match) { + case 'HH': + return ('0' + hour).slice(-2); + case 'H': + return hour; + case 'hh': + return ('0' + convert24to12(hour)).slice(-2); + case 'h': + return convert24to12(hour); + case 'mm': + return ('0' + time.minute).slice(-2); + case 'm': + return time.minute; + case 'ss': + return ('0' + time.second).slice(-2); + case 's': + return time.second; + case 'l': + return ('00' + time.millisec).slice(-3); + case 'c': + return ('00' + time.microsec).slice(-3); + case 'z': + return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, false); + case 'Z': + return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, true); + case 'T': + return ampmName.charAt(0).toUpperCase(); + case 'TT': + return ampmName.toUpperCase(); + case 't': + return ampmName.charAt(0).toLowerCase(); + case 'tt': + return ampmName.toLowerCase(); + default: + return match.replace(/'/g, ""); + } + }); + + return tmptime; + }; + + /* + * the bad hack :/ override datepicker so it doesn't close on select + // inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378 + */ + $.datepicker._base_selectDate = $.datepicker._selectDate; + $.datepicker._selectDate = function (id, dateStr) { + var inst = this._getInst($(id)[0]), + tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + tp_inst._limitMinMaxDateTime(inst, true); + inst.inline = inst.stay_open = true; + //This way the onSelect handler called from calendarpicker get the full dateTime + this._base_selectDate(id, dateStr); + inst.inline = inst.stay_open = false; + this._notifyChange(inst); + this._updateDatepicker(inst); + } else { + this._base_selectDate(id, dateStr); + } + }; + + /* + * second bad hack :/ override datepicker so it triggers an event when changing the input field + * and does not redraw the datepicker on every selectDate event + */ + $.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker; + $.datepicker._updateDatepicker = function (inst) { + + // don't popup the datepicker if there is another instance already opened + var input = inst.input[0]; + if ($.datepicker._curInst && $.datepicker._curInst !== inst && $.datepicker._datepickerShowing && $.datepicker._lastInput !== input) { + return; + } + + if (typeof (inst.stay_open) !== 'boolean' || inst.stay_open === false) { + + this._base_updateDatepicker(inst); + + // Reload the time control when changing something in the input text field. + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + tp_inst._addTimePicker(inst); + } + } + }; + + /* + * third bad hack :/ override datepicker so it allows spaces and colon in the input field + */ + $.datepicker._base_doKeyPress = $.datepicker._doKeyPress; + $.datepicker._doKeyPress = function (event) { + var inst = $.datepicker._getInst(event.target), + tp_inst = $.datepicker._get(inst, 'timepicker'); + + if (tp_inst) { + if ($.datepicker._get(inst, 'constrainInput')) { + var ampm = tp_inst.support.ampm, + tz = tp_inst._defaults.showTimezone !== null ? tp_inst._defaults.showTimezone : tp_inst.support.timezone, + dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')), + datetimeChars = tp_inst._defaults.timeFormat.toString() + .replace(/[hms]/g, '') + .replace(/TT/g, ampm ? 'APM' : '') + .replace(/Tt/g, ampm ? 'AaPpMm' : '') + .replace(/tT/g, ampm ? 'AaPpMm' : '') + .replace(/T/g, ampm ? 'AP' : '') + .replace(/tt/g, ampm ? 'apm' : '') + .replace(/t/g, ampm ? 'ap' : '') + + " " + tp_inst._defaults.separator + + tp_inst._defaults.timeSuffix + + (tz ? tp_inst._defaults.timezoneList.join('') : '') + + (tp_inst._defaults.amNames.join('')) + (tp_inst._defaults.pmNames.join('')) + + dateChars, + chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode); + return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1); + } + } + + return $.datepicker._base_doKeyPress(event); + }; + + /* + * Fourth bad hack :/ override _updateAlternate function used in inline mode to init altField + * Update any alternate field to synchronise with the main field. + */ + $.datepicker._base_updateAlternate = $.datepicker._updateAlternate; + $.datepicker._updateAlternate = function (inst) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var altField = tp_inst._defaults.altField; + if (altField) { // update alternate field too + var altFormat = tp_inst._defaults.altFormat || tp_inst._defaults.dateFormat, + date = this._getDate(inst), + formatCfg = $.datepicker._getFormatConfig(inst), + altFormattedDateTime = '', + altSeparator = tp_inst._defaults.altSeparator ? tp_inst._defaults.altSeparator : tp_inst._defaults.separator, + altTimeSuffix = tp_inst._defaults.altTimeSuffix ? tp_inst._defaults.altTimeSuffix : tp_inst._defaults.timeSuffix, + altTimeFormat = tp_inst._defaults.altTimeFormat !== null ? tp_inst._defaults.altTimeFormat : tp_inst._defaults.timeFormat; + + altFormattedDateTime += $.datepicker.formatTime(altTimeFormat, tp_inst, tp_inst._defaults) + altTimeSuffix; + if (!tp_inst._defaults.timeOnly && !tp_inst._defaults.altFieldTimeOnly && date !== null) { + if (tp_inst._defaults.altFormat) { + altFormattedDateTime = $.datepicker.formatDate(tp_inst._defaults.altFormat, date, formatCfg) + altSeparator + altFormattedDateTime; + } else { + altFormattedDateTime = tp_inst.formattedDate + altSeparator + altFormattedDateTime; + } + } + $(altField).val(altFormattedDateTime); + } + } else { + $.datepicker._base_updateAlternate(inst); + } + }; + + /* + * Override key up event to sync manual input changes. + */ + $.datepicker._base_doKeyUp = $.datepicker._doKeyUp; + $.datepicker._doKeyUp = function (event) { + var inst = $.datepicker._getInst(event.target), + tp_inst = $.datepicker._get(inst, 'timepicker'); + + if (tp_inst) { + if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) { + try { + $.datepicker._updateDatepicker(inst); + } catch (err) { + $.timepicker.log(err); + } + } + } + + return $.datepicker._base_doKeyUp(event); + }; + + /* + * override "Today" button to also grab the time. + */ + $.datepicker._base_gotoToday = $.datepicker._gotoToday; + $.datepicker._gotoToday = function (id) { + var inst = this._getInst($(id)[0]), + $dp = inst.dpDiv; + this._base_gotoToday(id); + var tp_inst = this._get(inst, 'timepicker'); + selectLocalTimezone(tp_inst); + var now = new Date(); + this._setTime(inst, now); + $('.ui-datepicker-today', $dp).click(); + }; + + /* + * Disable & enable the Time in the datetimepicker + */ + $.datepicker._disableTimepickerDatepicker = function (target) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] + if (tp_inst) { + inst.settings.showTimepicker = false; + tp_inst._defaults.showTimepicker = false; + tp_inst._updateDateTime(inst); + } + }; + + $.datepicker._enableTimepickerDatepicker = function (target) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] + if (tp_inst) { + inst.settings.showTimepicker = true; + tp_inst._defaults.showTimepicker = true; + tp_inst._addTimePicker(inst); // Could be disabled on page load + tp_inst._updateDateTime(inst); + } + }; + + /* + * Create our own set time function + */ + $.datepicker._setTime = function (inst, date) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var defaults = tp_inst._defaults; + + // calling _setTime with no date sets time to defaults + tp_inst.hour = date ? date.getHours() : defaults.hour; + tp_inst.minute = date ? date.getMinutes() : defaults.minute; + tp_inst.second = date ? date.getSeconds() : defaults.second; + tp_inst.millisec = date ? date.getMilliseconds() : defaults.millisec; + tp_inst.microsec = date ? date.getMicroseconds() : defaults.microsec; + + //check if within min/max times.. + tp_inst._limitMinMaxDateTime(inst, true); + + tp_inst._onTimeChange(); + tp_inst._updateDateTime(inst); + } + }; + + /* + * Create new public method to set only time, callable as $().datepicker('setTime', date) + */ + $.datepicker._setTimeDatepicker = function (target, date, withDate) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + this._setDateFromField(inst); + var tp_date; + if (date) { + if (typeof date === "string") { + tp_inst._parseTime(date, withDate); + tp_date = new Date(); + tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); + tp_date.setMicroseconds(tp_inst.microsec); + } else { + tp_date = new Date(date.getTime()); + tp_date.setMicroseconds(date.getMicroseconds()); + } + if (tp_date.toString() === 'Invalid Date') { + tp_date = undefined; + } + this._setTime(inst, tp_date); + } + } + + }; + + /* + * override setDate() to allow setting time too within Date object + */ + $.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker; + $.datepicker._setDateDatepicker = function (target, date) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + if (typeof (date) === 'string') { + date = new Date(date); + if (!date.getTime()) { + $.timepicker.log("Error creating Date object from string."); + } + } + + var tp_inst = this._get(inst, 'timepicker'); + var tp_date; + if (date instanceof Date) { + tp_date = new Date(date.getTime()); + tp_date.setMicroseconds(date.getMicroseconds()); + } else { + tp_date = date; + } + + // This is important if you are using the timezone option, javascript's Date + // object will only return the timezone offset for the current locale, so we + // adjust it accordingly. If not using timezone option this won't matter.. + // If a timezone is different in tp, keep the timezone as is + if (tp_inst && tp_date) { + // look out for DST if tz wasn't specified + if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) { + tp_inst.timezone = tp_date.getTimezoneOffset() * -1; + } + date = $.timepicker.timezoneAdjust(date, tp_inst.timezone); + tp_date = $.timepicker.timezoneAdjust(tp_date, tp_inst.timezone); + } + + this._updateDatepicker(inst); + this._base_setDateDatepicker.apply(this, arguments); + this._setTimeDatepicker(target, tp_date, true); + }; + + /* + * override getDate() to allow getting time too within Date object + */ + $.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker; + $.datepicker._getDateDatepicker = function (target, noDefault) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + // if it hasn't yet been defined, grab from field + if (inst.lastVal === undefined) { + this._setDateFromField(inst, noDefault); + } + + var date = this._getDate(inst); + if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) { + date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); + date.setMicroseconds(tp_inst.microsec); + + // This is important if you are using the timezone option, javascript's Date + // object will only return the timezone offset for the current locale, so we + // adjust it accordingly. If not using timezone option this won't matter.. + if (tp_inst.timezone != null) { + // look out for DST if tz wasn't specified + if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) { + tp_inst.timezone = date.getTimezoneOffset() * -1; + } + date = $.timepicker.timezoneAdjust(date, tp_inst.timezone); + } + } + return date; + } + return this._base_getDateDatepicker(target, noDefault); + }; + + /* + * override parseDate() because UI 1.8.14 throws an error about "Extra characters" + * An option in datapicker to ignore extra format characters would be nicer. + */ + $.datepicker._base_parseDate = $.datepicker.parseDate; + $.datepicker.parseDate = function (format, value, settings) { + var date; + try { + date = this._base_parseDate(format, value, settings); + } catch (err) { + // Hack! The error message ends with a colon, a space, and + // the "extra" characters. We rely on that instead of + // attempting to perfectly reproduce the parsing algorithm. + if (err.indexOf(":") >= 0) { + date = this._base_parseDate(format, value.substring(0, value.length - (err.length - err.indexOf(':') - 2)), settings); + $.timepicker.log("Error parsing the date string: " + err + "\ndate string = " + value + "\ndate format = " + format); + } else { + throw err; + } + } + return date; + }; + + /* + * override formatDate to set date with time to the input + */ + $.datepicker._base_formatDate = $.datepicker._formatDate; + $.datepicker._formatDate = function (inst, day, month, year) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + tp_inst._updateDateTime(inst); + return tp_inst.$input.val(); + } + return this._base_formatDate(inst); + }; + + /* + * override options setter to add time to maxDate(Time) and minDate(Time). MaxDate + */ + $.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker; + $.datepicker._optionDatepicker = function (target, name, value) { + var inst = this._getInst(target), + name_clone; + if (!inst) { + return null; + } + + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var min = null, + max = null, + onselect = null, + overrides = tp_inst._defaults.evnts, + fns = {}, + prop; + if (typeof name === 'string') { // if min/max was set with the string + if (name === 'minDate' || name === 'minDateTime') { + min = value; + } else if (name === 'maxDate' || name === 'maxDateTime') { + max = value; + } else if (name === 'onSelect') { + onselect = value; + } else if (overrides.hasOwnProperty(name)) { + if (typeof (value) === 'undefined') { + return overrides[name]; + } + fns[name] = value; + name_clone = {}; //empty results in exiting function after overrides updated + } + } else if (typeof name === 'object') { //if min/max was set with the JSON + if (name.minDate) { + min = name.minDate; + } else if (name.minDateTime) { + min = name.minDateTime; + } else if (name.maxDate) { + max = name.maxDate; + } else if (name.maxDateTime) { + max = name.maxDateTime; + } + for (prop in overrides) { + if (overrides.hasOwnProperty(prop) && name[prop]) { + fns[prop] = name[prop]; + } + } + } + for (prop in fns) { + if (fns.hasOwnProperty(prop)) { + overrides[prop] = fns[prop]; + if (!name_clone) { + name_clone = $.extend({}, name); + } + delete name_clone[prop]; + } + } + if (name_clone && isEmptyObject(name_clone)) { + return; + } + if (min) { //if min was set + if (min === 0) { + min = new Date(); + } else { + min = new Date(min); + } + tp_inst._defaults.minDate = min; + tp_inst._defaults.minDateTime = min; + } else if (max) { //if max was set + if (max === 0) { + max = new Date(); + } else { + max = new Date(max); + } + tp_inst._defaults.maxDate = max; + tp_inst._defaults.maxDateTime = max; + } else if (onselect) { + tp_inst._defaults.onSelect = onselect; + } + } + if (value === undefined) { + return this._base_optionDatepicker.call($.datepicker, target, name); + } + return this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value); + }; + + /* + * jQuery isEmptyObject does not check hasOwnProperty - if someone has added to the object prototype, + * it will return false for all objects + */ + var isEmptyObject = function (obj) { + var prop; + for (prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + return true; + }; + + /* + * jQuery extend now ignores nulls! + */ + var extendRemove = function (target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] === null || props[name] === undefined) { + target[name] = props[name]; + } + } + return target; + }; + + /* + * Determine by the time format which units are supported + * Returns an object of booleans for each unit + */ + var detectSupport = function (timeFormat) { + var tf = timeFormat.replace(/'.*?'/g, '').toLowerCase(), // removes literals + isIn = function (f, t) { // does the format contain the token? + return f.indexOf(t) !== -1 ? true : false; + }; + return { + hour: isIn(tf, 'h'), + minute: isIn(tf, 'm'), + second: isIn(tf, 's'), + millisec: isIn(tf, 'l'), + microsec: isIn(tf, 'c'), + timezone: isIn(tf, 'z'), + ampm: isIn(tf, 't') && isIn(timeFormat, 'h'), + iso8601: isIn(timeFormat, 'Z') + }; + }; + + /* + * Converts 24 hour format into 12 hour + * Returns 12 hour without leading 0 + */ + var convert24to12 = function (hour) { + hour %= 12; + + if (hour === 0) { + hour = 12; + } + + return String(hour); + }; + + var computeEffectiveSetting = function (settings, property) { + return settings && settings[property] ? settings[property] : $.timepicker._defaults[property]; + }; + + /* + * Splits datetime string into date and time substrings. + * Throws exception when date can't be parsed + * Returns {dateString: dateString, timeString: timeString} + */ + var splitDateTime = function (dateTimeString, timeSettings) { + // The idea is to get the number separator occurrences in datetime and the time format requested (since time has + // fewer unknowns, mostly numbers and am/pm). We will use the time pattern to split. + var separator = computeEffectiveSetting(timeSettings, 'separator'), + format = computeEffectiveSetting(timeSettings, 'timeFormat'), + timeParts = format.split(separator), // how many occurrences of separator may be in our format? + timePartsLen = timeParts.length, + allParts = dateTimeString.split(separator), + allPartsLen = allParts.length; + + if (allPartsLen > 1) { + return { + dateString: allParts.splice(0, allPartsLen - timePartsLen).join(separator), + timeString: allParts.splice(0, timePartsLen).join(separator) + }; + } + + return { + dateString: dateTimeString, + timeString: '' + }; + }; + + /* + * Internal function to parse datetime interval + * Returns: {date: Date, timeObj: Object}, where + * date - parsed date without time (type Date) + * timeObj = {hour: , minute: , second: , millisec: , microsec: } - parsed time. Optional + */ + var parseDateTimeInternal = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { + var date, + parts, + parsedTime; + + parts = splitDateTime(dateTimeString, timeSettings); + date = $.datepicker._base_parseDate(dateFormat, parts.dateString, dateSettings); + + if (parts.timeString === '') { + return { + date: date + }; + } + + parsedTime = $.datepicker.parseTime(timeFormat, parts.timeString, timeSettings); + + if (!parsedTime) { + throw 'Wrong time format'; + } + + return { + date: date, + timeObj: parsedTime + }; + }; + + /* + * Internal function to set timezone_select to the local timezone + */ + var selectLocalTimezone = function (tp_inst, date) { + if (tp_inst && tp_inst.timezone_select) { + var now = date || new Date(); + tp_inst.timezone_select.val(-now.getTimezoneOffset()); + } + }; + + /* + * Create a Singleton Instance + */ + $.timepicker = new Timepicker(); + + /** + * Get the timezone offset as string from a date object (eg '+0530' for UTC+5.5) + * @param {number} tzMinutes if not a number, less than -720 (-1200), or greater than 840 (+1400) this value is returned + * @param {boolean} iso8601 if true formats in accordance to iso8601 "+12:45" + * @return {string} + */ + $.timepicker.timezoneOffsetString = function (tzMinutes, iso8601) { + if (isNaN(tzMinutes) || tzMinutes > 840 || tzMinutes < -720) { + return tzMinutes; + } + + var off = tzMinutes, + minutes = off % 60, + hours = (off - minutes) / 60, + iso = iso8601 ? ':' : '', + tz = (off >= 0 ? '+' : '-') + ('0' + Math.abs(hours)).slice(-2) + iso + ('0' + Math.abs(minutes)).slice(-2); + + if (tz === '+00:00') { + return 'Z'; + } + return tz; + }; + + /** + * Get the number in minutes that represents a timezone string + * @param {string} tzString formatted like "+0500", "-1245", "Z" + * @return {number} the offset minutes or the original string if it doesn't match expectations + */ + $.timepicker.timezoneOffsetNumber = function (tzString) { + var normalized = tzString.toString().replace(':', ''); // excuse any iso8601, end up with "+1245" + + if (normalized.toUpperCase() === 'Z') { // if iso8601 with Z, its 0 minute offset + return 0; + } + + if (!/^(\-|\+)\d{4}$/.test(normalized)) { // possibly a user defined tz, so just give it back + return tzString; + } + + return ((normalized.substr(0, 1) === '-' ? -1 : 1) * // plus or minus + ((parseInt(normalized.substr(1, 2), 10) * 60) + // hours (converted to minutes) + parseInt(normalized.substr(3, 2), 10))); // minutes + }; + + /** + * No way to set timezone in js Date, so we must adjust the minutes to compensate. (think setDate, getDate) + * @param {Date} date + * @param {string} toTimezone formatted like "+0500", "-1245" + * @return {Date} + */ + $.timepicker.timezoneAdjust = function (date, toTimezone) { + var toTz = $.timepicker.timezoneOffsetNumber(toTimezone); + if (!isNaN(toTz)) { + date.setMinutes(date.getMinutes() + -date.getTimezoneOffset() - toTz); + } + return date; + }; + + /** + * Calls `timepicker()` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * n.b. The input value must be correctly formatted (reformatting is not supported) + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the timepicker() call + * @return {jQuery} + */ + $.timepicker.timeRange = function (startTime, endTime, options) { + return $.timepicker.handleRange('timepicker', startTime, endTime, options); + }; + + /** + * Calls `datetimepicker` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @param {string} method Can be used to specify the type of picker to be added + * @return {jQuery} + */ + $.timepicker.datetimeRange = function (startTime, endTime, options) { + $.timepicker.handleRange('datetimepicker', startTime, endTime, options); + }; + + /** + * Calls `datepicker` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @return {jQuery} + */ + $.timepicker.dateRange = function (startTime, endTime, options) { + $.timepicker.handleRange('datepicker', startTime, endTime, options); + }; + + /** + * Calls `method` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {string} method Can be used to specify the type of picker to be added + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @return {jQuery} + */ + $.timepicker.handleRange = function (method, startTime, endTime, options) { + options = $.extend({}, { + minInterval: 0, // min allowed interval in milliseconds + maxInterval: 0, // max allowed interval in milliseconds + start: {}, // options for start picker + end: {} // options for end picker + }, options); + + function checkDates(changed, other) { + var startdt = startTime[method]('getDate'), + enddt = endTime[method]('getDate'), + changeddt = changed[method]('getDate'); + + if (startdt !== null) { + var minDate = new Date(startdt.getTime()), + maxDate = new Date(startdt.getTime()); + + minDate.setMilliseconds(minDate.getMilliseconds() + options.minInterval); + maxDate.setMilliseconds(maxDate.getMilliseconds() + options.maxInterval); + + if (options.minInterval > 0 && minDate > enddt) { // minInterval check + endTime[method]('setDate', minDate); + } else if (options.maxInterval > 0 && maxDate < enddt) { // max interval check + endTime[method]('setDate', maxDate); + } else if (startdt > enddt) { + other[method]('setDate', changeddt); + } + } + } + + function selected(changed, other, option) { + if (!changed.val()) { + return; + } + var date = changed[method].call(changed, 'getDate'); + if (date !== null && options.minInterval > 0) { + if (option === 'minDate') { + date.setMilliseconds(date.getMilliseconds() + options.minInterval); + } + if (option === 'maxDate') { + date.setMilliseconds(date.getMilliseconds() - options.minInterval); + } + } + if (date.getTime) { + other[method].call(other, 'option', option, date); + } + } + + $.fn[method].call(startTime, $.extend({ + onClose: function (dateText, inst) { + checkDates($(this), endTime); + }, + onSelect: function (selectedDateTime) { + selected($(this), endTime, 'minDate'); + } + }, options, options.start)); + $.fn[method].call(endTime, $.extend({ + onClose: function (dateText, inst) { + checkDates($(this), startTime); + }, + onSelect: function (selectedDateTime) { + selected($(this), startTime, 'maxDate'); + } + }, options, options.end)); + + checkDates(startTime, endTime); + selected(startTime, endTime, 'minDate'); + selected(endTime, startTime, 'maxDate'); + return $([startTime.get(0), endTime.get(0)]); + }; + + /** + * Log error or data to the console during error or debugging + * @param {Object} err pass any type object to log to the console during error or debugging + * @return {void} + */ + $.timepicker.log = function (err) { + if (window.console) { + window.console.log(err); + } + }; + + /* + * Add util object to allow access to private methods for testability. + */ + $.timepicker._util = { + _extendRemove: extendRemove, + _isEmptyObject: isEmptyObject, + _convert24to12: convert24to12, + _detectSupport: detectSupport, + _selectLocalTimezone: selectLocalTimezone, + _computeEffectiveSetting: computeEffectiveSetting, + _splitDateTime: splitDateTime, + _parseDateTimeInternal: parseDateTimeInternal + }; + + /* + * Microsecond support + */ + if (!Date.prototype.getMicroseconds) { + Date.prototype.microseconds = 0; + Date.prototype.getMicroseconds = function () { + return this.microseconds; + }; + Date.prototype.setMicroseconds = function (m) { + this.setMilliseconds(this.getMilliseconds() + Math.floor(m / 1000)); + this.microseconds = m % 1000; + return this; + }; + } + + /* + * Keep up with the version + */ + $.timepicker.version = "1.4.3"; + +})); diff --git a/lib/web/jquery/ui-modules/tooltip.js b/lib/web/jquery/ui-modules/tooltip.js new file mode 100644 index 000000000000..61ec01071432 --- /dev/null +++ b/lib/web/jquery/ui-modules/tooltip.js @@ -0,0 +1,404 @@ +/*! + * jQuery UI Tooltip - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/tooltip/ + */ + +define([ + 'jquery', + 'jquery-ui-modules/core', + 'jquery-ui-modules/widget', + 'jquery-ui-modules/position' +], function ($) { + + var increments = 0; + + function addDescribedBy(elem, id) { + var describedby = (elem.attr("aria-describedby") || "").split(/\s+/); + describedby.push(id); + elem + .data("ui-tooltip-id", id) + .attr("aria-describedby", $.trim(describedby.join(" "))); + } + + function removeDescribedBy(elem) { + var id = elem.data("ui-tooltip-id"), + describedby = (elem.attr("aria-describedby") || "").split(/\s+/), + index = $.inArray(id, describedby); + if (index !== -1) { + describedby.splice(index, 1); + } + + elem.removeData("ui-tooltip-id"); + describedby = $.trim(describedby.join(" ")); + if (describedby) { + elem.attr("aria-describedby", describedby); + } else { + elem.removeAttr("aria-describedby"); + } + } + + $.widget("ui.tooltip", { + version: "1.10.4", + options: { + content: function () { + // support: IE<9, Opera in jQuery <1.7 + // .text() can't accept undefined, so coerce to a string + var title = $(this).attr("title") || ""; + // Escape title, since we're going from an attribute to raw HTML + return $("<a>").text(title).html(); + }, + hide: true, + // Disabled elements have inconsistent behavior across browsers (#8661) + items: "[title]:not([disabled])", + position: { + my: "left top+15", + at: "left bottom", + collision: "flipfit flip" + }, + show: true, + tooltipClass: null, + track: false, + + // callbacks + close: null, + open: null + }, + + _create: function () { + this._on({ + mouseover: "open", + focusin: "open" + }); + + // IDs of generated tooltips, needed for destroy + this.tooltips = {}; + // IDs of parent tooltips where we removed the title attribute + this.parents = {}; + + if (this.options.disabled) { + this._disable(); + } + }, + + _setOption: function (key, value) { + var that = this; + + if (key === "disabled") { + this[value ? "_disable" : "_enable"](); + this.options[key] = value; + // disable element style changes + return; + } + + this._super(key, value); + + if (key === "content") { + $.each(this.tooltips, function (id, element) { + that._updateContent(element); + }); + } + }, + + _disable: function () { + var that = this; + + // close open tooltips + $.each(this.tooltips, function (id, element) { + var event = $.Event("blur"); + event.target = event.currentTarget = element[0]; + that.close(event, true); + }); + + // remove title attributes to prevent native tooltips + this.element.find(this.options.items).addBack().each(function () { + var element = $(this); + if (element.is("[title]")) { + element + .data("ui-tooltip-title", element.attr("title")) + .attr("title", ""); + } + }); + }, + + _enable: function () { + // restore title attributes + this.element.find(this.options.items).addBack().each(function () { + var element = $(this); + if (element.data("ui-tooltip-title")) { + element.attr("title", element.data("ui-tooltip-title")); + } + }); + }, + + open: function (event) { + var that = this, + target = $(event ? event.target : this.element) + // we need closest here due to mouseover bubbling, + // but always pointing at the same event target + .closest(this.options.items); + + // No element to show a tooltip for or the tooltip is already open + if (!target.length || target.data("ui-tooltip-id")) { + return; + } + + if (target.attr("title")) { + target.data("ui-tooltip-title", target.attr("title")); + } + + target.data("ui-tooltip-open", true); + + // kill parent tooltips, custom or native, for hover + if (event && event.type === "mouseover") { + target.parents().each(function () { + var parent = $(this), + blurEvent; + if (parent.data("ui-tooltip-open")) { + blurEvent = $.Event("blur"); + blurEvent.target = blurEvent.currentTarget = this; + that.close(blurEvent, true); + } + if (parent.attr("title")) { + parent.uniqueId(); + that.parents[this.id] = { + element: this, + title: parent.attr("title") + }; + parent.attr("title", ""); + } + }); + } + + this._updateContent(target, event); + }, + + _updateContent: function (target, event) { + var content, + contentOption = this.options.content, + that = this, + eventType = event ? event.type : null; + + if (typeof contentOption === "string") { + return this._open(event, target, contentOption); + } + + content = contentOption.call(target[0], function (response) { + // ignore async response if tooltip was closed already + if (!target.data("ui-tooltip-open")) { + return; + } + // IE may instantly serve a cached response for ajax requests + // delay this call to _open so the other call to _open runs first + that._delay(function () { + // jQuery creates a special event for focusin when it doesn't + // exist natively. To improve performance, the native event + // object is reused and the type is changed. Therefore, we can't + // rely on the type being correct after the event finished + // bubbling, so we set it back to the previous value. (#8740) + if (event) { + event.type = eventType; + } + this._open(event, target, response); + }); + }); + if (content) { + this._open(event, target, content); + } + }, + + _open: function (event, target, content) { + var tooltip, events, delayedShow, + positionOption = $.extend({}, this.options.position); + + if (!content) { + return; + } + + // Content can be updated multiple times. If the tooltip already + // exists, then just update the content and bail. + tooltip = this._find(target); + if (tooltip.length) { + tooltip.find(".ui-tooltip-content").html(content); + return; + } + + // if we have a title, clear it to prevent the native tooltip + // we have to check first to avoid defining a title if none exists + // (we don't want to cause an element to start matching [title]) + // + // We use removeAttr only for key events, to allow IE to export the correct + // accessible attributes. For mouse events, set to empty string to avoid + // native tooltip showing up (happens only when removing inside mouseover). + if (target.is("[title]")) { + if (event && event.type === "mouseover") { + target.attr("title", ""); + } else { + target.removeAttr("title"); + } + } + + tooltip = this._tooltip(target); + addDescribedBy(target, tooltip.attr("id")); + tooltip.find(".ui-tooltip-content").html(content); + + function position(event) { + positionOption.of = event; + if (tooltip.is(":hidden")) { + return; + } + tooltip.position(positionOption); + } + + if (this.options.track && event && /^mouse/.test(event.type)) { + this._on(this.document, { + mousemove: position + }); + // trigger once to override element-relative positioning + position(event); + } else { + tooltip.position($.extend({ + of: target + }, this.options.position)); + } + + tooltip.hide(); + + this._show(tooltip, this.options.show); + // Handle tracking tooltips that are shown with a delay (#8644). As soon + // as the tooltip is visible, position the tooltip using the most recent + // event. + if (this.options.show && this.options.show.delay) { + delayedShow = this.delayedShow = setInterval(function () { + if (tooltip.is(":visible")) { + position(positionOption.of); + clearInterval(delayedShow); + } + }, $.fx.interval); + } + + this._trigger("open", event, {tooltip: tooltip}); + + events = { + keyup: function (event) { + if (event.keyCode === $.ui.keyCode.ESCAPE) { + var fakeEvent = $.Event(event); + fakeEvent.currentTarget = target[0]; + this.close(fakeEvent, true); + } + }, + remove: function () { + this._removeTooltip(tooltip); + } + }; + if (!event || event.type === "mouseover") { + events.mouseleave = "close"; + } + if (!event || event.type === "focusin") { + events.focusout = "close"; + } + this._on(true, target, events); + }, + + close: function (event) { + var that = this, + target = $(event ? event.currentTarget : this.element), + tooltip = this._find(target); + + // disabling closes the tooltip, so we need to track when we're closing + // to avoid an infinite loop in case the tooltip becomes disabled on close + if (this.closing) { + return; + } + + // Clear the interval for delayed tracking tooltips + clearInterval(this.delayedShow); + + // only set title if we had one before (see comment in _open()) + if (target.data("ui-tooltip-title")) { + target.attr("title", target.data("ui-tooltip-title")); + } + + removeDescribedBy(target); + + tooltip.stop(true); + this._hide(tooltip, this.options.hide, function () { + that._removeTooltip($(this)); + }); + + target.removeData("ui-tooltip-open"); + this._off(target, "mouseleave focusout keyup"); + // Remove 'remove' binding only on delegated targets + if (target[0] !== this.element[0]) { + this._off(target, "remove"); + } + this._off(this.document, "mousemove"); + + if (event && event.type === "mouseleave") { + $.each(this.parents, function (id, parent) { + $(parent.element).attr("title", parent.title); + delete that.parents[id]; + }); + } + + this.closing = true; + this._trigger("close", event, {tooltip: tooltip}); + this.closing = false; + }, + + _tooltip: function (element) { + var id = "ui-tooltip-" + increments++, + tooltip = $("<div>") + .attr({ + id: id, + role: "tooltip" + }) + .addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content " + + (this.options.tooltipClass || "")); + $("<div>") + .addClass("ui-tooltip-content") + .appendTo(tooltip); + tooltip.appendTo(this.document[0].body); + this.tooltips[id] = element; + return tooltip; + }, + + _find: function (target) { + var id = target.data("ui-tooltip-id"); + return id ? $("#" + id) : $(); + }, + + _removeTooltip: function (tooltip) { + tooltip.remove(); + delete this.tooltips[tooltip.attr("id")]; + }, + + _destroy: function () { + var that = this; + + // close open tooltips + $.each(this.tooltips, function (id, element) { + // Delegate to close method to handle common cleanup + var event = $.Event("blur"); + event.target = event.currentTarget = element[0]; + that.close(event, true); + + // Remove immediately; destroying an open tooltip doesn't use the + // hide animation + $("#" + id).remove(); + + // Restore the title + if (element.data("ui-tooltip-title")) { + element.attr("title", element.data("ui-tooltip-title")); + element.removeData("ui-tooltip-title"); + } + }); + } + }); + +}); diff --git a/lib/web/jquery/ui-modules/widget.js b/lib/web/jquery/ui-modules/widget.js new file mode 100644 index 000000000000..6c5dcbaa742e --- /dev/null +++ b/lib/web/jquery/ui-modules/widget.js @@ -0,0 +1,527 @@ +/*! + * jQuery UI Widget - v1.10.4 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + +define([ + 'jquery' +], function ($, undefined) { + + var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; + $.cleanData = function (elems) { + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + try { + $(elem).triggerHandler("remove"); + // http://bugs.jquery.com/ticket/8235 + } catch (e) { + } + } + _cleanData(elems); + }; + + $.widget = function (name, base, prototype) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split(".")[0]; + + name = name.split(".")[1]; + fullName = namespace + "-" + name; + + if (!prototype) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[":"][fullName.toLowerCase()] = function (elem) { + return !!$.data(elem, fullName); + }; + + $[namespace] = $[namespace] || {}; + existingConstructor = $[namespace][name]; + constructor = $[namespace][name] = function (options, element) { + // allow instantiation without "new" keyword + if (!this._createWidget) { + return new constructor(options, element); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if (arguments.length) { + this._createWidget(options, element); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend(constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend({}, prototype), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend({}, basePrototype.options); + $.each(prototype, function (prop, value) { + if (!$.isFunction(value)) { + proxiedPrototype[prop] = value; + return; + } + proxiedPrototype[prop] = (function () { + var _super = function () { + return base.prototype[prop].apply(this, arguments); + }, + _superApply = function (args) { + return base.prototype[prop].apply(this, args); + }; + return function () { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply(this, arguments); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend(basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if (existingConstructor) { + $.each(existingConstructor._childConstructors, function (i, child) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget(childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push(constructor); + } + + $.widget.bridge(name, constructor); + }; + + $.widget.extend = function (target) { + var input = slice.call(arguments, 1), + inputIndex = 0, + inputLength = input.length, + key, + value; + for (; inputIndex < inputLength; inputIndex++) { + for (key in input[inputIndex]) { + value = input[inputIndex][key]; + if (input[inputIndex].hasOwnProperty(key) && value !== undefined) { + // Clone objects + if ($.isPlainObject(value)) { + target[key] = $.isPlainObject(target[key]) ? + $.widget.extend({}, target[key], value) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend({}, value); + // Copy everything else by reference + } else { + target[key] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function (name, object) { + var fullName = object.prototype.widgetFullName || name; + $.fn[name] = function (options) { + var isMethodCall = typeof options === "string", + args = slice.call(arguments, 1), + returnValue = this; + + // allow multiple hashes to be passed on init + options = !isMethodCall && args.length ? + $.widget.extend.apply(null, [options].concat(args)) : + options; + + if (isMethodCall) { + this.each(function () { + var methodValue, + instance = $.data(this, fullName); + if (!instance) { + return $.error("cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'"); + } + if (!$.isFunction(instance[options]) || options.charAt(0) === "_") { + return $.error("no such method '" + options + "' for " + name + " widget instance"); + } + methodValue = instance[options].apply(instance, args); + if (methodValue !== instance && methodValue !== undefined) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack(methodValue.get()) : + methodValue; + return false; + } + }); + } else { + this.each(function () { + var instance = $.data(this, fullName); + if (instance) { + instance.option(options || {})._init(); + } else { + $.data(this, fullName, new object(options, this)); + } + }); + } + + return returnValue; + }; + }; + + $.Widget = function ( /* options, element */) { + }; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "<div>", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function (options, element) { + element = $(element || this.defaultElement || this)[0]; + this.element = $(element); + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend({}, + this.options, + this._getCreateOptions(), + options); + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if (element !== this) { + $.data(element, this.widgetFullName, this); + this._on(true, this.element, { + remove: function (event) { + if (event.target === element) { + this.destroy(); + } + } + }); + this.document = $(element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element); + this.window = $(this.document[0].defaultView || this.document[0].parentWindow); + } + + this._create(); + this._trigger("create", null, this._getCreateEventData()); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function () { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind(this.eventNamespace) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData(this.widgetName) + .removeData(this.widgetFullName) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData($.camelCase(this.widgetFullName)); + this.widget() + .unbind(this.eventNamespace) + .removeAttr("aria-disabled") + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled"); + + // clean up events and states + this.bindings.unbind(this.eventNamespace); + this.hoverable.removeClass("ui-state-hover"); + this.focusable.removeClass("ui-state-focus"); + }, + _destroy: $.noop, + + widget: function () { + return this.element; + }, + + option: function (key, value) { + var options = key, + parts, + curOption, + i; + + if (arguments.length === 0) { + // don't return a reference to the internal hash + return $.widget.extend({}, this.options); + } + + if (typeof key === "string") { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split("."); + key = parts.shift(); + if (parts.length) { + curOption = options[key] = $.widget.extend({}, this.options[key]); + for (i = 0; i < parts.length - 1; i++) { + curOption[parts[i]] = curOption[parts[i]] || {}; + curOption = curOption[parts[i]]; + } + key = parts.pop(); + if (arguments.length === 1) { + return curOption[key] === undefined ? null : curOption[key]; + } + curOption[key] = value; + } else { + if (arguments.length === 1) { + return this.options[key] === undefined ? null : this.options[key]; + } + options[key] = value; + } + } + + this._setOptions(options); + + return this; + }, + _setOptions: function (options) { + var key; + + for (key in options) { + this._setOption(key, options[key]); + } + + return this; + }, + _setOption: function (key, value) { + this.options[key] = value; + + if (key === "disabled") { + this.widget() + .toggleClass(this.widgetFullName + "-disabled ui-state-disabled", !!value) + .attr("aria-disabled", value); + this.hoverable.removeClass("ui-state-hover"); + this.focusable.removeClass("ui-state-focus"); + } + + return this; + }, + + enable: function () { + return this._setOption("disabled", false); + }, + disable: function () { + return this._setOption("disabled", true); + }, + + _on: function (suppressDisabledCheck, element, handlers) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if (typeof suppressDisabledCheck !== "boolean") { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if (!handlers) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + // accept selectors, DOM elements + element = delegateElement = $(element); + this.bindings = this.bindings.add(element); + } + + $.each(handlers, function (event, handler) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if (!suppressDisabledCheck && + (instance.options.disabled === true || + $(this).hasClass("ui-state-disabled"))) { + return; + } + return (typeof handler === "string" ? instance[handler] : handler) + .apply(instance, arguments); + } + + // copy the guid so direct unbinding works + if (typeof handler !== "string") { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match(/^(\w+)\s*(.*)$/), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if (selector) { + delegateElement.delegate(selector, eventName, handlerProxy); + } else { + element.bind(eventName, handlerProxy); + } + }); + }, + + _off: function (element, eventName) { + eventName = (eventName || "").split(" ").join(this.eventNamespace + " ") + this.eventNamespace; + element.unbind(eventName).undelegate(eventName); + }, + + _delay: function (handler, delay) { + function handlerProxy() { + return (typeof handler === "string" ? instance[handler] : handler) + .apply(instance, arguments); + } + + var instance = this; + return setTimeout(handlerProxy, delay || 0); + }, + + _hoverable: function (element) { + this.hoverable = this.hoverable.add(element); + this._on(element, { + mouseenter: function (event) { + $(event.currentTarget).addClass("ui-state-hover"); + }, + mouseleave: function (event) { + $(event.currentTarget).removeClass("ui-state-hover"); + } + }); + }, + + _focusable: function (element) { + this.focusable = this.focusable.add(element); + this._on(element, { + focusin: function (event) { + $(event.currentTarget).addClass("ui-state-focus"); + }, + focusout: function (event) { + $(event.currentTarget).removeClass("ui-state-focus"); + } + }); + }, + + _trigger: function (type, event, data) { + var prop, orig, + callback = this.options[type]; + + data = data || {}; + event = $.Event(event); + event.type = (type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[0]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if (orig) { + for (prop in orig) { + if (!(prop in event)) { + event[prop] = orig[prop]; + } + } + } + + this.element.trigger(event, data); + return !($.isFunction(callback) && + callback.apply(this.element[0], [event].concat(data)) === false || + event.isDefaultPrevented()); + } + }; + + $.each({show: "fadeIn", hide: "fadeOut"}, function (method, defaultEffect) { + $.Widget.prototype["_" + method] = function (element, options, callback) { + if (typeof options === "string") { + options = {effect: options}; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if (typeof options === "number") { + options = {duration: options}; + } + hasOptions = !$.isEmptyObject(options); + options.complete = callback; + if (options.delay) { + element.delay(options.delay); + } + if (hasOptions && $.effects && $.effects.effect[effectName]) { + element[method](options); + } else if (effectName !== method && element[effectName]) { + element[effectName](options.duration, options.easing, callback); + } else { + element.queue(function (next) { + $(this)[method](); + if (callback) { + callback.call(element[0]); + } + next(); + }); + } + }; + }); + +}); diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js index 2c00718d97ea..93badda89df6 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js @@ -193,6 +193,7 @@ define([ settings = { selector: '#' + this.getId(), theme: 'modern', + skin: 'magento', 'entity_encoding': 'raw', 'convert_urls': false, 'content_css': this.config.tinymce4['content_css'], @@ -263,6 +264,10 @@ define([ } }; + if (this.config.skin) { + settings.skin = this.config.skin; + } + if (this.config.baseStaticUrl && this.config.baseStaticDefaultUrl) { settings['document_base_url'] = this.config.baseStaticUrl; } @@ -654,9 +659,9 @@ define([ // process tag attributes string attributesString = attributesString.gsub(/([a-z0-9\-\_]+)="(.*?)(\{\{.+?\}\})(.*?)"/i, function (m) { - decodedDirectiveString = encodeURIComponent(Base64.mageEncode(m[3].replace(/"/g, '"'))); + decodedDirectiveString = encodeURIComponent(Base64.mageEncode(m[3].replace(/"/g, '"') + m[4])); - return m[1] + '="' + m[2] + this.makeDirectiveUrl(decodedDirectiveString) + m[4] + '"'; + return m[1] + '="' + m[2] + this.makeDirectiveUrl(decodedDirectiveString) + '"'; }.bind(this)); return '<' + match[1] + attributesString + '>'; diff --git a/lib/web/mage/adminhtml/wysiwyg/widget.js b/lib/web/mage/adminhtml/wysiwyg/widget.js index aa38e2e1875f..5c1a77b6382a 100644 --- a/lib/web/mage/adminhtml/wysiwyg/widget.js +++ b/lib/web/mage/adminhtml/wysiwyg/widget.js @@ -468,6 +468,7 @@ define([ if (activeNode) { editor.selection.select(activeNode); editor.selection.setContent(transport.responseText); + editor.fire('Change'); } else if (this.bMark) { editor.selection.moveToBookmark(this.bMark); } diff --git a/lib/web/mage/calendar.js b/lib/web/mage/calendar.js index a9ccf2cf787f..85f1904c0e19 100644 --- a/lib/web/mage/calendar.js +++ b/lib/web/mage/calendar.js @@ -10,8 +10,9 @@ if (typeof define === 'function' && define.amd) { define([ 'jquery', - 'jquery/ui', - 'jquery/jquery-ui-timepicker-addon' + 'jquery-ui-modules/widget', + 'jquery-ui-modules/datepicker', + 'jquery-ui-modules/timepicker' ], factory); } else { factory(window.jQuery); diff --git a/lib/web/mage/collapsible.js b/lib/web/mage/collapsible.js index cae6ee01b0de..46b50f57f7e8 100644 --- a/lib/web/mage/collapsible.js +++ b/lib/web/mage/collapsible.js @@ -5,7 +5,8 @@ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', + 'jquery-ui-modules/core', 'jquery/jquery-storageapi', 'mage/mage' ], function ($) { diff --git a/lib/web/mage/dataPost.js b/lib/web/mage/dataPost.js index cc56ee266e08..6ed1fdcf0adc 100644 --- a/lib/web/mage/dataPost.js +++ b/lib/web/mage/dataPost.js @@ -7,7 +7,7 @@ define([ 'jquery', 'mage/template', 'Magento_Ui/js/modal/confirm', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, mageTemplate, uiConfirm) { 'use strict'; diff --git a/lib/web/mage/deletable-item.js b/lib/web/mage/deletable-item.js index 5b42500e21c3..7421ee98340e 100644 --- a/lib/web/mage/deletable-item.js +++ b/lib/web/mage/deletable-item.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/lib/web/mage/dialog.js b/lib/web/mage/dialog.js index d90d5d6960d9..360f8e83dab5 100644 --- a/lib/web/mage/dialog.js +++ b/lib/web/mage/dialog.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/dialog' ], function ($) { 'use strict'; diff --git a/lib/web/mage/dropdown.js b/lib/web/mage/dropdown.js index 389c083e4b4c..1f67afa415a7 100644 --- a/lib/web/mage/dropdown.js +++ b/lib/web/mage/dropdown.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/dialog', 'mage/translate' ], function ($) { 'use strict'; diff --git a/lib/web/mage/edit-trigger.js b/lib/web/mage/edit-trigger.js index c2009e78c15e..e28a826ec87e 100644 --- a/lib/web/mage/edit-trigger.js +++ b/lib/web/mage/edit-trigger.js @@ -13,7 +13,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui' + 'jquery-ui-modules/widget' ], factory); } else { factory(root.jQuery, root.mageTemplate); diff --git a/lib/web/mage/fieldset-controls.js b/lib/web/mage/fieldset-controls.js index a95218e32b4a..62418ccea0c5 100644 --- a/lib/web/mage/fieldset-controls.js +++ b/lib/web/mage/fieldset-controls.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/lib/web/mage/item-table.js b/lib/web/mage/item-table.js index 7c1cdf4e47d9..97056bbdad34 100644 --- a/lib/web/mage/item-table.js +++ b/lib/web/mage/item-table.js @@ -9,7 +9,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, mageTemplate) { 'use strict'; diff --git a/lib/web/mage/list.js b/lib/web/mage/list.js index 41ae951237a3..297fffd7ab03 100644 --- a/lib/web/mage/list.js +++ b/lib/web/mage/list.js @@ -9,7 +9,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, mageTemplate) { 'use strict'; diff --git a/lib/web/mage/loader.js b/lib/web/mage/loader.js index f0274a5347ff..83299efba422 100644 --- a/lib/web/mage/loader.js +++ b/lib/web/mage/loader.js @@ -6,7 +6,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], function ($, mageTemplate) { 'use strict'; diff --git a/lib/web/mage/loader_old.js b/lib/web/mage/loader_old.js index 5233586149d9..f759758acd12 100644 --- a/lib/web/mage/loader_old.js +++ b/lib/web/mage/loader_old.js @@ -10,7 +10,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui', + 'jquery-ui-modules/widget', 'mage/translate' ], factory); } else { diff --git a/lib/web/mage/menu.js b/lib/web/mage/menu.js index 4853ba221c29..ee6ad2dc9647 100644 --- a/lib/web/mage/menu.js +++ b/lib/web/mage/menu.js @@ -6,7 +6,7 @@ define([ 'jquery', 'matchMedia', - 'jquery/ui', + 'jquery-ui-modules/menu', 'jquery/jquery.mobile.custom', 'mage/translate' ], function ($, mediaCheck) { diff --git a/lib/web/mage/multiselect.js b/lib/web/mage/multiselect.js index d911bf094333..2e15d80cdc7e 100644 --- a/lib/web/mage/multiselect.js +++ b/lib/web/mage/multiselect.js @@ -8,7 +8,7 @@ define([ 'jquery', 'text!mage/multiselect.html', 'Magento_Ui/js/modal/alert', - 'jquery/ui', + 'jquery-ui-modules/widget', 'jquery/editableMultiselect/js/jquery.multiselect' ], function (_, $, searchTemplate, alert) { 'use strict'; diff --git a/lib/web/mage/popup-window.js b/lib/web/mage/popup-window.js index 37c3b27a4510..5d7b0d1ddfd5 100644 --- a/lib/web/mage/popup-window.js +++ b/lib/web/mage/popup-window.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/lib/web/mage/redirect-url.js b/lib/web/mage/redirect-url.js index 8407e56ea071..9c14d5bbde3d 100644 --- a/lib/web/mage/redirect-url.js +++ b/lib/web/mage/redirect-url.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/lib/web/mage/sticky.js b/lib/web/mage/sticky.js index f98774e203ea..b6e29bb3cae2 100644 --- a/lib/web/mage/sticky.js +++ b/lib/web/mage/sticky.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/lib/web/mage/tabs.js b/lib/web/mage/tabs.js index 65c452d33bf1..496a271c631f 100644 --- a/lib/web/mage/tabs.js +++ b/lib/web/mage/tabs.js @@ -5,7 +5,8 @@ define([ 'jquery', - 'jquery/ui', + 'jquery-ui-modules/widget', + 'jquery-ui-modules/core', 'mage/mage', 'mage/collapsible' ], function ($) { diff --git a/lib/web/mage/toggle.js b/lib/web/mage/toggle.js index b49657b99d84..3e04406c3b75 100644 --- a/lib/web/mage/toggle.js +++ b/lib/web/mage/toggle.js @@ -5,7 +5,7 @@ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($) { 'use strict'; diff --git a/lib/web/mage/tooltip.js b/lib/web/mage/tooltip.js index 7aad682da70d..6dc6114cc70a 100644 --- a/lib/web/mage/tooltip.js +++ b/lib/web/mage/tooltip.js @@ -8,7 +8,7 @@ */ define([ 'jquery', - 'jquery/ui' + 'jquery-ui-modules/tooltip' ], function ($) { 'use strict'; diff --git a/lib/web/mage/translate-inline-vde.js b/lib/web/mage/translate-inline-vde.js index 38413ade46cc..485413e3c928 100644 --- a/lib/web/mage/translate-inline-vde.js +++ b/lib/web/mage/translate-inline-vde.js @@ -13,7 +13,8 @@ define([ 'jquery', 'mage/template', - 'jquery/ui', + 'jquery-ui-modules/widget', + 'jquery-ui-modules/core', 'mage/translate-inline', 'mage/translate' ], factory); diff --git a/lib/web/mage/translate-inline.js b/lib/web/mage/translate-inline.js index 141af6e141c3..cdd2b8ad322f 100644 --- a/lib/web/mage/translate-inline.js +++ b/lib/web/mage/translate-inline.js @@ -10,7 +10,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui', + 'jquery-ui-modules/dialog', 'mage/translate' ], factory); } else { diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index a7a5ade2ae9f..b284f0002bc6 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -10,7 +10,7 @@ define([ 'jquery', 'moment', - 'jquery/ui', + 'jquery-ui-modules/widget', 'jquery/validate', 'mage/translate' ], factory); diff --git a/lib/web/mage/zoom.js b/lib/web/mage/zoom.js index 0082de721971..a7ad19fb560a 100644 --- a/lib/web/mage/zoom.js +++ b/lib/web/mage/zoom.js @@ -13,7 +13,7 @@ define([ 'jquery', 'mage/template', - 'jquery/ui' + 'jquery-ui-modules/widget' ], factory); } else { factory(root.jQuery, root.mageTemplate); diff --git a/lib/web/tiny_mce_4/jquery.tinymce.min.js b/lib/web/tiny_mce_4/jquery.tinymce.min.js index 651f1e0745b6..4a69c27b34ee 100755 --- a/lib/web/tiny_mce_4/jquery.tinymce.min.js +++ b/lib/web/tiny_mce_4/jquery.tinymce.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c;g("0",[],function(){return function(a){function b(){function a(a){"remove"===a&&this.each(function(a,b){var c=d(b);c&&c.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(a,b){var c=i().get(b.id.replace(/_parent$/,""));c&&c.remove()})}function b(b){var c,d=this;if(null!=b)a.call(d),d.each(function(a,c){var d;(d=i().get(c.id))&&d.setContent(b)});else if(d.length>0&&(c=i().get(d[0].id)))return c.getContent()}function d(a){var b=null;return a&&a.id&&g.tinymce&&(b=i().get(a.id)),b}function e(a){return!!(a&&a.length&&g.tinymce&&a.is(":tinymce"))}var h={};f.each(["text","html","val"],function(a,g){var i=h[g]=f.fn[g],j="text"===g;f.fn[g]=function(a){var g=this;if(!e(g))return i.apply(g,arguments);if(a!==c)return b.call(g.filter(":tinymce"),a),i.apply(g.not(":tinymce"),arguments),g;var h="",k=arguments;return(j?g:g.eq(0)).each(function(a,b){var c=d(b);h+=c?j?c.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):c.getContent({save:!0}):i.apply(f(b),k)}),h}}),f.each(["append","prepend"],function(a,b){var g=h[b]=f.fn[b],i="prepend"===b;f.fn[b]=function(a){var b=this;return e(b)?a!==c?("string"==typeof a&&b.filter(":tinymce").each(function(b,c){var e=d(c);e&&e.setContent(i?a+e.getContent():e.getContent()+a)}),g.apply(b.not(":tinymce"),arguments),b):void 0:g.apply(b,arguments)}}),f.each(["remove","replaceWith","replaceAll","empty"],function(b,c){var d=h[c]=f.fn[c];f.fn[c]=function(){return a.call(this,c),d.apply(this,arguments)}}),h.attr=f.fn.attr,f.fn.attr=function(a,g){var i=this,j=arguments;if(!a||"value"!==a||!e(i))return g!==c?h.attr.apply(i,j):h.attr.apply(i,j);if(g!==c)return b.call(i.filter(":tinymce"),g),h.attr.apply(i.not(":tinymce"),j),i;var k=i[0],l=d(k);return l?l.getContent({save:!0}):h.attr.apply(f(k),j)}}var c,d,e,f,g,h=[];g=a?a:window,f=g.jQuery;var i=function(){return g.tinymce};f.fn.tinymce=function(a){function c(){var c=[],d=0;e||(b(),e=!0),m.each(function(b,e){var f,g=e.id,h=a.oninit;g||(e.id=g=i().DOM.uniqueId()),i().get(g)||(f=i().createEditor(g,a),c.push(f),f.on("init",function(){var a,b=h;m.css("visibility",""),h&&++d==c.length&&("string"==typeof b&&(a=b.indexOf(".")===-1?null:i().resolve(b.replace(/\.\w+$/,"")),b=i().resolve(b)),b.apply(a||i(),c))}))}),f.each(c,function(a,b){b.render()})}var j,k,l,m=this,n="";if(!m.length)return m;if(!a)return i()?i().get(m[0].id):null;if(m.css("visibility","hidden"),g.tinymce||d||!(j=a.script_url))1===d?h.push(c):c();else{d=1,k=j.substring(0,j.lastIndexOf("/")),j.indexOf(".min")!=-1&&(n=".min"),g.tinymce=g.tinyMCEPreInit||{base:k,suffix:n},j.indexOf("gzip")!=-1&&(l=a.language||"en",j=j+(/\?/.test(j)?"&":"?")+"js=true&core=true&suffix="+escape(n)+"&themes="+escape(a.theme||"modern")+"&plugins="+escape(a.plugins||"")+"&languages="+(l||""),g.tinyMCE_GZ||(g.tinyMCE_GZ={start:function(){function b(a){i().ScriptLoader.markDone(i().baseURI.toAbsolute(a))}b("langs/"+l+".js"),b("themes/"+a.theme+"/theme"+n+".js"),b("themes/"+a.theme+"/langs/"+l+".js"),f.each(a.plugins.split(","),function(a,c){c&&(b("plugins/"+c+"/plugin"+n+".js"),b("plugins/"+c+"/langs/"+l+".js"))})},end:function(){}}));var o=document.createElement("script");o.type="text/javascript",o.onload=o.onreadystatechange=function(b){b=b||window.event,2===d||"load"!=b.type&&!/complete|loaded/.test(o.readyState)||(i().dom.Event.domLoaded=1,d=2,a.script_loaded&&a.script_loaded(),c(),f.each(h,function(a,b){b()}))},o.src=j,document.body.appendChild(o)}return m},f.extend(f.expr[":"],{tinymce:function(a){var b;return!!(a.id&&"tinymce"in g&&(b=i().get(a.id),b&&b.editorManager===i()))}})}}),d("0")()}(); \ No newline at end of file +!function(){var f,c,u,p,d,s=[];d="undefined"!=typeof global?global:window,p=d.jQuery;var v=function(){return d.tinymce};p.fn.tinymce=function(o){var e,t,i,l=this,r="";if(!l.length)return l;if(!o)return v()?v().get(l[0].id):null;l.css("visibility","hidden");var n=function(){var a=[],c=0;u||(m(),u=!0),l.each(function(e,t){var n,i=t.id,r=o.oninit;i||(t.id=i=v().DOM.uniqueId()),v().get(i)||(n=v().createEditor(i,o),a.push(n),n.on("init",function(){var e,t=r;l.css("visibility",""),r&&++c==a.length&&("string"==typeof t&&(e=-1===t.indexOf(".")?null:v().resolve(t.replace(/\.\w+$/,"")),t=v().resolve(t)),t.apply(e||v(),a))}))}),p.each(a,function(e,t){t.render()})};if(d.tinymce||c||!(e=o.script_url))1===c?s.push(n):n();else{c=1,t=e.substring(0,e.lastIndexOf("/")),-1!=e.indexOf(".min")&&(r=".min"),d.tinymce=d.tinyMCEPreInit||{base:t,suffix:r},-1!=e.indexOf("gzip")&&(i=o.language||"en",e=e+(/\?/.test(e)?"&":"?")+"js=true&core=true&suffix="+escape(r)+"&themes="+escape(o.theme||"modern")+"&plugins="+escape(o.plugins||"")+"&languages="+(i||""),d.tinyMCE_GZ||(d.tinyMCE_GZ={start:function(){var n=function(e){v().ScriptLoader.markDone(v().baseURI.toAbsolute(e))};n("langs/"+i+".js"),n("themes/"+o.theme+"/theme"+r+".js"),n("themes/"+o.theme+"/langs/"+i+".js"),p.each(o.plugins.split(","),function(e,t){t&&(n("plugins/"+t+"/plugin"+r+".js"),n("plugins/"+t+"/langs/"+i+".js"))})},end:function(){}}));var a=document.createElement("script");a.type="text/javascript",a.onload=a.onreadystatechange=function(e){e=e||window.event,2===c||"load"!=e.type&&!/complete|loaded/.test(a.readyState)||(v().dom.Event.domLoaded=1,c=2,o.script_loaded&&o.script_loaded(),n(),p.each(s,function(e,t){t()}))},a.src=e,document.body.appendChild(a)}return l},p.extend(p.expr[":"],{tinymce:function(e){var t;return!!(e.id&&"tinymce"in d&&(t=v().get(e.id))&&t.editorManager===v())}});var m=function(){var r=function(e){"remove"===e&&this.each(function(e,t){var n=l(t);n&&n.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(e,t){var n=v().get(t.id.replace(/_parent$/,""));n&&n.remove()})},o=function(i){var e,t=this;if(null!=i)r.call(t),t.each(function(e,t){var n;(n=v().get(t.id))&&n.setContent(i)});else if(0<t.length&&(e=v().get(t[0].id)))return e.getContent()},l=function(e){var t=null;return e&&e.id&&d.tinymce&&(t=v().get(e.id)),t},u=function(e){return!!(e&&e.length&&d.tinymce&&e.is(":tinymce"))},s={};p.each(["text","html","val"],function(e,t){var a=s[t]=p.fn[t],c="text"===t;p.fn[t]=function(e){var t=this;if(!u(t))return a.apply(t,arguments);if(e!==f)return o.call(t.filter(":tinymce"),e),a.apply(t.not(":tinymce"),arguments),t;var i="",r=arguments;return(c?t:t.eq(0)).each(function(e,t){var n=l(t);i+=n?c?n.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):n.getContent({save:!0}):a.apply(p(t),r)}),i}}),p.each(["append","prepend"],function(e,t){var n=s[t]=p.fn[t],r="prepend"===t;p.fn[t]=function(i){var e=this;return u(e)?i!==f?("string"==typeof i&&e.filter(":tinymce").each(function(e,t){var n=l(t);n&&n.setContent(r?i+n.getContent():n.getContent()+i)}),n.apply(e.not(":tinymce"),arguments),e):void 0:n.apply(e,arguments)}}),p.each(["remove","replaceWith","replaceAll","empty"],function(e,t){var n=s[t]=p.fn[t];p.fn[t]=function(){return r.call(this,t),n.apply(this,arguments)}}),s.attr=p.fn.attr,p.fn.attr=function(e,t){var n=this,i=arguments;if(!e||"value"!==e||!u(n))return s.attr.apply(n,i);if(t!==f)return o.call(n.filter(":tinymce"),t),s.attr.apply(n.not(":tinymce"),i),n;var r=n[0],a=l(r);return a?a.getContent({save:!0}):s.attr.apply(p(r),i)}}}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/advlist/plugin.min.js b/lib/web/tiny_mce_4/plugins/advlist/plugin.min.js index 440152f6aeb9..122cd8ff6469 100755 --- a/lib/web/tiny_mce_4/plugins/advlist/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/advlist/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("advlist",function(a){function c(b){return a.$.contains(a.getBody(),b)}function d(a){return a&&/^(OL|UL|DL)$/.test(a.nodeName)&&c(a)}function e(a,c){var d=[];return c&&b.each(c.split(/[ ,]/),function(a){d.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),d}function f(c,d){a.undoManager.transact(function(){var e,f=a.dom,g=a.selection;if(e=f.getParent(g.getNode(),"ol,ul"),!e||e.nodeName!=c||d===!1){var h={"list-style-type":d?d:""};a.execCommand("UL"==c?"InsertUnorderedList":"InsertOrderedList",!1,h)}e=f.getParent(g.getNode(),"ol,ul"),e&&b.each(f.select("ol,ul",e).concat([e]),function(a){a.nodeName!==c&&d!==!1&&(a=f.rename(a,c)),f.setStyle(a,"listStyleType",d?d:null),a.removeAttribute("data-mce-style")}),a.focus()})}function g(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var h,i,j=function(a,c){var d=a.settings.plugins?a.settings.plugins:"";return b.inArray(d.split(/[ ,]/),c)!==-1};h=e("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),i=e("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square"));var k=function(c){return function(){var e=this;a.on("NodeChange",function(a){var f=b.grep(a.parents,d);e.active(f.length>0&&f[0].nodeName===c)})}};j(a,"lists")&&(a.addCommand("ApplyUnorderedListStyle",function(a,b){f("UL",b["list-style-type"])}),a.addCommand("ApplyOrderedListStyle",function(a,b){f("OL",b["list-style-type"])}),a.addButton("numlist",{type:h.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:h,onPostRender:k("OL"),onshow:g,onselect:function(a){f("OL",a.control.settings.data)},onclick:function(){f("OL",!1)}}),a.addButton("bullist",{type:i.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:k("UL"),menu:i,onshow:g,onselect:function(a){f("UL",a.control.settings.data)},onclick:function(){f("UL",!1)}}))}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.util.Tools"),s=function(t,e,n){var r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===n?null:{"list-style-type":n})},o=function(n){n.addCommand("ApplyUnorderedListStyle",function(t,e){s(n,"UL",e["list-style-type"])}),n.addCommand("ApplyOrderedListStyle",function(t,e){s(n,"OL",e["list-style-type"])})},e=function(t){var e=t.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman");return e?e.split(/[ ,]/):[]},n=function(t){var e=t.getParam("advlist_bullet_styles","default,circle,disc,square");return e?e.split(/[ ,]/):[]},u=function(t){return t&&/^(TH|TD)$/.test(t.nodeName)},c=function(r){return function(t){return t&&/^(OL|UL|DL)$/.test(t.nodeName)&&(n=t,(e=r).$.contains(e.getBody(),n));var e,n}},d=function(t){var e=t.dom.getParent(t.selection.getNode(),"ol,ul");return t.dom.getStyle(e,"listStyleType")||""},p=function(t){return a.map(t,function(t){return{text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()}),data:"default"===t?"":t}})},f=function(i,l){return function(t){var o=t.control;i.on("NodeChange",function(t){var e=function(t,e){for(var n=0;n<t.length;n++)if(e(t[n]))return n;return-1}(t.parents,u),n=-1!==e?t.parents.slice(0,e):t.parents,r=a.grep(n,c(i));o.active(0<r.length&&r[0].nodeName===l)})}},m=function(e,t,n,r,o,i){var l;e.addButton(t,{active:!1,type:"splitbutton",tooltip:n,menu:p(i),onPostRender:f(e,o),onshow:(l=e,function(t){var e=d(l);t.control.items().each(function(t){t.active(t.settings.data===e)})}),onselect:function(t){s(e,o,t.control.settings.data)},onclick:function(){e.execCommand(r)}})},r=function(t,e,n,r,o,i){var l,a,s,u,c;0<i.length?m(t,e,n,r,o,i):(a=e,s=n,u=r,c=o,(l=t).addButton(a,{active:!1,type:"button",tooltip:s,onPostRender:f(l,c),onclick:function(){l.execCommand(u)}}))},i=function(t){r(t,"numlist","Numbered list","InsertOrderedList","OL",e(t)),r(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))};t.add("advlist",function(t){var e,n,r;n="lists",r=(e=t).settings.plugins?e.settings.plugins:"",-1!==a.inArray(r.split(/[ ,]/),n)&&(i(t),o(t))})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/anchor/plugin.min.js b/lib/web/tiny_mce_4/plugins/anchor/plugin.min.js index 6b3361d26fc7..177f5051755a 100755 --- a/lib/web/tiny_mce_4/plugins/anchor/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/anchor/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.Env")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){return b.add("anchor",function(b){var c=function(a){return!a.attr("href")&&(a.attr("id")||a.attr("name"))&&!a.firstChild},d=function(a){return function(b){for(var d=0;d<b.length;d++)c(b[d])&&b[d].attr("contenteditable",a)}},e=function(a){return/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(a)},f=function(){var a=b.selection.getNode(),c="A"==a.tagName&&""===b.dom.getAttrib(a,"href"),d="";c&&(d=a.id||a.name||""),b.windowManager.open({title:"Anchor",body:{type:"textbox",name:"id",size:40,label:"Id",value:d},onsubmit:function(d){var f=d.data.id;return e(f)?void(c?(a.removeAttribute("name"),a.id=f):(b.selection.collapse(!0),b.execCommand("mceInsertContent",!1,b.dom.createHTML("a",{id:f})))):(d.preventDefault(),void b.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."))}})};a.ceFalse&&b.on("PreInit",function(){b.parser.addNodeFilter("a",d("false")),b.serializer.addNodeFilter("a",d(null))}),b.addCommand("mceAnchor",f),b.addButton("anchor",{icon:"anchor",tooltip:"Anchor",onclick:f,stateSelector:"a:not([href])"}),b.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",onclick:f})}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)},e=function(t){var e=t.selection.getNode();return"A"===e.tagName&&""===t.dom.getAttrib(e,"href")?e.id||e.name:""},i=function(t,e){var n=t.selection.getNode();"A"===n.tagName&&""===t.dom.getAttrib(n,"href")?(n.removeAttribute("name"),n.id=e,t.undoManager.add()):(t.focus(),t.selection.collapse(!0),t.execCommand("mceInsertContent",!1,t.dom.createHTML("a",{id:e})))},n=function(r){var t=e(r);r.windowManager.open({title:"Anchor",body:{type:"textbox",name:"id",size:40,label:"Id",value:t},onsubmit:function(t){var e,n,o=t.data.id;e=r,(a(n=o)?(i(e,n),0):(e.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),1))&&t.preventDefault()}})},o=function(t){t.addCommand("mceAnchor",function(){n(t)})},r=function(o){return function(t){for(var e=0;e<t.length;e++)(n=t[e]).attr("href")||!n.attr("id")&&!n.attr("name")||n.firstChild||t[e].attr("contenteditable",o);var n}},c=function(t){t.on("PreInit",function(){t.parser.addNodeFilter("a",r("false")),t.serializer.addNodeFilter("a",r(null))})},d=function(t){t.addButton("anchor",{icon:"anchor",tooltip:"Anchor",cmd:"mceAnchor",stateSelector:"a:not([href])"}),t.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",cmd:"mceAnchor"})};t.add("anchor",function(t){c(t),o(t),d(t)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/autolink/plugin.min.js b/lib/web/tiny_mce_4/plugins/autolink/plugin.min.js index 5d1231e7373e..1238a1d231cf 100755 --- a/lib/web/tiny_mce_4/plugins/autolink/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/autolink/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.Env")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){return b.add("autolink",function(b){function c(a){f(a,-1,"(",!0)}function d(a){f(a,0,"",!0)}function e(a){f(a,-1,"",!1)}function f(a,b,c){function d(a,b){if(b<0&&(b=0),3==a.nodeType){var c=a.data.length;b>c&&(b=c)}return b}function e(a,b){1!=a.nodeType||a.hasChildNodes()?g.setStart(a,d(a,b)):g.setStartBefore(a)}function f(a,b){1!=a.nodeType||a.hasChildNodes()?g.setEnd(a,d(a,b)):g.setEndAfter(a)}var g,i,j,k,l,m,n,o,p,q;if("A"!=a.selection.getNode().tagName){if(g=a.selection.getRng(!0).cloneRange(),g.startOffset<5){if(o=g.endContainer.previousSibling,!o){if(!g.endContainer.firstChild||!g.endContainer.firstChild.nextSibling)return;o=g.endContainer.firstChild.nextSibling}if(p=o.length,e(o,p),f(o,p),g.endOffset<5)return;i=g.endOffset,k=o}else{if(k=g.endContainer,3!=k.nodeType&&k.firstChild){for(;3!=k.nodeType&&k.firstChild;)k=k.firstChild;3==k.nodeType&&(e(k,0),f(k,k.nodeValue.length))}i=1==g.endOffset?2:g.endOffset-1-b}j=i;do e(k,i>=2?i-2:0),f(k,i>=1?i-1:0),i-=1,q=g.toString();while(" "!=q&&""!==q&&160!=q.charCodeAt(0)&&i-2>=0&&q!=c);g.toString()==c||160==g.toString().charCodeAt(0)?(e(k,i),f(k,j),i+=1):0===g.startOffset?(e(k,0),f(k,j)):(e(k,i),f(k,j)),m=g.toString(),"."==m.charAt(m.length-1)&&f(k,j-1),m=g.toString(),n=m.match(h),n&&("www."==n[1]?n[1]="http://www.":/@$/.test(n[1])&&!/^mailto:/.test(n[1])&&(n[1]="mailto:"+n[1]),l=a.selection.getBookmark(),a.selection.setRng(g),a.execCommand("createlink",!1,n[1]+n[2]),a.settings.default_link_target&&a.dom.setAttrib(a.selection.getNode(),"target",a.settings.default_link_target),a.selection.moveToBookmark(l),a.nodeChanged())}}var g,h=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return b.settings.autolink_pattern&&(h=b.settings.autolink_pattern),b.on("keydown",function(a){if(13==a.keyCode)return e(b)}),a.ie?void b.on("focus",function(){if(!g){g=!0;try{b.execCommand("AutoUrlDetect",!1,!0)}catch(a){}}}):(b.on("keypress",function(a){if(41==a.keyCode)return c(b)}),void b.on("keyup",function(a){if(32==a.keyCode)return d(b)}))}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),i=tinymce.util.Tools.resolve("tinymce.Env"),m=function(e){return e.getParam("autolink_pattern",/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i)},y=function(e){return e.getParam("default_link_target","")},o=function(e,t){if(t<0&&(t=0),3===e.nodeType){var n=e.data.length;n<t&&(t=n)}return t},k=function(e,t,n){1!==t.nodeType||t.hasChildNodes()?e.setStart(t,o(t,n)):e.setStartBefore(t)},p=function(e,t,n){1!==t.nodeType||t.hasChildNodes()?e.setEnd(t,o(t,n)):e.setEndAfter(t)},r=function(e,t,n){var i,o,r,a,f,s,d,l,c,u,g=m(e),h=y(e);if("A"!==e.selection.getNode().tagName){if((i=e.selection.getRng(!0).cloneRange()).startOffset<5){if(!(l=i.endContainer.previousSibling)){if(!i.endContainer.firstChild||!i.endContainer.firstChild.nextSibling)return;l=i.endContainer.firstChild.nextSibling}if(c=l.length,k(i,l,c),p(i,l,c),i.endOffset<5)return;o=i.endOffset,a=l}else{if(3!==(a=i.endContainer).nodeType&&a.firstChild){for(;3!==a.nodeType&&a.firstChild;)a=a.firstChild;3===a.nodeType&&(k(i,a,0),p(i,a,a.nodeValue.length))}o=1===i.endOffset?2:i.endOffset-1-t}for(r=o;k(i,a,2<=o?o-2:0),p(i,a,1<=o?o-1:0),o-=1," "!==(u=i.toString())&&""!==u&&160!==u.charCodeAt(0)&&0<=o-2&&u!==n;);var C;(C=i.toString())===n||" "===C||160===C.charCodeAt(0)?(k(i,a,o),p(i,a,r),o+=1):(0===i.startOffset?k(i,a,0):k(i,a,o),p(i,a,r)),"."===(s=i.toString()).charAt(s.length-1)&&p(i,a,r-1),(d=(s=i.toString().trim()).match(g))&&("www."===d[1]?d[1]="http://www.":/@$/.test(d[1])&&!/^mailto:/.test(d[1])&&(d[1]="mailto:"+d[1]),f=e.selection.getBookmark(),e.selection.setRng(i),e.execCommand("createlink",!1,d[1]+d[2]),h&&e.dom.setAttrib(e.selection.getNode(),"target",h),e.selection.moveToBookmark(f),e.nodeChanged())}},t=function(t){var n;t.on("keydown",function(e){13!==e.keyCode||r(t,-1,"")}),i.ie?t.on("focus",function(){if(!n){n=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(t.on("keypress",function(e){41!==e.keyCode||r(t,-1,"(")}),t.on("keyup",function(e){32!==e.keyCode||r(t,0,"")}))};e.add("autolink",function(e){t(e)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/autoresize/plugin.min.js b/lib/web/tiny_mce_4/plugins/autoresize/plugin.min.js index 081b5d32dc29..f63945671a6e 100755 --- a/lib/web/tiny_mce_4/plugins/autoresize/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/autoresize/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("5",tinymce.util.Tools.resolve),g("1",["5"],function(a){return a("tinymce.dom.DOMUtils")}),g("2",["5"],function(a){return a("tinymce.Env")}),g("3",["5"],function(a){return a("tinymce.PluginManager")}),g("4",["5"],function(a){return a("tinymce.util.Delay")}),g("0",["1","2","3","4"],function(a,b,c,d){var e=a.DOM;return c.add("autoresize",function(a){function c(){return a.plugins.fullscreen&&a.plugins.fullscreen.isFullscreen()}function f(d){var g,j,k,l,m,n,o,p,q,r,s,t;if(j=a.getDoc()){if(k=j.body,l=j.documentElement,m=h.autoresize_min_height,!k||d&&"setcontent"===d.type&&d.initial||c())return void(k&&l&&(k.style.overflowY="auto",l.style.overflowY="auto"));o=a.dom.getStyle(k,"margin-top",!0),p=a.dom.getStyle(k,"margin-bottom",!0),q=a.dom.getStyle(k,"padding-top",!0),r=a.dom.getStyle(k,"padding-bottom",!0),s=a.dom.getStyle(k,"border-top-width",!0),t=a.dom.getStyle(k,"border-bottom-width",!0),n=k.offsetHeight+parseInt(o,10)+parseInt(p,10)+parseInt(q,10)+parseInt(r,10)+parseInt(s,10)+parseInt(t,10),(isNaN(n)||n<=0)&&(n=b.ie?k.scrollHeight:b.webkit&&0===k.clientHeight?0:k.offsetHeight),n>h.autoresize_min_height&&(m=n),h.autoresize_max_height&&n>h.autoresize_max_height?(m=h.autoresize_max_height,k.style.overflowY="auto",l.style.overflowY="auto"):(k.style.overflowY="hidden",l.style.overflowY="hidden",k.scrollTop=0),m!==i&&(g=m-i,e.setStyle(a.iframeElement,"height",m+"px"),i=m,b.webKit&&g<0&&f(d))}}function g(b,c,e){d.setEditorTimeout(a,function(){f({}),b--?g(b,c,e):e&&e()},c)}var h=a.settings,i=0;a.settings.inline||(h.autoresize_min_height=parseInt(a.getParam("autoresize_min_height",a.getElement().offsetHeight),10),h.autoresize_max_height=parseInt(a.getParam("autoresize_max_height",0),10),a.on("init",function(){var b,c;b=a.getParam("autoresize_overflow_padding",1),c=a.getParam("autoresize_bottom_margin",50),b!==!1&&a.dom.setStyles(a.getBody(),{paddingLeft:b,paddingRight:b}),c!==!1&&a.dom.setStyles(a.getBody(),{paddingBottom:c})}),a.on("nodechange setcontent keyup FullscreenStateChanged",f),a.getParam("autoresize_on_init",!0)&&a.on("init",function(){g(20,100,function(){g(5,1e3)})}),a.addCommand("mceAutoResize",f))}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var i=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return i(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),y=tinymce.util.Tools.resolve("tinymce.Env"),r=tinymce.util.Tools.resolve("tinymce.util.Delay"),h=function(t){return parseInt(t.getParam("autoresize_min_height",t.getElement().offsetHeight),10)},v=function(t){return parseInt(t.getParam("autoresize_max_height",0),10)},o=function(t){return t.getParam("autoresize_overflow_padding",1)},a=function(t){return t.getParam("autoresize_bottom_margin",50)},n=function(t){return t.getParam("autoresize_on_init",!0)},u=function(t,e,n,i,o){r.setEditorTimeout(t,function(){_(t,e),n--?u(t,e,n,i,o):o&&o()},i)},S=function(t,e){var n=t.getBody();n&&(n.style.overflowY=e?"":"hidden",e||(n.scrollTop=0))},_=function(t,e){var n,i,o,r,a,u,s,l,g,c,f,d=t.dom;if(i=t.getDoc())if((m=t).plugins.fullscreen&&m.plugins.fullscreen.isFullscreen())S(t,!0);else{var m;o=i.body,r=h(t),u=d.getStyle(o,"margin-top",!0),s=d.getStyle(o,"margin-bottom",!0),l=d.getStyle(o,"padding-top",!0),g=d.getStyle(o,"padding-bottom",!0),c=d.getStyle(o,"border-top-width",!0),f=d.getStyle(o,"border-bottom-width",!0),a=o.offsetHeight+parseInt(u,10)+parseInt(s,10)+parseInt(l,10)+parseInt(g,10)+parseInt(c,10)+parseInt(f,10),(isNaN(a)||a<=0)&&(a=y.ie?o.scrollHeight:y.webkit&&0===o.clientHeight?0:o.offsetHeight),a>h(t)&&(r=a);var p=v(t);p&&p<a?(r=p,S(t,!0)):S(t,!1),r!==e.get()&&(n=r-e.get(),d.setStyle(t.iframeElement,"height",r+"px"),e.set(r),y.webkit&&n<0&&_(t,e))}},s={setup:function(i,e){i.on("init",function(){var t,e,n=i.dom;t=o(i),e=a(i),!1!==t&&n.setStyles(i.getBody(),{paddingLeft:t,paddingRight:t}),!1!==e&&n.setStyles(i.getBody(),{paddingBottom:e})}),i.on("nodechange setcontent keyup FullscreenStateChanged",function(t){_(i,e)}),n(i)&&i.on("init",function(){u(i,e,20,100,function(){u(i,e,5,1e3)})})},resize:_},l=function(t,e){t.addCommand("mceAutoResize",function(){s.resize(t,e)})};t.add("autoresize",function(t){if(!t.inline){var e=i(0);l(t,e),s.setup(t,e)}})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/autosave/plugin.min.js b/lib/web/tiny_mce_4/plugins/autosave/plugin.min.js index e9eb688964a9..1eab72013822 100755 --- a/lib/web/tiny_mce_4/plugins/autosave/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/autosave/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("6",tinymce.util.Tools.resolve),g("1",["6"],function(a){return a("tinymce.EditorManager")}),g("2",["6"],function(a){return a("tinymce.PluginManager")}),g("3",["6"],function(a){return a("tinymce.util.LocalStorage")}),g("4",["6"],function(a){return a("tinymce.util.Tools")}),h("5",window),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return a._beforeUnloadHandler=function(){var b;return d.each(a.editors,function(a){a.plugins.autosave&&a.plugins.autosave.storeDraft(),!b&&a.isDirty()&&a.getParam("autosave_ask_before_unload",!0)&&(b=a.translate("You have unsaved changes are you sure you want to navigate away?"))}),b},b.add("autosave",function(b){function f(a,b){var c={s:1e3,m:6e4};return a=/^(\d+)([ms]?)$/.exec(""+(a||b)),(a[2]?c[a[2]]:1)*parseInt(a,10)}function g(){var a=parseInt(c.getItem(o+"time"),10)||0;return!((new Date).getTime()-a>q.autosave_retention)||(h(!1),!1)}function h(a){c.removeItem(o+"draft"),c.removeItem(o+"time"),a!==!1&&b.fire("RemoveDraft")}function i(){!n()&&b.isDirty()&&(c.setItem(o+"draft",b.getContent({format:"raw",no_events:!0})),c.setItem(o+"time",(new Date).getTime()),b.fire("StoreDraft"))}function j(){g()&&(b.setContent(c.getItem(o+"draft"),{format:"raw"}),b.fire("RestoreDraft"))}function k(){p||(setInterval(function(){b.removed||i()},q.autosave_interval),p=!0)}function l(){var a=this;a.disabled(!g()),b.on("StoreDraft RestoreDraft RemoveDraft",function(){a.disabled(!g())}),k()}function m(){b.undoManager.beforeChange(),j(),h(),b.undoManager.add()}function n(a){var c=b.settings.forced_root_block;return a=d.trim("undefined"==typeof a?b.getBody().innerHTML:a),""===a||new RegExp("^<"+c+"[^>]*>((\xa0| |[ \t]|<br[^>]*>)+?|)</"+c+">|<br>$","i").test(a)}var o,p,q=b.settings;o=q.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",o=o.replace(/\{path\}/g,document.location.pathname),o=o.replace(/\{query\}/g,document.location.search),o=o.replace(/\{id\}/g,b.id),q.autosave_interval=f(q.autosave_interval,"30s"),q.autosave_retention=f(q.autosave_retention,"20m"),b.addButton("restoredraft",{title:"Restore last draft",onclick:m,onPostRender:l}),b.addMenuItem("restoredraft",{text:"Restore last draft",onclick:m,onPostRender:l,context:"file"}),b.settings.autosave_restore_when_empty!==!1&&(b.on("init",function(){g()&&n()&&j()}),b.on("saveContent",function(){h()})),e.onbeforeunload=a._beforeUnloadHandler,this.hasDraft=g,this.storeDraft=i,this.restoreDraft=j,this.removeDraft=h,this.isEmpty=n}),function(){}}),d("0")()}(); \ No newline at end of file +!function(a){"use strict";var i=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return i(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=function(t,e){var n=t||e,r=/^(\d+)([ms]?)$/.exec(""+n);return(r[2]?{s:1e3,m:6e4}[r[2]]:1)*parseInt(n,10)},s=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,a.document.location.pathname)).replace(/\{query\}/g,a.document.location.search)).replace(/\{hash\}/g,a.document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t,e){var n=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+n+"[^>]*>((\xa0| |[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(e)},f=function(t){var e=parseInt(r.getItem(s(t)+"time"),10)||0;return!((new Date).getTime()-e>u(t.settings.autosave_retention,"20m")&&(l(t,!1),1))},l=function(t,e){var n=s(t);r.removeItem(n+"draft"),r.removeItem(n+"time"),!1!==e&&t.fire("RemoveDraft")},m=function(t){var e=s(t);!c(t)&&t.isDirty()&&(r.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),r.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},v=function(t){var e=s(t);f(t)&&(t.setContent(r.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},d=function(t,e){var n=u(t.settings.autosave_interval,"30s");e.get()||(setInterval(function(){t.removed||m(t)},n),e.set(!0))},g=function(t){t.undoManager.transact(function(){v(t),l(t)}),t.focus()};function y(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var p=tinymce.util.Tools.resolve("tinymce.EditorManager");p._beforeUnloadHandler=function(){var e;return o.each(p.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var h=function(n,r){return function(t){var e=t.control;e.disabled(!f(n)),n.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!f(n))}),d(n,r)}};t.add("autosave",function(t){var e,n,r,o=i(!1);return a.window.onbeforeunload=p._beforeUnloadHandler,n=o,(e=t).addButton("restoredraft",{title:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n)}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n),context:"file"}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&v(t)}),{hasDraft:y(f,r=t),storeDraft:y(m,r),restoreDraft:y(v,r),removeDraft:y(l,r),isEmpty:y(c,r)}})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/bbcode/plugin.min.js b/lib/web/tiny_mce_4/plugins/bbcode/plugin.min.js index 966358a0d26f..b2b9d701c666 100755 --- a/lib/web/tiny_mce_4/plugins/bbcode/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/bbcode/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("bbcode",function(){return{init:function(a){var b=this,c=a.getParam("bbcode_dialect","punbb").toLowerCase();a.on("beforeSetContent",function(a){a.content=b["_"+c+"_bbcode2html"](a.content)}),a.on("postProcess",function(a){a.set&&(a.content=b["_"+c+"_bbcode2html"](a.content)),a.get&&(a.content=b["_"+c+"_html2bbcode"](a.content))})},getInfo:function(){return{longname:"BBCode Plugin",author:"Ephox Corp",authorurl:"http://www.tinymce.com",infourl:"http://www.tinymce.com/wiki.php/Plugin:bbcode"}},_punbb_html2bbcode:function(a){function c(b,c){a=a.replace(b,c)}return a=b.trim(a),c(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),c(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),c(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),c(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),c(/<font>(.*?)<\/font>/gi,"$1"),c(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),c(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),c(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),c(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),c(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),c(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),c(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),c(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),c(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),c(/<\/(strong|b)>/gi,"[/b]"),c(/<(strong|b)>/gi,"[b]"),c(/<\/(em|i)>/gi,"[/i]"),c(/<(em|i)>/gi,"[i]"),c(/<\/u>/gi,"[/u]"),c(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),c(/<u>/gi,"[u]"),c(/<blockquote[^>]*>/gi,"[quote]"),c(/<\/blockquote>/gi,"[/quote]"),c(/<br \/>/gi,"\n"),c(/<br\/>/gi,"\n"),c(/<br>/gi,"\n"),c(/<p>/gi,""),c(/<\/p>/gi,"\n"),c(/ |\u00a0/gi," "),c(/"/gi,'"'),c(/</gi,"<"),c(/>/gi,">"),c(/&/gi,"&"),a},_punbb_bbcode2html:function(a){function c(b,c){a=a.replace(b,c)}return a=b.trim(a),c(/\n/gi,"<br />"),c(/\[b\]/gi,"<strong>"),c(/\[\/b\]/gi,"</strong>"),c(/\[i\]/gi,"<em>"),c(/\[\/i\]/gi,"</em>"),c(/\[u\]/gi,"<u>"),c(/\[\/u\]/gi,"</u>"),c(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),c(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),c(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),c(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),c(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span> '),c(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span> '),a}}}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var o=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.util.Tools"),e=function(e){e=t.trim(e);var o=function(o,t){e=e.replace(o,t)};return o(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),o(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),o(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),o(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),o(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),o(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),o(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),o(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),o(/<font>(.*?)<\/font>/gi,"$1"),o(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),o(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),o(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),o(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),o(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),o(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),o(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),o(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),o(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),o(/<\/(strong|b)>/gi,"[/b]"),o(/<(strong|b)>/gi,"[b]"),o(/<\/(em|i)>/gi,"[/i]"),o(/<(em|i)>/gi,"[i]"),o(/<\/u>/gi,"[/u]"),o(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),o(/<u>/gi,"[u]"),o(/<blockquote[^>]*>/gi,"[quote]"),o(/<\/blockquote>/gi,"[/quote]"),o(/<br \/>/gi,"\n"),o(/<br\/>/gi,"\n"),o(/<br>/gi,"\n"),o(/<p>/gi,""),o(/<\/p>/gi,"\n"),o(/ |\u00a0/gi," "),o(/"/gi,'"'),o(/</gi,"<"),o(/>/gi,">"),o(/&/gi,"&"),e},i=function(e){e=t.trim(e);var o=function(o,t){e=e.replace(o,t)};return o(/\n/gi,"<br />"),o(/\[b\]/gi,"<strong>"),o(/\[\/b\]/gi,"</strong>"),o(/\[i\]/gi,"<em>"),o(/\[\/i\]/gi,"</em>"),o(/\[u\]/gi,"<u>"),o(/\[\/u\]/gi,"</u>"),o(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),o(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),o(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),o(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),o(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span> '),o(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span> '),e};o.add("bbcode",function(){return{init:function(o){o.on("beforeSetContent",function(o){o.content=i(o.content)}),o.on("postProcess",function(o){o.set&&(o.content=i(o.content)),o.get&&(o.content=e(o.content))})}}})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/charmap/plugin.min.js b/lib/web/tiny_mce_4/plugins/charmap/plugin.min.js index 3fd9e23d5d87..9ea3f757513c 100755 --- a/lib/web/tiny_mce_4/plugins/charmap/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/charmap/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("charmap",function(a){function c(){return[["160","no-break space"],["173","soft hyphen"],["34","quotation mark"],["162","cent sign"],["8364","euro sign"],["163","pound sign"],["165","yen sign"],["169","copyright sign"],["174","registered sign"],["8482","trade mark sign"],["8240","per mille sign"],["181","micro sign"],["183","middle dot"],["8226","bullet"],["8230","three dot leader"],["8242","minutes / feet"],["8243","seconds / inches"],["167","section sign"],["182","paragraph sign"],["223","sharp s / ess-zed"],["8249","single left-pointing angle quotation mark"],["8250","single right-pointing angle quotation mark"],["171","left pointing guillemet"],["187","right pointing guillemet"],["8216","left single quotation mark"],["8217","right single quotation mark"],["8220","left double quotation mark"],["8221","right double quotation mark"],["8218","single low-9 quotation mark"],["8222","double low-9 quotation mark"],["60","less-than sign"],["62","greater-than sign"],["8804","less-than or equal to"],["8805","greater-than or equal to"],["8211","en dash"],["8212","em dash"],["175","macron"],["8254","overline"],["164","currency sign"],["166","broken bar"],["168","diaeresis"],["161","inverted exclamation mark"],["191","turned question mark"],["710","circumflex accent"],["732","small tilde"],["176","degree sign"],["8722","minus sign"],["177","plus-minus sign"],["247","division sign"],["8260","fraction slash"],["215","multiplication sign"],["185","superscript one"],["178","superscript two"],["179","superscript three"],["188","fraction one quarter"],["189","fraction one half"],["190","fraction three quarters"],["402","function / florin"],["8747","integral"],["8721","n-ary sumation"],["8734","infinity"],["8730","square root"],["8764","similar to"],["8773","approximately equal to"],["8776","almost equal to"],["8800","not equal to"],["8801","identical to"],["8712","element of"],["8713","not an element of"],["8715","contains as member"],["8719","n-ary product"],["8743","logical and"],["8744","logical or"],["172","not sign"],["8745","intersection"],["8746","union"],["8706","partial differential"],["8704","for all"],["8707","there exists"],["8709","diameter"],["8711","backward difference"],["8727","asterisk operator"],["8733","proportional to"],["8736","angle"],["180","acute accent"],["184","cedilla"],["170","feminine ordinal indicator"],["186","masculine ordinal indicator"],["8224","dagger"],["8225","double dagger"],["192","A - grave"],["193","A - acute"],["194","A - circumflex"],["195","A - tilde"],["196","A - diaeresis"],["197","A - ring above"],["256","A - macron"],["198","ligature AE"],["199","C - cedilla"],["200","E - grave"],["201","E - acute"],["202","E - circumflex"],["203","E - diaeresis"],["274","E - macron"],["204","I - grave"],["205","I - acute"],["206","I - circumflex"],["207","I - diaeresis"],["298","I - macron"],["208","ETH"],["209","N - tilde"],["210","O - grave"],["211","O - acute"],["212","O - circumflex"],["213","O - tilde"],["214","O - diaeresis"],["216","O - slash"],["332","O - macron"],["338","ligature OE"],["352","S - caron"],["217","U - grave"],["218","U - acute"],["219","U - circumflex"],["220","U - diaeresis"],["362","U - macron"],["221","Y - acute"],["376","Y - diaeresis"],["562","Y - macron"],["222","THORN"],["224","a - grave"],["225","a - acute"],["226","a - circumflex"],["227","a - tilde"],["228","a - diaeresis"],["229","a - ring above"],["257","a - macron"],["230","ligature ae"],["231","c - cedilla"],["232","e - grave"],["233","e - acute"],["234","e - circumflex"],["235","e - diaeresis"],["275","e - macron"],["236","i - grave"],["237","i - acute"],["238","i - circumflex"],["239","i - diaeresis"],["299","i - macron"],["240","eth"],["241","n - tilde"],["242","o - grave"],["243","o - acute"],["244","o - circumflex"],["245","o - tilde"],["246","o - diaeresis"],["248","o slash"],["333","o macron"],["339","ligature oe"],["353","s - caron"],["249","u - grave"],["250","u - acute"],["251","u - circumflex"],["252","u - diaeresis"],["363","u - macron"],["253","y - acute"],["254","thorn"],["255","y - diaeresis"],["563","y - macron"],["913","Alpha"],["914","Beta"],["915","Gamma"],["916","Delta"],["917","Epsilon"],["918","Zeta"],["919","Eta"],["920","Theta"],["921","Iota"],["922","Kappa"],["923","Lambda"],["924","Mu"],["925","Nu"],["926","Xi"],["927","Omicron"],["928","Pi"],["929","Rho"],["931","Sigma"],["932","Tau"],["933","Upsilon"],["934","Phi"],["935","Chi"],["936","Psi"],["937","Omega"],["945","alpha"],["946","beta"],["947","gamma"],["948","delta"],["949","epsilon"],["950","zeta"],["951","eta"],["952","theta"],["953","iota"],["954","kappa"],["955","lambda"],["956","mu"],["957","nu"],["958","xi"],["959","omicron"],["960","pi"],["961","rho"],["962","final sigma"],["963","sigma"],["964","tau"],["965","upsilon"],["966","phi"],["967","chi"],["968","psi"],["969","omega"],["8501","alef symbol"],["982","pi symbol"],["8476","real part symbol"],["978","upsilon - hook symbol"],["8472","Weierstrass p"],["8465","imaginary part"],["8592","leftwards arrow"],["8593","upwards arrow"],["8594","rightwards arrow"],["8595","downwards arrow"],["8596","left right arrow"],["8629","carriage return"],["8656","leftwards double arrow"],["8657","upwards double arrow"],["8658","rightwards double arrow"],["8659","downwards double arrow"],["8660","left right double arrow"],["8756","therefore"],["8834","subset of"],["8835","superset of"],["8836","not a subset of"],["8838","subset of or equal to"],["8839","superset of or equal to"],["8853","circled plus"],["8855","circled times"],["8869","perpendicular"],["8901","dot operator"],["8968","left ceiling"],["8969","right ceiling"],["8970","left floor"],["8971","right floor"],["9001","left-pointing angle bracket"],["9002","right-pointing angle bracket"],["9674","lozenge"],["9824","black spade suit"],["9827","black club suit"],["9829","black heart suit"],["9830","black diamond suit"],["8194","en space"],["8195","em space"],["8201","thin space"],["8204","zero width non-joiner"],["8205","zero width joiner"],["8206","left-to-right mark"],["8207","right-to-left mark"]]}function d(a){return b.grep(a,function(a){return j(a)&&2==a.length})}function e(a){return j(a)?[].concat(d(a)):"function"==typeof a?a():[]}function f(b){var c=a.settings;return c.charmap&&(b=e(c.charmap)),c.charmap_append?[].concat(b).concat(e(c.charmap_append)):b}function g(){return f(c())}function h(b){a.fire("insertCustomChar",{chr:b}).chr,a.execCommand("mceInsertContent",!1,b)}function i(){function b(a){for(;a;){if("TD"==a.nodeName)return a;a=a.parentNode}}var c,d,e,f;c='<table role="presentation" cellspacing="0" class="mce-charmap"><tbody>';var i=g(),j=Math.min(i.length,25),k=Math.ceil(i.length/j);for(e=0;e<k;e++){for(c+="<tr>",d=0;d<j;d++){var l=e*j+d;if(l<i.length){var m=i[l],n=m?String.fromCharCode(parseInt(m[0],10)):" ";c+='<td title="'+m[1]+'"><div tabindex="-1" title="'+m[1]+'" role="button" data-chr="'+n+'">'+n+"</div></td>"}else c+="<td />"}c+="</tr>"}c+="</tbody></table>";var o={type:"container",html:c,onclick:function(a){var c=a.target;if(/^(TD|DIV)$/.test(c.nodeName)){var d=b(c).firstChild;d&&d.hasAttribute("data-chr")&&(h(d.getAttribute("data-chr")),a.ctrlKey||f.close())}},onmouseover:function(a){var c=b(a.target);c&&c.firstChild?(f.find("#preview").text(c.firstChild.firstChild.data),f.find("#previewTitle").text(c.title)):(f.find("#preview").text(" "),f.find("#previewTitle").text(" "))}};f=a.windowManager.open({title:"Special character",spacing:10,padding:10,items:[o,{type:"container",layout:"flex",direction:"column",align:"center",spacing:5,minWidth:160,minHeight:160,items:[{type:"label",name:"preview",text:" ",style:"font-size: 40px; text-align: center",border:1,minWidth:140,minHeight:80},{type:"spacer",minHeight:20},{type:"label",name:"previewTitle",text:" ",style:"white-space: pre-wrap;",border:1,minWidth:140}]}],buttons:[{text:"Close",onclick:function(){f.close()}}]})}var j=b.isArray;return a.addCommand("mceShowCharmap",i),a.addButton("charmap",{icon:"charmap",tooltip:"Special character",cmd:"mceShowCharmap"}),a.addMenuItem("charmap",{icon:"charmap",text:"Special character",cmd:"mceShowCharmap",context:"insert"}),{getCharMap:g,insertChar:h}}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),i=function(e,t){return e.fire("insertCustomChar",{chr:t})},l=function(e,t){var a=i(e,t).chr;e.execCommand("mceInsertContent",!1,a)},a=tinymce.util.Tools.resolve("tinymce.util.Tools"),r=function(e){return e.settings.charmap},n=function(e){return e.settings.charmap_append},o=a.isArray,c=function(e){return o(e)?[].concat((t=e,a.grep(t,function(e){return o(e)&&2===e.length}))):"function"==typeof e?e():[];var t},s=function(e){return function(e,t){var a=r(e);a&&(t=c(a));var i=n(e);return i?[].concat(t).concat(c(i)):t}(e,[["160","no-break space"],["173","soft hyphen"],["34","quotation mark"],["162","cent sign"],["8364","euro sign"],["163","pound sign"],["165","yen sign"],["169","copyright sign"],["174","registered sign"],["8482","trade mark sign"],["8240","per mille sign"],["181","micro sign"],["183","middle dot"],["8226","bullet"],["8230","three dot leader"],["8242","minutes / feet"],["8243","seconds / inches"],["167","section sign"],["182","paragraph sign"],["223","sharp s / ess-zed"],["8249","single left-pointing angle quotation mark"],["8250","single right-pointing angle quotation mark"],["171","left pointing guillemet"],["187","right pointing guillemet"],["8216","left single quotation mark"],["8217","right single quotation mark"],["8220","left double quotation mark"],["8221","right double quotation mark"],["8218","single low-9 quotation mark"],["8222","double low-9 quotation mark"],["60","less-than sign"],["62","greater-than sign"],["8804","less-than or equal to"],["8805","greater-than or equal to"],["8211","en dash"],["8212","em dash"],["175","macron"],["8254","overline"],["164","currency sign"],["166","broken bar"],["168","diaeresis"],["161","inverted exclamation mark"],["191","turned question mark"],["710","circumflex accent"],["732","small tilde"],["176","degree sign"],["8722","minus sign"],["177","plus-minus sign"],["247","division sign"],["8260","fraction slash"],["215","multiplication sign"],["185","superscript one"],["178","superscript two"],["179","superscript three"],["188","fraction one quarter"],["189","fraction one half"],["190","fraction three quarters"],["402","function / florin"],["8747","integral"],["8721","n-ary sumation"],["8734","infinity"],["8730","square root"],["8764","similar to"],["8773","approximately equal to"],["8776","almost equal to"],["8800","not equal to"],["8801","identical to"],["8712","element of"],["8713","not an element of"],["8715","contains as member"],["8719","n-ary product"],["8743","logical and"],["8744","logical or"],["172","not sign"],["8745","intersection"],["8746","union"],["8706","partial differential"],["8704","for all"],["8707","there exists"],["8709","diameter"],["8711","backward difference"],["8727","asterisk operator"],["8733","proportional to"],["8736","angle"],["180","acute accent"],["184","cedilla"],["170","feminine ordinal indicator"],["186","masculine ordinal indicator"],["8224","dagger"],["8225","double dagger"],["192","A - grave"],["193","A - acute"],["194","A - circumflex"],["195","A - tilde"],["196","A - diaeresis"],["197","A - ring above"],["256","A - macron"],["198","ligature AE"],["199","C - cedilla"],["200","E - grave"],["201","E - acute"],["202","E - circumflex"],["203","E - diaeresis"],["274","E - macron"],["204","I - grave"],["205","I - acute"],["206","I - circumflex"],["207","I - diaeresis"],["298","I - macron"],["208","ETH"],["209","N - tilde"],["210","O - grave"],["211","O - acute"],["212","O - circumflex"],["213","O - tilde"],["214","O - diaeresis"],["216","O - slash"],["332","O - macron"],["338","ligature OE"],["352","S - caron"],["217","U - grave"],["218","U - acute"],["219","U - circumflex"],["220","U - diaeresis"],["362","U - macron"],["221","Y - acute"],["376","Y - diaeresis"],["562","Y - macron"],["222","THORN"],["224","a - grave"],["225","a - acute"],["226","a - circumflex"],["227","a - tilde"],["228","a - diaeresis"],["229","a - ring above"],["257","a - macron"],["230","ligature ae"],["231","c - cedilla"],["232","e - grave"],["233","e - acute"],["234","e - circumflex"],["235","e - diaeresis"],["275","e - macron"],["236","i - grave"],["237","i - acute"],["238","i - circumflex"],["239","i - diaeresis"],["299","i - macron"],["240","eth"],["241","n - tilde"],["242","o - grave"],["243","o - acute"],["244","o - circumflex"],["245","o - tilde"],["246","o - diaeresis"],["248","o slash"],["333","o macron"],["339","ligature oe"],["353","s - caron"],["249","u - grave"],["250","u - acute"],["251","u - circumflex"],["252","u - diaeresis"],["363","u - macron"],["253","y - acute"],["254","thorn"],["255","y - diaeresis"],["563","y - macron"],["913","Alpha"],["914","Beta"],["915","Gamma"],["916","Delta"],["917","Epsilon"],["918","Zeta"],["919","Eta"],["920","Theta"],["921","Iota"],["922","Kappa"],["923","Lambda"],["924","Mu"],["925","Nu"],["926","Xi"],["927","Omicron"],["928","Pi"],["929","Rho"],["931","Sigma"],["932","Tau"],["933","Upsilon"],["934","Phi"],["935","Chi"],["936","Psi"],["937","Omega"],["945","alpha"],["946","beta"],["947","gamma"],["948","delta"],["949","epsilon"],["950","zeta"],["951","eta"],["952","theta"],["953","iota"],["954","kappa"],["955","lambda"],["956","mu"],["957","nu"],["958","xi"],["959","omicron"],["960","pi"],["961","rho"],["962","final sigma"],["963","sigma"],["964","tau"],["965","upsilon"],["966","phi"],["967","chi"],["968","psi"],["969","omega"],["8501","alef symbol"],["982","pi symbol"],["8476","real part symbol"],["978","upsilon - hook symbol"],["8472","Weierstrass p"],["8465","imaginary part"],["8592","leftwards arrow"],["8593","upwards arrow"],["8594","rightwards arrow"],["8595","downwards arrow"],["8596","left right arrow"],["8629","carriage return"],["8656","leftwards double arrow"],["8657","upwards double arrow"],["8658","rightwards double arrow"],["8659","downwards double arrow"],["8660","left right double arrow"],["8756","therefore"],["8834","subset of"],["8835","superset of"],["8836","not a subset of"],["8838","subset of or equal to"],["8839","superset of or equal to"],["8853","circled plus"],["8855","circled times"],["8869","perpendicular"],["8901","dot operator"],["8968","left ceiling"],["8969","right ceiling"],["8970","left floor"],["8971","right floor"],["9001","left-pointing angle bracket"],["9002","right-pointing angle bracket"],["9674","lozenge"],["9824","black spade suit"],["9827","black club suit"],["9829","black heart suit"],["9830","black diamond suit"],["8194","en space"],["8195","em space"],["8201","thin space"],["8204","zero width non-joiner"],["8205","zero width joiner"],["8206","left-to-right mark"],["8207","right-to-left mark"]])},t=function(t){return{getCharMap:function(){return s(t)},insertChar:function(e){l(t,e)}}},u=function(e){var t,a,i,r=Math.min(e.length,25),n=Math.ceil(e.length/r);for(t='<table role="presentation" cellspacing="0" class="mce-charmap"><tbody>',i=0;i<n;i++){for(t+="<tr>",a=0;a<r;a++){var o=i*r+a;if(o<e.length){var l=e[o],c=parseInt(l[0],10),s=l?String.fromCharCode(c):" ";t+='<td title="'+l[1]+'"><div tabindex="-1" title="'+l[1]+'" role="button" data-chr="'+c+'">'+s+"</div></td>"}else t+="<td />"}t+="</tr>"}return t+="</tbody></table>"},d=function(e){for(;e;){if("TD"===e.nodeName)return e;e=e.parentNode}},m=function(n){var o,e={type:"container",html:u(s(n)),onclick:function(e){var t=e.target;if(/^(TD|DIV)$/.test(t.nodeName)){var a=d(t).firstChild;if(a&&a.hasAttribute("data-chr")){var i=a.getAttribute("data-chr"),r=parseInt(i,10);isNaN(r)||l(n,String.fromCharCode(r)),e.ctrlKey||o.close()}}},onmouseover:function(e){var t=d(e.target);t&&t.firstChild?(o.find("#preview").text(t.firstChild.firstChild.data),o.find("#previewTitle").text(t.title)):(o.find("#preview").text(" "),o.find("#previewTitle").text(" "))}};o=n.windowManager.open({title:"Special character",spacing:10,padding:10,items:[e,{type:"container",layout:"flex",direction:"column",align:"center",spacing:5,minWidth:160,minHeight:160,items:[{type:"label",name:"preview",text:" ",style:"font-size: 40px; text-align: center",border:1,minWidth:140,minHeight:80},{type:"spacer",minHeight:20},{type:"label",name:"previewTitle",text:" ",style:"white-space: pre-wrap;",border:1,minWidth:140}]}],buttons:[{text:"Close",onclick:function(){o.close()}}]})},g=function(e){e.addCommand("mceShowCharmap",function(){m(e)})},p=function(e){e.addButton("charmap",{icon:"charmap",tooltip:"Special character",cmd:"mceShowCharmap"}),e.addMenuItem("charmap",{icon:"charmap",text:"Special character",cmd:"mceShowCharmap",context:"insert"})};e.add("charmap",function(e){return g(e),p(e),t(e)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/code/plugin.min.js b/lib/web/tiny_mce_4/plugins/code/plugin.min.js index 7a0437ed96a9..7afcca644ef7 100755 --- a/lib/web/tiny_mce_4/plugins/code/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/code/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.dom.DOMUtils")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){return b.add("code",function(b){function c(){var c=b.windowManager.open({title:"Source code",body:{type:"textbox",name:"code",multiline:!0,minWidth:b.getParam("code_dialog_width",600),minHeight:b.getParam("code_dialog_height",Math.min(a.DOM.getViewPort().h-200,500)),spellcheck:!1,style:"direction: ltr; text-align: left"},onSubmit:function(a){b.focus(),b.undoManager.transact(function(){b.setContent(a.data.code)}),b.selection.setCursorLocation(),b.nodeChanged()}});c.find("#code").value(b.getContent({source_view:!0}))}b.addCommand("mceCodeEditor",c),b.addButton("code",{icon:"code",tooltip:"Source code",onclick:c}),b.addMenuItem("code",{icon:"code",text:"Source code",context:"tools",onclick:c})}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),o=function(t){return t.getParam("code_dialog_width",600)},i=function(t){return t.getParam("code_dialog_height",Math.min(n.DOM.getViewPort().h-200,500))},c=function(t,n){t.focus(),t.undoManager.transact(function(){t.setContent(n)}),t.selection.setCursorLocation(),t.nodeChanged()},d=function(t){return t.getContent({source_view:!0})},e=function(n){var t=o(n),e=i(n);n.windowManager.open({title:"Source code",body:{type:"textbox",name:"code",multiline:!0,minWidth:t,minHeight:e,spellcheck:!1,style:"direction: ltr; text-align: left"},onSubmit:function(t){c(n,t.data.code)}}).find("#code").value(d(n))},u=function(t){t.addCommand("mceCodeEditor",function(){e(t)})},a=function(t){t.addButton("code",{icon:"code",tooltip:"Source code",onclick:function(){e(t)}}),t.addMenuItem("code",{icon:"code",text:"Source code",onclick:function(){e(t)}})};t.add("code",function(t){return u(t),a(t),{}})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/codesample/plugin.min.js b/lib/web/tiny_mce_4/plugins/codesample/plugin.min.js index a0285edc6480..c50f3b25cb9c 100755 --- a/lib/web/tiny_mce_4/plugins/codesample/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/codesample/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("6",tinymce.util.Tools.resolve),g("1",["6"],function(a){return a("tinymce.Env")}),g("2",["6"],function(a){return a("tinymce.PluginManager")}),g("3",[],function(){var a={},b="undefined"!=typeof a?a:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},c=function(){var a=/\blang(?:uage)?-(?!\*)(\w+)\b/i,c=b.Prism={util:{encode:function(a){return a instanceof d?new d(a.type,c.util.encode(a.content),a.alias):"Array"===c.util.type(a)?a.map(c.util.encode):a.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(a){return Object.prototype.toString.call(a).match(/\[object (\w+)\]/)[1]},clone:function(a){var b=c.util.type(a);switch(b){case"Object":var d={};for(var e in a)a.hasOwnProperty(e)&&(d[e]=c.util.clone(a[e]));return d;case"Array":return a.map&&a.map(function(a){return c.util.clone(a)})}return a}},languages:{extend:function(a,b){var d=c.util.clone(c.languages[a]);for(var e in b)d[e]=b[e];return d},insertBefore:function(a,b,d,e){e=e||c.languages;var f=e[a];if(2==arguments.length){d=arguments[1];for(var g in d)d.hasOwnProperty(g)&&(f[g]=d[g]);return f}var h={};for(var i in f)if(f.hasOwnProperty(i)){if(i==b)for(var g in d)d.hasOwnProperty(g)&&(h[g]=d[g]);h[i]=f[i]}return c.languages.DFS(c.languages,function(b,c){c===e[a]&&b!=a&&(this[b]=h)}),e[a]=h},DFS:function(a,b,d){for(var e in a)a.hasOwnProperty(e)&&(b.call(a,e,a[e],d||e),"Object"===c.util.type(a[e])?c.languages.DFS(a[e],b):"Array"===c.util.type(a[e])&&c.languages.DFS(a[e],b,e))}},plugins:{},highlightAll:function(a,b){for(var d,e=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'),f=0;d=e[f++];)c.highlightElement(d,a===!0,b)},highlightElement:function(d,e,f){for(var g,h,i=d;i&&!a.test(i.className);)i=i.parentNode;i&&(g=(i.className.match(a)||[,""])[1],h=c.languages[g]),d.className=d.className.replace(a,"").replace(/\s+/g," ")+" language-"+g,i=d.parentNode,/pre/i.test(i.nodeName)&&(i.className=i.className.replace(a,"").replace(/\s+/g," ")+" language-"+g);var j=d.textContent,k={element:d,language:g,grammar:h,code:j};if(!j||!h)return void c.hooks.run("complete",k);if(c.hooks.run("before-highlight",k),e&&b.Worker){var l=new Worker(c.filename);l.onmessage=function(a){k.highlightedCode=a.data,c.hooks.run("before-insert",k),k.element.innerHTML=k.highlightedCode,f&&f.call(k.element),c.hooks.run("after-highlight",k),c.hooks.run("complete",k)},l.postMessage(JSON.stringify({language:k.language,code:k.code,immediateClose:!0}))}else k.highlightedCode=c.highlight(k.code,k.grammar,k.language),c.hooks.run("before-insert",k),k.element.innerHTML=k.highlightedCode,f&&f.call(d),c.hooks.run("after-highlight",k),c.hooks.run("complete",k)},highlight:function(a,b,e){var f=c.tokenize(a,b);return d.stringify(c.util.encode(f),e)},tokenize:function(a,b,d){var e=c.Token,f=[a],g=b.rest;if(g){for(var h in g)b[h]=g[h];delete b.rest}a:for(var h in b)if(b.hasOwnProperty(h)&&b[h]){var i=b[h];i="Array"===c.util.type(i)?i:[i];for(var j=0;j<i.length;++j){var k=i[j],l=k.inside,m=!!k.lookbehind,n=0,o=k.alias;k=k.pattern||k;for(var p=0;p<f.length;p++){var q=f[p];if(f.length>a.length)break a;if(!(q instanceof e)){k.lastIndex=0;var r=k.exec(q);if(r){m&&(n=r[1].length);var s=r.index-1+n,r=r[0].slice(n),t=r.length,u=s+t,v=q.slice(0,s+1),w=q.slice(u+1),x=[p,1];v&&x.push(v);var y=new e(h,l?c.tokenize(r,l):r,o);x.push(y),w&&x.push(w),Array.prototype.splice.apply(f,x)}}}}}return f},hooks:{all:{},add:function(a,b){var d=c.hooks.all;d[a]=d[a]||[],d[a].push(b)},run:function(a,b){var d=c.hooks.all[a];if(d&&d.length)for(var e,f=0;e=d[f++];)e(b)}}},d=c.Token=function(a,b,c){this.type=a,this.content=b,this.alias=c};if(d.stringify=function(a,b,e){if("string"==typeof a)return a;if("Array"===c.util.type(a))return a.map(function(c){return d.stringify(c,b,a)}).join("");var f={type:a.type,content:d.stringify(a.content,b,e),tag:"span",classes:["token",a.type],attributes:{},language:b,parent:e};if("comment"==f.type&&(f.attributes.spellcheck="true"),a.alias){var g="Array"===c.util.type(a.alias)?a.alias:[a.alias];Array.prototype.push.apply(f.classes,g)}c.hooks.run("wrap",f);var h="";for(var i in f.attributes)h+=(h?" ":"")+i+'="'+(f.attributes[i]||"")+'"';return"<"+f.tag+' class="'+f.classes.join(" ")+'" '+h+">"+f.content+"</"+f.tag+">"},!b.document)return b.addEventListener?(b.addEventListener("message",function(a){var d=JSON.parse(a.data),e=d.language,f=d.code,g=d.immediateClose;b.postMessage(c.highlight(f,c.languages[e],e)),g&&b.close()},!1),b.Prism):b.Prism}();return"undefined"!=typeof module&&module.exports&&(module.exports=c),"undefined"!=typeof global&&(global.Prism=c),c.languages.markup={comment:/<!--[\w\W]*?-->/,prolog:/<\?[\w\W]+?\?>/,doctype:/<!DOCTYPE[\w\W]+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[^\s>\/=.]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),c.languages.xml=c.languages.markup,c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,c.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},c.languages.css.atrule.inside.rest=c.util.clone(c.languages.css),c.languages.markup&&(c.languages.insertBefore("markup","tag",{style:{pattern:/<style[\w\W]*?>[\w\W]*?<\/style>/i,inside:{tag:{pattern:/<style[\w\W]*?>|<\/style>/i,inside:c.languages.markup.tag.inside},rest:c.languages.css},alias:"language-css"}}),c.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:c.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:c.languages.css}},alias:"language-css"}},c.languages.markup.tag)),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),c.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),c.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.insertBefore("markup","tag",{script:{pattern:/<script[\w\W]*?>[\w\W]*?<\/script>/i,inside:{tag:{pattern:/<script[\w\W]*?>|<\/script>/i,inside:c.languages.markup.tag.inside},rest:c.languages.javascript},alias:"language-javascript"}}),c.languages.js=c.languages.javascript,c.languages.c=c.languages.extend("clike",{keyword:/\b(asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,operator:/\-[>-]?|\+\+?|!=?|<<?=?|>>?=?|==?|&&?|\|?\||[~^%?*\/]/,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)[ful]*\b/i}),c.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+([^\r\n\\]|\\.|\\(?:\r\n?|\n))*/im,lookbehind:!0,alias:"property",inside:{string:{pattern:/(#\s*include\s*)(<.+?>|("|')(\\?.)+?\3)/,lookbehind:!0}}}}),delete c.languages.c["class-name"],delete c.languages.c["boolean"],c.languages.csharp=c.languages.extend("clike",{keyword:/\b(abstract|as|async|await|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while|add|alias|ascending|async|await|descending|dynamic|from|get|global|group|into|join|let|orderby|partial|remove|select|set|value|var|where|yield)\b/,string:[/@("|')(\1\1|\\\1|\\?(?!\1)[\s\S])*\1/,/("|')(\\?.)*?\1/],number:/\b-?(0x[\da-f]+|\d*\.?\d+)\b/i}),c.languages.insertBefore("csharp","keyword",{preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0}}),c.languages.cpp=c.languages.extend("c",{keyword:/\b(alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|class|compl|const|constexpr|const_cast|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|float|for|friend|goto|if|inline|int|long|mutable|namespace|new|noexcept|nullptr|operator|private|protected|public|register|reinterpret_cast|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,"boolean":/\b(true|false)\b/,operator:/[-+]{1,2}|!=?|<{1,2}=?|>{1,2}=?|\->|:{1,2}|={1,2}|\^|~|%|&{1,2}|\|?\||\?|\*|\/|\b(and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/}),c.languages.insertBefore("cpp","keyword",{"class-name":{pattern:/(class\s+)[a-z0-9_]+/i,lookbehind:!0}}),c.languages.java=c.languages.extend("clike",{keyword:/\b(abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<<?=?|>>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),c.languages.php=c.languages.extend("clike",{keyword:/\b(and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\w\W]*?\*\/|\/\/.*)/,lookbehind:!0}}),c.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),c.languages.insertBefore("php","keyword",{delimiter:/\?>|<\?(?:php)?/i,variable:/\$\w+\b/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),c.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),c.languages.markup&&(c.hooks.add("before-highlight",function(a){"php"===a.language&&(a.tokenStack=[],a.backupCode=a.code,a.code=a.code.replace(/(?:<\?php|<\?)[\w\W]*?(?:\?>)/gi,function(b){return a.tokenStack.push(b),"{{{PHP"+a.tokenStack.length+"}}}"}))}),c.hooks.add("before-insert",function(a){"php"===a.language&&(a.code=a.backupCode,delete a.backupCode)}),c.hooks.add("after-highlight",function(a){if("php"===a.language){for(var b,d=0;b=a.tokenStack[d];d++)a.highlightedCode=a.highlightedCode.replace("{{{PHP"+(d+1)+"}}}",c.highlight(b,a.grammar,"php").replace(/\$/g,"$$$$"));a.element.innerHTML=a.highlightedCode}}),c.hooks.add("wrap",function(a){"php"===a.language&&"markup"===a.type&&(a.content=a.content.replace(/(\{\{\{PHP[0-9]+\}\}\})/g,'<span class="token php">$1</span>'))}),c.languages.insertBefore("php","comment",{markup:{pattern:/<[^?]\/?(.*?)>/,inside:c.languages.markup},php:/\{\{\{PHP[0-9]+\}\}\}/})),c.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:/"""[\s\S]+?"""|'''[\s\S]+?'''|("|')(?:\\?.)*?\1/,"function":{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)[a-z0-9_]+/i,lookbehind:!0},keyword:/\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,"boolean":/\b(?:True|False)\b/,number:/\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,punctuation:/[{}[\];(),.:]/},function(a){a.languages.ruby=a.languages.extend("clike",{comment:/#(?!\{[^\r\n]*?\}).*/,keyword:/\b(alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var b={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:a.util.clone(a.languages.ruby)}};a.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,inside:{interpolation:b}},{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}],variable:/[@$]+[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/,symbol:/:[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/}),a.languages.insertBefore("ruby","number",{builtin:/\b(Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|File|Fixnum|Fload|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z][a-zA-Z_0-9]*(?:[?!]|\b)/}),a.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,inside:{interpolation:b}},{pattern:/("|')(#\{[^}]+\}|\\(?:\r?\n|\r)|\\?.)*?\1/,inside:{interpolation:b}}]}(c),c}),g("7",["6"],function(a){return a("tinymce.dom.DOMUtils")}),g("5",[],function(){function a(a){return a&&"PRE"==a.nodeName&&a.className.indexOf("language-")!==-1}function b(a){return function(b,c){return a(c)}}return{isCodeSample:a,trimArg:b}}),g("4",["7","3","5"],function(a,b,c){function d(a){var b=[{text:"HTML/XML",value:"markup"},{text:"JavaScript",value:"javascript"},{text:"CSS",value:"css"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"}],c=a.settings.codesample_languages;return c?c:b}function e(a,c,d){a.undoManager.transact(function(){var e=f(a);d=i.encode(d),e?(a.dom.setAttrib(e,"class","language-"+c),e.innerHTML=d,b.highlightElement(e),a.selection.select(e)):(a.insertContent('<pre id="__new" class="language-'+c+'">'+d+"</pre>"),a.selection.select(a.$("#__new").removeAttr("id")[0]))})}function f(a){var b=a.selection.getNode();return c.isCodeSample(b)?b:null}function g(a){var b=f(a);return b?b.textContent:""}function h(a){var b,c=f(a);return c?(b=c.className.match(/language-(\w+)/),b?b[1]:""):""}var i=a.DOM;return{open:function(a){a.windowManager.open({title:"Insert/Edit code sample",minWidth:Math.min(i.getViewPort().w,a.getParam("codesample_dialog_width",800)),minHeight:Math.min(i.getViewPort().h,a.getParam("codesample_dialog_height",650)),layout:"flex",direction:"column",align:"stretch",body:[{type:"listbox",name:"language",label:"Language",maxWidth:200,value:h(a),values:d(a)},{type:"textbox",name:"code",multiline:!0,spellcheck:!1,ariaLabel:"Code view",flex:1,style:"direction: ltr; text-align: left",classes:"monospace",value:g(a),autofocus:!0}],onSubmit:function(b){e(a,b.data.language,b.data.code)}})}}}),g("0",["1","2","3","4","5"],function(a,b,c,d,e){var f,g=e.trimArg;return b.add("codesample",function(b,h){function i(){var a,c=b.settings.codesample_content_css;b.inline&&f||!b.inline&&j||(b.inline?f=!0:j=!0,c!==!1&&(a=b.dom.create("link",{rel:"stylesheet",href:c?c:h+"/css/prism.css"}),b.getDoc().getElementsByTagName("head")[0].appendChild(a)))}var j,k=b.$;a.ceFalse&&(b.on("PreProcess",function(a){k("pre[contenteditable=false]",a.node).filter(g(e.isCodeSample)).each(function(a,b){var c=k(b),d=b.textContent;c.attr("class",k.trim(c.attr("class"))),c.removeAttr("contentEditable"),c.empty().append(k("<code></code>").each(function(){this.textContent=d}))})}),b.on("SetContent",function(){var a=k("pre").filter(g(e.isCodeSample)).filter(function(a,b){return"false"!==b.contentEditable});a.length&&b.undoManager.transact(function(){a.each(function(a,d){k(d).find("br").each(function(a,c){c.parentNode.replaceChild(b.getDoc().createTextNode("\n"),c)}),d.contentEditable=!1,d.innerHTML=b.dom.encode(d.textContent),c.highlightElement(d),d.className=k.trim(d.className)})})}),b.addCommand("codesample",function(){var a=b.selection.getNode();b.selection.isCollapsed()||e.isCodeSample(a)?d.open(b):b.formatter.toggle("code")}),b.addButton("codesample",{cmd:"codesample",title:"Insert/Edit code sample"}),b.on("init",i))}),function(){}}),d("0")()}(); \ No newline at end of file +!function(u){"use strict";var n=function(e){var t=e,a=function(){return t};return{get:a,set:function(e){t=e},clone:function(){return n(a())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),i=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=function(e){return e.settings.codesample_content_css},a=function(e){return e.settings.codesample_languages},o=function(e){return Math.min(i.DOM.getViewPort().w,e.getParam("codesample_dialog_width",800))},l=function(e){return Math.min(i.DOM.getViewPort().w,e.getParam("codesample_dialog_height",650))},t={},r=t,g=void 0!==t?t:"undefined"!=typeof WorkerGlobalScope&&u.self instanceof WorkerGlobalScope?u.self:{},c=function(){var c=/\blang(?:uage)?-(?!\*)(\w+)\b/i,S=g.Prism={util:{encode:function(e){return e instanceof o?new o(e.type,S.util.encode(e.content),e.alias):"Array"===S.util.type(e)?e.map(S.util.encode):e.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){switch(S.util.type(e)){case"Object":var t={};for(var a in e)e.hasOwnProperty(a)&&(t[a]=S.util.clone(e[a]));return t;case"Array":return e.map&&e.map(function(e){return S.util.clone(e)})}return e}},languages:{extend:function(e,t){var a=S.util.clone(S.languages[e]);for(var n in t)a[n]=t[n];return a},insertBefore:function(a,e,t,n){var i=(n=n||S.languages)[a];if(2===arguments.length){for(var r in t=e)t.hasOwnProperty(r)&&(i[r]=t[r]);return i}var s={};for(var o in i)if(i.hasOwnProperty(o)){if(o===e)for(var r in t)t.hasOwnProperty(r)&&(s[r]=t[r]);s[o]=i[o]}return S.languages.DFS(S.languages,function(e,t){t===n[a]&&e!==a&&(this[e]=s)}),n[a]=s},DFS:function(e,t,a){for(var n in e)e.hasOwnProperty(n)&&(t.call(e,n,e[n],a||n),"Object"===S.util.type(e[n])?S.languages.DFS(e[n],t):"Array"===S.util.type(e[n])&&S.languages.DFS(e[n],t,n))}},plugins:{},highlightAll:function(e,t){for(var a=u.document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'),n=0,i=void 0;i=a[n++];)S.highlightElement(i,!0===e,t)},highlightElement:function(e,t,a){for(var n,i,r=e;r&&!c.test(r.className);)r=r.parentNode;r&&(n=(r.className.match(c)||[,""])[1],i=S.languages[n]),e.className=e.className.replace(c,"").replace(/\s+/g," ")+" language-"+n,r=e.parentNode,/pre/i.test(r.nodeName)&&(r.className=r.className.replace(c,"").replace(/\s+/g," ")+" language-"+n);var s=e.textContent,o={element:e,language:n,grammar:i,code:s};if(s&&i)if(S.hooks.run("before-highlight",o),t&&g.Worker){var l=new u.Worker(S.filename);l.onmessage=function(e){o.highlightedCode=e.data,S.hooks.run("before-insert",o),o.element.innerHTML=o.highlightedCode,a&&a.call(o.element),S.hooks.run("after-highlight",o),S.hooks.run("complete",o)},l.postMessage(JSON.stringify({language:o.language,code:o.code,immediateClose:!0}))}else o.highlightedCode=S.highlight(o.code,o.grammar,o.language),S.hooks.run("before-insert",o),o.element.innerHTML=o.highlightedCode,a&&a.call(e),S.hooks.run("after-highlight",o),S.hooks.run("complete",o);else S.hooks.run("complete",o)},highlight:function(e,t,a){var n=S.tokenize(e,t);return o.stringify(S.util.encode(n),a)},tokenize:function(e,t,a){var n=S.Token,i=[e],r=t.rest;if(r){for(var s in r)t[s]=r[s];delete t.rest}e:for(var s in t)if(t.hasOwnProperty(s)&&t[s]){var o=t[s];o="Array"===S.util.type(o)?o:[o];for(var l=0;l<o.length;++l){var c=o[l],u=c.inside,g=!!c.lookbehind,d=0,p=c.alias;c=c.pattern||c;for(var f=0;f<i.length;f++){var h=i[f];if(i.length>e.length)break e;if(!(h instanceof n)){c.lastIndex=0;var m=c.exec(h);if(m){g&&(d=m[1].length);var b=m.index-1+d,y=b+(m=m[0].slice(d)).length,v=h.slice(0,b+1),k=h.slice(y+1),w=[f,1];v&&w.push(v);var x=new n(s,u?S.tokenize(m,u):m,p);w.push(x),k&&w.push(k),Array.prototype.splice.apply(i,w)}}}}}return i},hooks:{all:{},add:function(e,t){var a=S.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=S.hooks.all[e];if(a&&a.length)for(var n=0,i=void 0;i=a[n++];)i(t)}}},o=S.Token=function(e,t,a){this.type=e,this.content=t,this.alias=a};if(o.stringify=function(t,a,e){if("string"==typeof t)return t;if("Array"===S.util.type(t))return t.map(function(e){return o.stringify(e,a,t)}).join("");var n={type:t.type,content:o.stringify(t.content,a,e),tag:"span",classes:["token",t.type],attributes:{},language:a,parent:e};if("comment"===n.type&&(n.attributes.spellcheck="true"),t.alias){var i="Array"===S.util.type(t.alias)?t.alias:[t.alias];Array.prototype.push.apply(n.classes,i)}S.hooks.run("wrap",n);var r="";for(var s in n.attributes)r+=(r?" ":"")+s+'="'+(n.attributes[s]||"")+'"';return"<"+n.tag+' class="'+n.classes.join(" ")+'" '+r+">"+n.content+"</"+n.tag+">"},!g.document)return g.addEventListener&&g.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,n=t.code,i=t.immediateClose;g.postMessage(S.highlight(n,S.languages[a],a)),i&&g.close()},!1),g.Prism}();void 0!==r&&(r.Prism=c),c.languages.markup={comment:/<!--[\w\W]*?-->/,prolog:/<\?[\w\W]+?\?>/,doctype:/<!DOCTYPE[\w\W]+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[^\s>\/=.]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),c.languages.xml=c.languages.markup,c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,c.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},c.languages.css.atrule.inside.rest=c.util.clone(c.languages.css),c.languages.markup&&(c.languages.insertBefore("markup","tag",{style:{pattern:/<style[\w\W]*?>[\w\W]*?<\/style>/i,inside:{tag:{pattern:/<style[\w\W]*?>|<\/style>/i,inside:c.languages.markup.tag.inside},rest:c.languages.css},alias:"language-css"}}),c.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:c.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:c.languages.css}},alias:"language-css"}},c.languages.markup.tag)),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),c.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),c.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.insertBefore("markup","tag",{script:{pattern:/<script[\w\W]*?>[\w\W]*?<\/script>/i,inside:{tag:{pattern:/<script[\w\W]*?>|<\/script>/i,inside:c.languages.markup.tag.inside},rest:c.languages.javascript},alias:"language-javascript"}}),c.languages.js=c.languages.javascript,c.languages.c=c.languages.extend("clike",{keyword:/\b(asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,operator:/\-[>-]?|\+\+?|!=?|<<?=?|>>?=?|==?|&&?|\|?\||[~^%?*\/]/,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)[ful]*\b/i}),c.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+([^\r\n\\]|\\.|\\(?:\r\n?|\n))*/im,lookbehind:!0,alias:"property",inside:{string:{pattern:/(#\s*include\s*)(<.+?>|("|')(\\?.)+?\3)/,lookbehind:!0}}}}),delete c.languages.c["class-name"],delete c.languages.c["boolean"],c.languages.csharp=c.languages.extend("clike",{keyword:/\b(abstract|as|async|await|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while|add|alias|ascending|async|await|descending|dynamic|from|get|global|group|into|join|let|orderby|partial|remove|select|set|value|var|where|yield)\b/,string:[/@("|')(\1\1|\\\1|\\?(?!\1)[\s\S])*\1/,/("|')(\\?.)*?\1/],number:/\b-?(0x[\da-f]+|\d*\.?\d+)\b/i}),c.languages.insertBefore("csharp","keyword",{preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0}}),c.languages.cpp=c.languages.extend("c",{keyword:/\b(alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|class|compl|const|constexpr|const_cast|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|float|for|friend|goto|if|inline|int|long|mutable|namespace|new|noexcept|nullptr|operator|private|protected|public|register|reinterpret_cast|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,"boolean":/\b(true|false)\b/,operator:/[-+]{1,2}|!=?|<{1,2}=?|>{1,2}=?|\->|:{1,2}|={1,2}|\^|~|%|&{1,2}|\|?\||\?|\*|\/|\b(and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/}),c.languages.insertBefore("cpp","keyword",{"class-name":{pattern:/(class\s+)[a-z0-9_]+/i,lookbehind:!0}}),c.languages.java=c.languages.extend("clike",{keyword:/\b(abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<<?=?|>>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),c.languages.php=c.languages.extend("clike",{keyword:/\b(and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\w\W]*?\*\/|\/\/.*)/,lookbehind:!0}}),c.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),c.languages.insertBefore("php","keyword",{delimiter:/\?>|<\?(?:php)?/i,variable:/\$\w+\b/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),c.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),c.languages.markup&&(c.hooks.add("before-highlight",function(t){"php"===t.language&&(t.tokenStack=[],t.backupCode=t.code,t.code=t.code.replace(/(?:<\?php|<\?)[\w\W]*?(?:\?>)/gi,function(e){return t.tokenStack.push(e),"{{{PHP"+t.tokenStack.length+"}}}"}))}),c.hooks.add("before-insert",function(e){"php"===e.language&&(e.code=e.backupCode,delete e.backupCode)}),c.hooks.add("after-highlight",function(e){if("php"===e.language){for(var t=0,a=void 0;a=e.tokenStack[t];t++)e.highlightedCode=e.highlightedCode.replace("{{{PHP"+(t+1)+"}}}",c.highlight(a,e.grammar,"php").replace(/\$/g,"$$$$"));e.element.innerHTML=e.highlightedCode}}),c.hooks.add("wrap",function(e){"php"===e.language&&"markup"===e.type&&(e.content=e.content.replace(/(\{\{\{PHP[0-9]+\}\}\})/g,'<span class="token php">$1</span>'))}),c.languages.insertBefore("php","comment",{markup:{pattern:/<[^?]\/?(.*?)>/,inside:c.languages.markup},php:/\{\{\{PHP[0-9]+\}\}\}/})),c.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:/"""[\s\S]+?"""|'''[\s\S]+?'''|("|')(?:\\?.)*?\1/,"function":{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)[a-z0-9_]+/i,lookbehind:!0},keyword:/\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,"boolean":/\b(?:True|False)\b/,number:/\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,punctuation:/[{}[\];(),.:]/},function(e){e.languages.ruby=e.languages.extend("clike",{comment:/#(?!\{[^\r\n]*?\}).*/,keyword:/\b(alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var t={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:e.util.clone(e.languages.ruby)}};e.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1[gim]{0,3}/,inside:{interpolation:t}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,inside:{interpolation:t}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,inside:{interpolation:t}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,inside:{interpolation:t}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,inside:{interpolation:t}},{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}],variable:/[@$]+[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/,symbol:/:[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/}),e.languages.insertBefore("ruby","number",{builtin:/\b(Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|File|Fixnum|Fload|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z][a-zA-Z_0-9]*(?:[?!]|\b)/}),e.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1/,inside:{interpolation:t}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,inside:{interpolation:t}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,inside:{interpolation:t}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,inside:{interpolation:t}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,inside:{interpolation:t}},{pattern:/("|')(#\{[^}]+\}|\\(?:\r?\n|\r)|\\?.)*?\1/,inside:{interpolation:t}}]}(c);var d={isCodeSample:function(e){return e&&"PRE"===e.nodeName&&-1!==e.className.indexOf("language-")},trimArg:function(a){return function(e,t){return a(t)}}},p=function(e){var t=e.selection.getNode();return d.isCodeSample(t)?t:null},f=p,h=function(t,a,n){t.undoManager.transact(function(){var e=p(t);n=i.DOM.encode(n),e?(t.dom.setAttrib(e,"class","language-"+a),e.innerHTML=n,c.highlightElement(e),t.selection.select(e)):(t.insertContent('<pre id="__new" class="language-'+a+'">'+n+"</pre>"),t.selection.select(t.$("#__new").removeAttr("id")[0]))})},m=function(e){var t=p(e);return t?t.textContent:""},b=function(e){var t=a(e);return t||[{text:"HTML/XML",value:"markup"},{text:"JavaScript",value:"javascript"},{text:"CSS",value:"css"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"}]},y=function(e){var t,a=f(e);return a&&(t=a.className.match(/language-(\w+)/))?t[1]:""},v=function(t){var e=o(t),a=l(t),n=y(t),i=b(t),r=m(t);t.windowManager.open({title:"Insert/Edit code sample",minWidth:e,minHeight:a,layout:"flex",direction:"column",align:"stretch",body:[{type:"listbox",name:"language",label:"Language",maxWidth:200,value:n,values:i},{type:"textbox",name:"code",multiline:!0,spellcheck:!1,ariaLabel:"Code view",flex:1,style:"direction: ltr; text-align: left",classes:"monospace",value:r,autofocus:!0}],onSubmit:function(e){h(t,e.data.language,e.data.code)}})},k=function(t){t.addCommand("codesample",function(){var e=t.selection.getNode();t.selection.isCollapsed()||d.isCodeSample(e)?v(t):t.formatter.toggle("code")})},w=function(a){var i=a.$;a.on("PreProcess",function(e){i("pre[contenteditable=false]",e.node).filter(d.trimArg(d.isCodeSample)).each(function(e,t){var a=i(t),n=t.textContent;a.attr("class",i.trim(a.attr("class"))),a.removeAttr("contentEditable"),a.empty().append(i("<code></code>").each(function(){this.textContent=n}))})}),a.on("SetContent",function(){var e=i("pre").filter(d.trimArg(d.isCodeSample)).filter(function(e,t){return"false"!==t.contentEditable});e.length&&a.undoManager.transact(function(){e.each(function(e,t){i(t).find("br").each(function(e,t){t.parentNode.replaceChild(a.getDoc().createTextNode("\n"),t)}),t.contentEditable=!1,t.innerHTML=a.dom.encode(t.textContent),c.highlightElement(t),t.className=i.trim(t.className)})})})},x=function(e,t,a,n){var i,r=s(e);e.inline&&a.get()||!e.inline&&n.get()||(e.inline?a.set(!0):n.set(!0),!1!==r&&(i=e.dom.create("link",{rel:"stylesheet",href:r||t+"/css/prism.css"}),e.getDoc().getElementsByTagName("head")[0].appendChild(i)))},S=function(e){e.addButton("codesample",{cmd:"codesample",title:"Insert/Edit code sample"}),e.addMenuItem("codesample",{cmd:"codesample",text:"Code sample",icon:"codesample"})},C=n(!1);e.add("codesample",function(t,e){var a=n(!1);w(t),S(t),k(t),t.on("init",function(){x(t,e,C,a)}),t.on("dblclick",function(e){d.isCodeSample(e.target)&&v(t)})})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/colorpicker/plugin.min.js b/lib/web/tiny_mce_4/plugins/colorpicker/plugin.min.js index 35bfea2141f3..10317a5f6f4b 100755 --- a/lib/web/tiny_mce_4/plugins/colorpicker/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/colorpicker/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Color")}),g("0",["1","2"],function(a,b){return a.add("colorpicker",function(a){function c(c,d){function e(a){var c=new b(a),d=c.toRgb();g.fromJSON({r:d.r,g:d.g,b:d.b,hex:c.toHex().substr(1)}),f(c.toHex())}function f(a){g.find("#preview")[0].getEl().style.background=a}var g=a.windowManager.open({title:"Color",items:{type:"container",layout:"flex",direction:"row",align:"stretch",padding:5,spacing:10,items:[{type:"colorpicker",value:d,onchange:function(){var a=this.rgb();g&&(g.find("#r").value(a.r),g.find("#g").value(a.g),g.find("#b").value(a.b),g.find("#hex").value(this.value().substr(1)),f(this.value()))}},{type:"form",padding:0,labelGap:5,defaults:{type:"textbox",size:7,value:"0",flex:1,spellcheck:!1,onchange:function(){var a,b,c=g.find("colorpicker")[0];return a=this.name(),b=this.value(),"hex"==a?(b="#"+b,e(b),void c.value(b)):(b={r:g.find("#r").value(),g:g.find("#g").value(),b:g.find("#b").value()},c.value(b),void e(b))}},items:[{name:"r",label:"R",autofocus:1},{name:"g",label:"G"},{name:"b",label:"B"},{name:"hex",label:"#",value:"000000"},{name:"preview",type:"container",border:1}]}]},onSubmit:function(){c("#"+this.toJSON().hex)}});e(d)}a.settings.color_picker_callback||(a.settings.color_picker_callback=c)}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=tinymce.util.Tools.resolve("tinymce.util.Color"),a=function(e,n){e.find("#preview")[0].getEl().style.background=n},o=function(e,n){var i=l(n),t=i.toRgb();e.fromJSON({r:t.r,g:t.g,b:t.b,hex:i.toHex().substr(1)}),a(e,i.toHex())},t=function(e,n,i){var t=e.windowManager.open({title:"Color",items:{type:"container",layout:"flex",direction:"row",align:"stretch",padding:5,spacing:10,items:[{type:"colorpicker",value:i,onchange:function(){var e=this.rgb();t&&(t.find("#r").value(e.r),t.find("#g").value(e.g),t.find("#b").value(e.b),t.find("#hex").value(this.value().substr(1)),a(t,this.value()))}},{type:"form",padding:0,labelGap:5,defaults:{type:"textbox",size:7,value:"0",flex:1,spellcheck:!1,onchange:function(){var e,n,i=t.find("colorpicker")[0];if(e=this.name(),n=this.value(),"hex"===e)return o(t,n="#"+n),void i.value(n);n={r:t.find("#r").value(),g:t.find("#g").value(),b:t.find("#b").value()},i.value(n),o(t,n)}},items:[{name:"r",label:"R",autofocus:1},{name:"g",label:"G"},{name:"b",label:"B"},{name:"hex",label:"#",value:"000000"},{name:"preview",type:"container",border:1}]}]},onSubmit:function(){n("#"+t.toJSON().hex)}});o(t,i)};e.add("colorpicker",function(i){i.settings.color_picker_callback||(i.settings.color_picker_callback=function(e,n){t(i,e,n)})})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/contextmenu/plugin.min.js b/lib/web/tiny_mce_4/plugins/contextmenu/plugin.min.js index 5209089240d2..a2e2c7547f1f 100755 --- a/lib/web/tiny_mce_4/plugins/contextmenu/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/contextmenu/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("7",tinymce.util.Tools.resolve),g("1",["7"],function(a){return a("tinymce.dom.DOMUtils")}),g("2",["7"],function(a){return a("tinymce.Env")}),g("3",["7"],function(a){return a("tinymce.PluginManager")}),g("4",["7"],function(a){return a("tinymce.ui.Menu")}),g("5",["7"],function(a){return a("tinymce.util.Tools")}),h("a",Array),h("b",Error),g("d",["a","b"],function(a,b){var c=function(){},d=function(a,b){return function(){return a(b.apply(null,arguments))}},e=function(a){return function(){return a}},f=function(a){return a},g=function(a,b){return a===b},h=function(b){for(var c=new a(arguments.length-1),d=1;d<arguments.length;d++)c[d-1]=arguments[d];return function(){for(var d=new a(arguments.length),e=0;e<d.length;e++)d[e]=arguments[e];var f=c.concat(d);return b.apply(null,f)}},i=function(a){return function(){return!a.apply(null,arguments)}},j=function(a){return function(){throw new b(a)}},k=function(a){return a()},l=function(a){a()},m=e(!1),n=e(!0);return{noop:c,compose:d,constant:e,identity:f,tripleEquals:g,curry:h,not:i,die:j,apply:k,call:l,never:m,always:n}}),h("e",Object),g("9",["d","e"],function(a,b){var c=a.never,d=a.always,e=function(){return f},f=function(){var f=function(a){return a.isNone()},g=function(a){return a()},h=function(a){return a},i=function(){},j={fold:function(a,b){return a()},is:c,isSome:c,isNone:d,getOr:h,getOrThunk:g,getOrDie:function(a){throw new Error(a||"error: getOrDie called on none.")},or:h,orThunk:g,map:e,ap:e,each:i,bind:e,flatten:e,exists:c,forall:d,filter:e,equals:f,equals_:f,toArray:function(){return[]},toString:a.constant("none()")};return b.freeze&&b.freeze(j),j}(),g=function(a){var b=function(){return a},h=function(){return k},i=function(b){return g(b(a))},j=function(b){return b(a)},k={fold:function(b,c){return c(a)},is:function(b){return a===b},isSome:d,isNone:c,getOr:b,getOrThunk:b,getOrDie:b,or:h,orThunk:h,map:i,ap:function(b){return b.fold(e,function(b){return g(b(a))})},each:function(b){b(a)},bind:j,flatten:b,exists:j,forall:j,filter:function(b){return b(a)?k:f},equals:function(b){return b.is(a)},equals_:function(b,d){return b.fold(c,function(b){return d(a,b)})},toArray:function(){return[a]},toString:function(){return"some("+a+")"}};return k},h=function(a){return null===a||void 0===a?f:g(a)};return{some:g,none:e,from:h}}),h("c",String),g("8",["9","a","b","c"],function(a,b,c,d){var e=function(){var a=b.prototype.indexOf,c=function(b,c){return a.call(b,c)},d=function(a,b){return u(a,b)};return void 0===a?d:c}(),f=function(b,c){var d=e(b,c);return d===-1?a.none():a.some(d)},g=function(a,b){return e(a,b)>-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d<a;d++)c.push(b(d));return c},j=function(a,b){for(var c=[],d=0;d<a.length;d+=b){var e=a.slice(d,d+b);c.push(e)}return c},k=function(a,c){for(var d=a.length,e=new b(d),f=0;f<d;f++){var g=a[f];e[f]=c(g,f,a)}return e},l=function(a,b){for(var c=0,d=a.length;c<d;c++){var e=a[c];b(e,c,a)}},m=function(a,b){for(var c=a.length-1;c>=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e<f;e++){var g=a[e],h=b(g,e,a)?c:d;h.push(g)}return{pass:c,fail:d}},o=function(a,b){for(var c=[],d=0,e=a.length;d<e;d++){var f=a[d];b(f,d,a)&&c.push(f)}return c},p=function(a,b){if(0===a.length)return[];for(var c=b(a[0]),d=[],e=[],f=0,g=a.length;f<g;f++){var h=a[f],i=b(h);i!==c&&(d.push(e),e=[]),c=i,e.push(h)}return 0!==e.length&&d.push(e),d},q=function(a,b,c){return m(a,function(a){c=b(c,a)}),c},r=function(a,b,c){return l(a,function(a){c=b(c,a)}),c},s=function(b,c){for(var d=0,e=b.length;d<e;d++){var f=b[d];if(c(f,d,b))return a.some(f)}return a.none()},t=function(b,c){for(var d=0,e=b.length;d<e;d++){var f=b[d];if(c(f,d,b))return a.some(d)}return a.none()},u=function(a,b){for(var c=0,d=a.length;c<d;++c)if(a[c]===b)return c;return-1},v=b.prototype.push,w=function(a){for(var d=[],e=0,f=a.length;e<f;++e){if(!b.prototype.isPrototypeOf(a[e]))throw new c("Arr.flatten item "+e+" was not an array, input: "+a);v.apply(d,a[e])}return d},x=function(a,b){var c=k(a,b);return w(c)},y=function(a,b){for(var c=0,d=a.length;c<d;++c){var e=a[c];if(b(e,c,a)!==!0)return!1}return!0},z=function(a,b){return a.length===b.length&&y(a,function(a,c){return a===b[c]})},A=b.prototype.slice,B=function(a){var b=A.call(a,0);return b.reverse(),b},C=function(a,b){return o(a,function(a){return!g(b,a)})},D=function(a,b){for(var c={},e=0,f=a.length;e<f;e++){var g=a[e];c[d(g)]=b(g,e)}return c},E=function(a){return[a]},F=function(a,b){var c=A.call(a,0);return c.sort(b),c};return{map:k,each:l,eachr:m,partition:n,filter:o,groupBy:p,indexOf:f,foldr:q,foldl:r,find:s,findIndex:t,flatten:w,bind:x,forall:y,exists:h,contains:g,equal:z,reverse:B,chunk:j,difference:C,mapToObject:D,pure:E,sort:F,range:i}}),g("6",["8"],function(a){var b=function(a,b,c){return b>=a.left&&b<=a.right&&c>=a.top&&c<=a.bottom},c=function(c,d,e){return!e.collapsed&&a.foldl(e.getClientRects(),function(a,e){return a||b(e,c,d)},!1)};return{isXYWithinRange:c}}),g("0",["1","2","3","4","5","6"],function(a,b,c,d,e,f){var g=a.DOM;return c.add("contextmenu",function(a){var c,h,i=a.settings.contextmenu_never_use_native,j=function(a){return a.ctrlKey&&!i},k=function(){return b.mac&&b.webkit},l=function(){return h===!0},m=function(a){return a&&"IMG"===a.nodeName},n=function(a,b){return m(a.target)&&f.isXYWithinRange(a.clientX,a.clientY,b)===!1};return a.on("mousedown",function(b){k()&&2===b.button&&!j(b)&&a.selection.isCollapsed()&&a.once("contextmenu",function(b){m(b.target)||a.selection.placeCaretAt(b.clientX,b.clientY)})}),a.on("contextmenu",function(b){var f;if(!j(b)){if(n(b,a.selection.getRng())&&a.selection.select(b.target),b.preventDefault(),f=a.settings.contextmenu||"link openlink image inserttable | cell row column deletetable",c)c.show();else{var i=[];e.each(f.split(/[ ,]/),function(b){var c=a.menuItems[b];"|"==b&&(c={text:b}),c&&(c.shortcut="",i.push(c))});for(var k=0;k<i.length;k++)"|"==i[k].text&&(0!==k&&k!=i.length-1||i.splice(k,1));c=new d({items:i,context:"contextmenu",classes:"contextmenu"}).renderTo(),c.on("hide",function(a){a.control===this&&(h=!1)}),a.on("remove",function(){c.remove(),c=null})}var l={x:b.pageX,y:b.pageY};a.inline||(l=g.getPos(a.getContentAreaContainer()),l.x+=b.clientX,l.y+=b.clientY),c.moveTo(l.x,l.y),h=!0}}),{isContextMenuVisible:l}}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var o=function(t){var n=t,e=function(){return n};return{get:e,set:function(t){n=t},clone:function(){return o(e())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),i=function(t){return{isContextMenuVisible:function(){return t.get()}}},r=function(t){return t.settings.contextmenu_never_use_native},u=function(t){return t.getParam("contextmenu","link openlink image inserttable | cell row column deletetable")},l=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=function(t){return l.DOM.select(t.settings.ui_container)[0]},a=function(t,n){return{x:t,y:n}},f=function(t,n,e){return a(t.x+n,t.y+e)},m=function(t,n){if(t&&"static"!==l.DOM.getStyle(t,"position",!0)){var e=l.DOM.getPos(t),o=e.x-t.scrollLeft,i=e.y-t.scrollTop;return f(n,-o,-i)}return f(n,0,0)},c=function(t,n){if(t.inline)return m(s(t),a((u=n).pageX,u.pageY));var e,o,i,r,u,c=(e=t.getContentAreaContainer(),o=a((r=n).clientX,r.clientY),i=l.DOM.getPos(e),f(o,i.x,i.y));return m(s(t),c)},g=tinymce.util.Tools.resolve("tinymce.ui.Factory"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),y=function(t,n,e,o){null===o.get()?o.set(function(e,n){var t,o,i=[];o=u(e),v.each(o.split(/[ ,]/),function(t){var n=e.menuItems[t];"|"===t&&(n={text:t}),n&&(n.shortcut="",i.push(n))});for(var r=0;r<i.length;r++)"|"===i[r].text&&(0!==r&&r!==i.length-1||i.splice(r,1));return(t=g.create("menu",{items:i,context:"contextmenu",classes:"contextmenu"})).uiContainer=s(e),t.renderTo(s(e)),t.on("hide",function(t){t.control===this&&n.set(!1)}),e.on("remove",function(){t.remove(),t=null}),t}(t,e)):o.get().show(),o.get().moveTo(n.x,n.y),e.set(!0)},x=function(e,o,i){e.on("contextmenu",function(t){var n;n=e,(!t.ctrlKey||r(n))&&(t.preventDefault(),y(e,c(e,t),o,i))})};t.add("contextmenu",function(t){var n=o(null),e=o(!1);return x(t,e,n),i(e)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/directionality/plugin.min.js b/lib/web/tiny_mce_4/plugins/directionality/plugin.min.js index 226c641aa953..bb48bcf94177 100755 --- a/lib/web/tiny_mce_4/plugins/directionality/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/directionality/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("directionality",function(a){function c(c){var d,e=a.dom,f=a.selection.getSelectedBlocks();f.length&&(d=e.getAttrib(f[0],"dir"),b.each(f,function(a){e.getParent(a.parentNode,"*[dir='"+c+"']",e.getRoot())||(d!=c?e.setAttrib(a,"dir",c):e.setAttrib(a,"dir",null))}),a.nodeChanged())}function d(a){var c=[];return b.each("h1 h2 h3 h4 h5 h6 div p".split(" "),function(b){c.push(b+"[dir="+a+"]")}),c.join(",")}a.addCommand("mceDirectionLTR",function(){c("ltr")}),a.addCommand("mceDirectionRTL",function(){c("rtl")}),a.addButton("ltr",{title:"Left to right",cmd:"mceDirectionLTR",stateSelector:d("ltr")}),a.addButton("rtl",{title:"Right to left",cmd:"mceDirectionRTL",stateSelector:d("rtl")})}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),e=function(t,e){var i,n=t.dom,o=t.selection.getSelectedBlocks();o.length&&(i=n.getAttrib(o[0],"dir"),c.each(o,function(t){n.getParent(t.parentNode,'*[dir="'+e+'"]',n.getRoot())||n.setAttrib(t,"dir",i!==e?e:null)}),t.nodeChanged())},i=function(t){t.addCommand("mceDirectionLTR",function(){e(t,"ltr")}),t.addCommand("mceDirectionRTL",function(){e(t,"rtl")})},n=function(e){var i=[];return c.each("h1 h2 h3 h4 h5 h6 div p".split(" "),function(t){i.push(t+"[dir="+e+"]")}),i.join(",")},o=function(t){t.addButton("ltr",{title:"Left to right",cmd:"mceDirectionLTR",stateSelector:n("ltr")}),t.addButton("rtl",{title:"Right to left",cmd:"mceDirectionRTL",stateSelector:n("rtl")})};t.add("directionality",function(t){i(t),o(t)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/emoticons/plugin.min.js b/lib/web/tiny_mce_4/plugins/emoticons/plugin.min.js index e29c829e7781..4e3cd251ab20 100755 --- a/lib/web/tiny_mce_4/plugins/emoticons/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/emoticons/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("emoticons",function(a,c){function d(){var a;return a='<table role="list" class="mce-grid">',b.each(e,function(d){a+="<tr>",b.each(d,function(b){var d=c+"/img/smiley-"+b+".gif";a+='<td><a href="#" data-mce-url="'+d+'" data-mce-alt="'+b+'" tabindex="-1" role="option" aria-label="'+b+'"><img src="'+d+'" style="width: 18px; height: 18px" role="presentation" /></a></td>'}),a+="</tr>"}),a+="</table>"}var e=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]];a.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:d,onclick:function(b){var c=a.dom.getParent(b.target,"a");c&&(a.insertContent('<img src="'+c.getAttribute("data-mce-url")+'" alt="'+c.getAttribute("data-mce-alt")+'" />'),this.hide())}},tooltip:"Emoticons"})}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),e=tinymce.util.Tools.resolve("tinymce.util.Tools"),n=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]],i=function(i){var o;return o='<table role="list" class="mce-grid">',e.each(n,function(t){o+="<tr>",e.each(t,function(t){var e=i+"/img/smiley-"+t+".gif";o+='<td><a href="#" data-mce-url="'+e+'" data-mce-alt="'+t+'" tabindex="-1" role="option" aria-label="'+t+'"><img src="'+e+'" style="width: 18px; height: 18px" role="presentation" /></a></td>'}),o+="</tr>"}),o+="</table>"},o=function(a,t){var e=i(t);a.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:e,onclick:function(t){var e,i,o,n=a.dom.getParent(t.target,"a");n&&(e=a,i=n.getAttribute("data-mce-url"),o=n.getAttribute("data-mce-alt"),e.insertContent(e.dom.createHTML("img",{src:i,alt:o})),this.hide())}},tooltip:"Emoticons"})};t.add("emoticons",function(t,e){o(t,e)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/fullpage/plugin.min.js b/lib/web/tiny_mce_4/plugins/fullpage/plugin.min.js index 6aa103dd6709..de5221a8e0b0 100755 --- a/lib/web/tiny_mce_4/plugins/fullpage/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/fullpage/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("7",tinymce.util.Tools.resolve),g("1",["7"],function(a){return a("tinymce.html.DomParser")}),g("2",["7"],function(a){return a("tinymce.html.Node")}),g("3",["7"],function(a){return a("tinymce.html.Serializer")}),g("4",["7"],function(a){return a("tinymce.PluginManager")}),g("5",["7"],function(a){return a("tinymce.util.Tools")}),g("6",["5"],function(a){var b=function(b,c){return a.each(b,function(a){c=c.replace(a,function(a){return"<!--mce:protected "+escape(a)+"-->"})}),c},c=function(a){return a.replace(/<!--mce:protected ([\s\S]*?)-->/g,function(a,b){return unescape(b)})};return{protectHtml:b,unprotectHtml:c}}),g("0",["1","2","3","4","5","6"],function(a,b,c,d,e,f){return d.add("fullpage",function(d){function g(){var a=h();d.windowManager.open({title:"Document properties",data:a,defaults:{type:"textbox",size:40},body:[{name:"title",label:"Title"},{name:"keywords",label:"Keywords"},{name:"description",label:"Description"},{name:"robots",label:"Robots"},{name:"author",label:"Author"},{name:"docencoding",label:"Encoding"}],onSubmit:function(b){i(e.extend(a,b.data))}})}function h(){function a(a,b){var c=a.attr(b);return c||""}var b,c,f=j(),g={};return g.fontface=d.getParam("fullpage_default_fontface",""),g.fontsize=d.getParam("fullpage_default_fontsize",""),b=f.firstChild,7==b.type&&(g.xml_pi=!0,c=/encoding="([^"]+)"/.exec(b.value),c&&(g.docencoding=c[1])),b=f.getAll("#doctype")[0],b&&(g.doctype="<!DOCTYPE"+b.value+">"),b=f.getAll("title")[0],b&&b.firstChild&&(g.title=b.firstChild.value),p(f.getAll("meta"),function(a){var b,c=a.attr("name"),d=a.attr("http-equiv");c?g[c.toLowerCase()]=a.attr("content"):"Content-Type"==d&&(b=/charset\s*=\s*(.*)\s*/gi.exec(a.attr("content")),b&&(g.docencoding=b[1]))}),b=f.getAll("html")[0],b&&(g.langcode=a(b,"lang")||a(b,"xml:lang")),g.stylesheets=[],e.each(f.getAll("link"),function(a){"stylesheet"==a.attr("rel")&&g.stylesheets.push(a.attr("href"))}),b=f.getAll("body")[0],b&&(g.langdir=a(b,"dir"),g.style=a(b,"style"),g.visited_color=a(b,"vlink"),g.link_color=a(b,"link"),g.active_color=a(b,"alink")),g}function i(a){function f(a,b,c){a.attr(b,c?c:void 0)}function g(a){i.firstChild?i.insert(a,i.firstChild):i.append(a)}var h,i,k,l,m,o=d.dom;h=j(),i=h.getAll("head")[0],i||(l=h.getAll("html")[0],i=new b("head",1),l.firstChild?l.insert(i,l.firstChild,!0):l.append(i)),l=h.firstChild,a.xml_pi?(m='version="1.0"',a.docencoding&&(m+=' encoding="'+a.docencoding+'"'),7!=l.type&&(l=new b("xml",7),h.insert(l,h.firstChild,!0)),l.value=m):l&&7==l.type&&l.remove(),l=h.getAll("#doctype")[0],a.doctype?(l||(l=new b("#doctype",10),a.xml_pi?h.insert(l,h.firstChild):g(l)),l.value=a.doctype.substring(9,a.doctype.length-1)):l&&l.remove(),l=null,p(h.getAll("meta"),function(a){"Content-Type"==a.attr("http-equiv")&&(l=a)}),a.docencoding?(l||(l=new b("meta",1),l.attr("http-equiv","Content-Type"),l.shortEnded=!0,g(l)),l.attr("content","text/html; charset="+a.docencoding)):l&&l.remove(),l=h.getAll("title")[0],a.title?(l?l.empty():(l=new b("title",1),g(l)),l.append(new b("#text",3)).value=a.title):l&&l.remove(),p("keywords,description,author,copyright,robots".split(","),function(c){var d,e,f=h.getAll("meta"),i=a[c];for(d=0;d<f.length;d++)if(e=f[d],e.attr("name")==c)return void(i?e.attr("content",i):e.remove());i&&(l=new b("meta",1),l.attr("name",c),l.attr("content",i),l.shortEnded=!0,g(l))});var q={};e.each(h.getAll("link"),function(a){"stylesheet"==a.attr("rel")&&(q[a.attr("href")]=a)}),e.each(a.stylesheets,function(a){q[a]||(l=new b("link",1),l.attr({rel:"stylesheet",text:"text/css",href:a}),l.shortEnded=!0,g(l)),delete q[a]}),e.each(q,function(a){a.remove()}),l=h.getAll("body")[0],l&&(f(l,"dir",a.langdir),f(l,"style",a.style),f(l,"vlink",a.visited_color),f(l,"link",a.link_color),f(l,"alink",a.active_color),o.setAttribs(d.getBody(),{style:a.style,dir:a.dir,vLink:a.visited_color,link:a.link_color,aLink:a.active_color})),l=h.getAll("html")[0],l&&(f(l,"lang",a.langcode),f(l,"xml:lang",a.langcode)),i.firstChild||i.remove(),k=new c({validate:!1,indent:!0,apply_source_formatting:!0,indent_before:"head,html,body,meta,title,script,link,style",indent_after:"head,html,body,meta,title,script,link,style"}).serialize(h),n=k.substring(0,k.indexOf("</body>"))}function j(){return new a({validate:!1,root_name:"#document"}).parse(n)}function k(a){function b(a){return a.replace(/<\/?[A-Z]+/g,function(a){return a.toLowerCase()})}var c,g,h,i,k,m="",q=d.dom;if(!(a.selection||(h=f.protectHtml(d.settings.protect,a.content),"raw"==a.format&&n||a.source_view&&d.getParam("fullpage_hide_in_source_view")))){0!==h.length||a.source_view||(h=e.trim(n)+"\n"+e.trim(h)+"\n"+e.trim(o)),h=h.replace(/<(\/?)BODY/gi,"<$1body"),c=h.indexOf("<body"),c!=-1?(c=h.indexOf(">",c),n=b(h.substring(0,c+1)),g=h.indexOf("</body",c),g==-1&&(g=h.length),a.content=e.trim(h.substring(c+1,g)),o=b(h.substring(g))):(n=l(),o="\n</body>\n</html>"),i=j(),p(i.getAll("style"),function(a){a.firstChild&&(m+=a.firstChild.value)}),k=i.getAll("body")[0],k&&q.setAttribs(d.getBody(),{style:k.attr("style")||"",dir:k.attr("dir")||"",vLink:k.attr("vlink")||"",link:k.attr("link")||"",aLink:k.attr("alink")||""}),q.remove("fullpage_styles");var r=d.getDoc().getElementsByTagName("head")[0];m&&(q.add(r,"style",{id:"fullpage_styles"},m),k=q.get("fullpage_styles"),k.styleSheet&&(k.styleSheet.cssText=m));var s={};e.each(r.getElementsByTagName("link"),function(a){"stylesheet"==a.rel&&a.getAttribute("data-mce-fullpage")&&(s[a.href]=a)}),e.each(i.getAll("link"),function(a){var b=a.attr("href");return!b||(s[b]||"stylesheet"!=a.attr("rel")||q.add(r,"link",{rel:"stylesheet",text:"text/css",href:b,"data-mce-fullpage":"1"}),void delete s[b])}),e.each(s,function(a){a.parentNode.removeChild(a)})}}function l(){var a,b="",c="";return d.getParam("fullpage_default_xml_pi")&&(b+='<?xml version="1.0" encoding="'+d.getParam("fullpage_default_encoding","ISO-8859-1")+'" ?>\n'),b+=d.getParam("fullpage_default_doctype","<!DOCTYPE html>"),b+="\n<html>\n<head>\n",(a=d.getParam("fullpage_default_title"))&&(b+="<title>"+a+"\n"),(a=d.getParam("fullpage_default_encoding"))&&(b+='\n'),(a=d.getParam("fullpage_default_font_family"))&&(c+="font-family: "+a+";"),(a=d.getParam("fullpage_default_font_size"))&&(c+="font-size: "+a+";"),(a=d.getParam("fullpage_default_text_color"))&&(c+="color: "+a+";"),b+="\n\n"}function m(a){a.selection||a.source_view&&d.getParam("fullpage_hide_in_source_view")||(a.content=f.unprotectHtml(e.trim(n)+"\n"+e.trim(a.content)+"\n"+e.trim(o)))}var n,o,p=e.each;d.addCommand("mceFullPageProperties",g),d.addButton("fullpage",{title:"Document properties",cmd:"mceFullPageProperties"}),d.addMenuItem("fullpage",{text:"Document properties",cmd:"mceFullPageProperties",context:"file"}),d.on("BeforeSetContent",k),d.on("GetContent",m)}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var l=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return l(n())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),g=tinymce.util.Tools.resolve("tinymce.util.Tools"),t=tinymce.util.Tools.resolve("tinymce.html.DomParser"),f=tinymce.util.Tools.resolve("tinymce.html.Node"),m=tinymce.util.Tools.resolve("tinymce.html.Serializer"),h=function(e){return e.getParam("fullpage_hide_in_source_view")},r=function(e){return e.getParam("fullpage_default_xml_pi")},o=function(e){return e.getParam("fullpage_default_encoding")},a=function(e){return e.getParam("fullpage_default_font_family")},c=function(e){return e.getParam("fullpage_default_font_size")},s=function(e){return e.getParam("fullpage_default_text_color")},u=function(e){return e.getParam("fullpage_default_title")},d=function(e){return e.getParam("fullpage_default_doctype","")},p=function(e){return t({validate:!1,root_name:"#document"}).parse(e)},y=p,v=function(e,t){var n,l,i=p(t),r={};function o(e,t){return e.attr(t)||""}return r.fontface=a(e),r.fontsize=c(e),7===(n=i.firstChild).type&&(r.xml_pi=!0,(l=/encoding="([^"]+)"/.exec(n.value))&&(r.docencoding=l[1])),(n=i.getAll("#doctype")[0])&&(r.doctype=""),(n=i.getAll("title")[0])&&n.firstChild&&(r.title=n.firstChild.value),g.each(i.getAll("meta"),function(e){var t,n=e.attr("name"),l=e.attr("http-equiv");n?r[n.toLowerCase()]=e.attr("content"):"Content-Type"===l&&(t=/charset\s*=\s*(.*)\s*/gi.exec(e.attr("content")))&&(r.docencoding=t[1])}),(n=i.getAll("html")[0])&&(r.langcode=o(n,"lang")||o(n,"xml:lang")),r.stylesheets=[],g.each(i.getAll("link"),function(e){"stylesheet"===e.attr("rel")&&r.stylesheets.push(e.attr("href"))}),(n=i.getAll("body")[0])&&(r.langdir=o(n,"dir"),r.style=o(n,"style"),r.visited_color=o(n,"vlink"),r.link_color=o(n,"link"),r.active_color=o(n,"alink")),r},_=function(e,r,t){var o,n,l,a,i,c=e.dom;function s(e,t,n){e.attr(t,n||undefined)}function u(e){n.firstChild?n.insert(e,n.firstChild):n.append(e)}o=p(t),(n=o.getAll("head")[0])||(a=o.getAll("html")[0],n=new f("head",1),a.firstChild?a.insert(n,a.firstChild,!0):a.append(n)),a=o.firstChild,r.xml_pi?(i='version="1.0"',r.docencoding&&(i+=' encoding="'+r.docencoding+'"'),7!==a.type&&(a=new f("xml",7),o.insert(a,o.firstChild,!0)),a.value=i):a&&7===a.type&&a.remove(),a=o.getAll("#doctype")[0],r.doctype?(a||(a=new f("#doctype",10),r.xml_pi?o.insert(a,o.firstChild):u(a)),a.value=r.doctype.substring(9,r.doctype.length-1)):a&&a.remove(),a=null,g.each(o.getAll("meta"),function(e){"Content-Type"===e.attr("http-equiv")&&(a=e)}),r.docencoding?(a||((a=new f("meta",1)).attr("http-equiv","Content-Type"),a.shortEnded=!0,u(a)),a.attr("content","text/html; charset="+r.docencoding)):a&&a.remove(),a=o.getAll("title")[0],r.title?(a?a.empty():u(a=new f("title",1)),a.append(new f("#text",3)).value=r.title):a&&a.remove(),g.each("keywords,description,author,copyright,robots".split(","),function(e){var t,n,l=o.getAll("meta"),i=r[e];for(t=0;t"))},n=function(n,l){var i=v(n,l.get());n.windowManager.open({title:"Document properties",data:i,defaults:{type:"textbox",size:40},body:[{name:"title",label:"Title"},{name:"keywords",label:"Keywords"},{name:"description",label:"Description"},{name:"robots",label:"Robots"},{name:"author",label:"Author"},{name:"docencoding",label:"Encoding"}],onSubmit:function(e){var t=_(n,g.extend(i,e.data),l.get());l.set(t)}})},i=function(e,t){e.addCommand("mceFullPageProperties",function(){n(e,t)})},b=function(e,t){return g.each(e,function(e){t=t.replace(e,function(e){return"\x3c!--mce:protected "+escape(e)+"--\x3e"})}),t},x=function(e){return e.replace(//g,function(e,t){return unescape(t)})},k=g.each,C=function(e){return e.replace(/<\/?[A-Z]+/g,function(e){return e.toLowerCase()})},A=function(e){var t,n="",l="";if(r(e)){var i=o(e);n+='\n'}return n+=d(e),n+="\n\n\n",(t=u(e))&&(n+=""+t+"\n"),(t=o(e))&&(n+='\n'),(t=a(e))&&(l+="font-family: "+t+";"),(t=c(e))&&(l+="font-size: "+t+";"),(t=s(e))&&(l+="color: "+t+";"),n+="\n\n"},w=function(r,o,a){r.on("BeforeSetContent",function(e){!function(e,t,n,l){var i,r,o,a,c,s="",u=e.dom;if(!(l.selection||(o=b(e.settings.protect,l.content),"raw"===l.format&&t.get()||l.source_view&&h(e)))){0!==o.length||l.source_view||(o=g.trim(t.get())+"\n"+g.trim(o)+"\n"+g.trim(n.get())),-1!==(i=(o=o.replace(/<(\/?)BODY/gi,"<$1body")).indexOf("",i),t.set(C(o.substring(0,i+1))),-1===(r=o.indexOf("\n")),a=y(t.get()),k(a.getAll("style"),function(e){e.firstChild&&(s+=e.firstChild.value)}),(c=a.getAll("body")[0])&&u.setAttribs(e.getBody(),{style:c.attr("style")||"",dir:c.attr("dir")||"",vLink:c.attr("vlink")||"",link:c.attr("link")||"",aLink:c.attr("alink")||""}),u.remove("fullpage_styles");var d=e.getDoc().getElementsByTagName("head")[0];s&&(u.add(d,"style",{id:"fullpage_styles"},s),(c=u.get("fullpage_styles")).styleSheet&&(c.styleSheet.cssText=s));var f={};g.each(d.getElementsByTagName("link"),function(e){"stylesheet"===e.rel&&e.getAttribute("data-mce-fullpage")&&(f[e.href]=e)}),g.each(a.getAll("link"),function(e){var t=e.attr("href");if(!t)return!0;f[t]||"stylesheet"!==e.attr("rel")||u.add(d,"link",{rel:"stylesheet",text:"text/css",href:t,"data-mce-fullpage":"1"}),delete f[t]}),g.each(f,function(e){e.parentNode.removeChild(e)})}}(r,o,a,e)}),r.on("GetContent",function(e){var t,n,l,i;t=r,n=o.get(),l=a.get(),(i=e).selection||i.source_view&&h(t)||(i.content=x(g.trim(n)+"\n"+g.trim(i.content)+"\n"+g.trim(l)))})},P=function(e){e.addButton("fullpage",{title:"Document properties",cmd:"mceFullPageProperties"}),e.addMenuItem("fullpage",{text:"Document properties",cmd:"mceFullPageProperties",context:"file"})};e.add("fullpage",function(e){var t=l(""),n=l("");i(e,t),P(e),w(e,t,n)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/fullscreen/plugin.min.js b/lib/web/tiny_mce_4/plugins/fullscreen/plugin.min.js index 3db217657291..259afc9a5b83 100755 --- a/lib/web/tiny_mce_4/plugins/fullscreen/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/fullscreen/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e Ctrl + Shift + P",action:"Focus to contextual toolbar"},{shortcut:b+" + K",action:"Insert link (if link plugin activated)"},{shortcut:b+" + S",action:"Save (if save plugin activated)"},{shortcut:b+" + F",action:"Find (if searchreplace plugin activated)"}];return{shortcuts:d}}),g("5",["8","9"],function(a,b){var c=function(){var c=function(a){return'aria-label="Action: '+a.action+", Shortcut: "+a.shortcut.replace(/Ctrl/g,"Control")+'"'},d=a.map(b.shortcuts,function(a){return'"+a.action+""+a.shortcut+""}).join("");return{title:"Handy Shortcuts",type:"container",style:"overflow-y: auto; overflow-x: hidden; max-height: 250px",items:[{type:"container",html:'
'+d+"
ActionShortcut
"}]}};return{makeTab:c}}),g("a",["e","j"],function(a,b){var c=function(){var a=b.keys,c=function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b};return void 0===a?c:a}(),d=function(a,b){for(var d=c(a),e=0,f=d.length;e${name}',{name:a,url:"https://www.tinymce.com/docs/plugins/"+a})})},h=function(a){var c=b.mapToArray(a.plugins,function(a,b){return"
  • "+g(b)+"
  • "}),d=c.length,e=c.join("");return"

    Plugins installed ("+d+"):

      "+e+"
    "},i=function(a){return{type:"container",html:'
    '+h(a)+"
    ",flex:1}},j=function(){return{type:"container",html:'

    Premium plugins:

    • PowerPaste
    • Spell Checker Pro
    • Accessibility Checker
    • Advanced Code Editor
    • Enhanced Media Embed
    • Link Checker

    Learn more...

    ',flex:1}},k=function(a){return{title:"Plugins",type:"container",style:"overflow-y: auto; overflow-x: hidden;",layout:"flex",padding:10,spacing:10,items:[i(a),j()]}};return{makeTab:k}}),g("7",["4"],function(a){var b=function(a,b){return 0===a.indexOf("@")?"X.X.X":a+"."+b},c=function(){var c=b(a.majorVersion,a.minorVersion),d='TinyMCE '+c+"";return[{type:"label",html:"You are using "+d},{type:"spacer",flex:1},{text:"Close",onclick:function(){this.parent().parent().close()}}]};return{makeRow:c}}),g("2",["4","5","6","7"],function(a,b,c,d){var e=function(a,e){return function(){a.windowManager.open({title:"Help",bodyType:"tabpanel",layout:"flex",body:[b.makeTab(),c.makeTab(a,e)],buttons:d.makeRow(),onPostRender:function(){var a=this.getEl("title");a.innerHTML='TinyMCE Logo'}})}};return{openDialog:e}}),g("0",["1","2"],function(a,b){var c=function(a,c){a.addButton("help",{icon:"help",onclick:b.openDialog(a,c)}),a.addMenuItem("Help",{text:"Help",icon:"help",context:"view",onclick:b.openDialog(a,c)}),a.addCommand("mceHelp",b.openDialog(a,c)),a.shortcuts.add("Alt+0","Open help dialog",b.openDialog(a,c))};return a.add("help",c),function(){}}),d("0")()}(); +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return function(){return e}};function l(r){for(var o=[],e=1;e'+A.translate(e.action)+""+e.shortcut+"";var t}).join("");return{title:"Handy Shortcuts",type:"container",style:"overflow-y: auto; overflow-x: hidden; max-height: 250px",items:[{type:"container",html:'
    "+e+"
    '+A.translate("Action")+""+A.translate("Shortcut")+"
    "}]}},P=Object.keys,_=[{key:"advlist",name:"Advanced List"},{key:"anchor",name:"Anchor"},{key:"autolink",name:"Autolink"},{key:"autoresize",name:"Autoresize"},{key:"autosave",name:"Autosave"},{key:"bbcode",name:"BBCode"},{key:"charmap",name:"Character Map"},{key:"code",name:"Code"},{key:"codesample",name:"Code Sample"},{key:"colorpicker",name:"Color Picker"},{key:"compat3x",name:"3.x Compatibility"},{key:"contextmenu",name:"Context Menu"},{key:"directionality",name:"Directionality"},{key:"emoticons",name:"Emoticons"},{key:"fullpage",name:"Full Page"},{key:"fullscreen",name:"Full Screen"},{key:"help",name:"Help"},{key:"hr",name:"Horizontal Rule"},{key:"image",name:"Image"},{key:"imagetools",name:"Image Tools"},{key:"importcss",name:"Import CSS"},{key:"insertdatetime",name:"Insert Date/Time"},{key:"legacyoutput",name:"Legacy Output"},{key:"link",name:"Link"},{key:"lists",name:"Lists"},{key:"media",name:"Media"},{key:"nonbreaking",name:"Nonbreaking"},{key:"noneditable",name:"Noneditable"},{key:"pagebreak",name:"Page Break"},{key:"paste",name:"Paste"},{key:"preview",name:"Preview"},{key:"print",name:"Print"},{key:"save",name:"Save"},{key:"searchreplace",name:"Search and Replace"},{key:"spellchecker",name:"Spell Checker"},{key:"tabfocus",name:"Tab Focus"},{key:"table",name:"Table"},{key:"template",name:"Template"},{key:"textcolor",name:"Text Color"},{key:"textpattern",name:"Text Pattern"},{key:"toc",name:"Table of Contents"},{key:"visualblocks",name:"Visual Blocks"},{key:"visualchars",name:"Visual Characters"},{key:"wordcount",name:"Word Count"}],H=l(function(e,o){return e.replace(/\$\{([^{}]*)\}/g,function(e,t){var n,r=o[t];return"string"==(n=typeof r)||"number"===n?r.toString():e})},'${name}'),F=function(t,n){return function(e,t){for(var n=0,r=e.length;n"+F(t,e)+""}),i=a.length,c=a.join("");return"

    "+A.translate(["Plugins installed ({0}):",i])+"

      "+c+"
    "},E=function(e){return{title:"Plugins",type:"container",style:"overflow-y: auto; overflow-x: hidden;",layout:"flex",padding:10,spacing:10,items:[(t=e,{type:"container",html:'
    '+M(t)+"
    ",flex:1}),{type:"container",html:'

    '+A.translate("Premium plugins:")+'

    • PowerPaste
    • Spell Checker Pro
    • Accessibility Checker
    • Advanced Code Editor
    • Enhanced Media Embed
    • Link Checker

    '+A.translate("Learn more...")+"

    ",flex:1}]};var t},I=tinymce.util.Tools.resolve("tinymce.EditorManager"),j=function(){var e,t,n='TinyMCE '+(e=I.majorVersion,t=I.minorVersion,0===e.indexOf("@")?"X.X.X":e+"."+t)+"";return[{type:"label",html:A.translate(["You are using {0}",n])},{type:"spacer",flex:1},{text:"Close",onclick:function(){this.parent().parent().close()}}]},L=function(e,t){return function(){e.windowManager.open({title:"Help",bodyType:"tabpanel",layout:"flex",body:[T(),E(e)],buttons:j(),onPostRender:function(){this.getEl("title").innerHTML='TinyMCE Logo'}})}},B=function(e,t){e.addCommand("mceHelp",L(e,t))},N=function(e,t){e.addButton("help",{icon:"help",onclick:L(e,t)}),e.addMenuItem("help",{text:"Help",icon:"help",context:"help",onclick:L(e,t)})};e.add("help",function(e,t){N(e,t),B(e,t),e.shortcuts.add("Alt+0","Open help dialog","mceHelp")})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/hr/plugin.min.js b/lib/web/tiny_mce_4/plugins/hr/plugin.min.js index 6c0dfa44780b..72bc2cabd109 100755 --- a/lib/web/tiny_mce_4/plugins/hr/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/hr/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i")}),a.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),a.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})}),function(){}}),d("0")()}(); \ No newline at end of file +!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(n){n.addCommand("InsertHorizontalRule",function(){n.execCommand("mceInsertContent",!1,"
    ")})},o=function(n){n.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),n.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})};n.add("hr",function(n){t(n),o(n)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/image/plugin.min.js b/lib/web/tiny_mce_4/plugins/image/plugin.min.js index 1a029df23490..d4764ad6251f 100755 --- a/lib/web/tiny_mce_4/plugins/image/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/image/plugin.min.js @@ -1 +1 @@ -!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i0&&/^[0-9]+$/.test(a)&&(a+="px"),a}if(b.settings.image_advtab){var c=o.toJSON(),d=w.parseStyle(c.style);d=l(d),c.vspace&&(d["margin-top"]=d["margin-bottom"]=a(c.vspace)),c.hspace&&(d["margin-left"]=d["margin-right"]=a(c.hspace)),c.border&&(d["border-width"]=a(c.border)),o.find("#style").value(w.serializeStyle(w.parseStyle(w.serializeStyle(d))))}}function n(){if(b.settings.image_advtab){var a=o.toJSON(),c=w.parseStyle(a.style);o.find("#vspace").value(""),o.find("#hspace").value(""),c=l(c),(c["margin-top"]&&c["margin-bottom"]||c["margin-right"]&&c["margin-left"])&&(c["margin-top"]===c["margin-bottom"]?o.find("#vspace").value(i(c["margin-top"])):o.find("#vspace").value(""),c["margin-right"]===c["margin-left"]?o.find("#hspace").value(i(c["margin-right"])):o.find("#hspace").value("")),c["border-width"]&&o.find("#border").value(i(c["border-width"])),o.find("#style").value(w.serializeStyle(w.parseStyle(w.serializeStyle(c))))}}var o,p,q,r,s,t,u,v={},w=b.dom,x=b.settings.image_dimensions!==!1;p=b.selection.getNode(),q=w.getParent(p,"figure.image"),q&&(p=w.select("img",q)[0]),p&&("IMG"!=p.nodeName||p.getAttribute("data-mce-object")||p.getAttribute("data-mce-placeholder"))&&(p=null),p&&(r=w.getAttrib(p,"width"),s=w.getAttrib(p,"height"),v={src:w.getAttrib(p,"src"),alt:w.getAttrib(p,"alt"),title:w.getAttrib(p,"title"),"class":w.getAttrib(p,"class"),width:r,height:s,caption:!!q}),c&&(t={type:"listbox",label:"Image list",values:g(c,function(a){a.value=b.convertURL(a.value||a.url,"src")},[{text:"None",value:""}]),value:v.src&&b.convertURL(v.src,"src"),onselect:function(a){var b=o.find("#alt");(!b.value()||a.lastControl&&b.value()==a.lastControl.text())&&b.value(a.control.text()),o.find("#src").value(a.control.value()).fire("change")},onPostRender:function(){t=this}}),b.settings.image_class_list&&(u={name:"class",type:"listbox",label:"Class",values:g(b.settings.image_class_list,function(a){a.value&&(a.textStyle=function(){return b.formatter.getCssText({inline:"img",classes:[a.value]})})})});var y=[{name:"src",type:"filepicker",filetype:"image",label:"Source",autofocus:!0,onchange:j,onbeforecall:k},t];b.settings.image_description!==!1&&y.push({name:"alt",type:"textbox",label:"Image description"}),b.settings.image_title&&y.push({name:"title",type:"textbox",label:"Image Title"}),x&&y.push({type:"container",label:"Dimensions",layout:"flex",direction:"row",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:3,onchange:e,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:3,onchange:e,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}),y.push(u),b.settings.image_caption&&a.ceFalse&&y.push({name:"caption",type:"checkbox",label:"Caption"}),b.settings.image_advtab?(p&&(p.style.marginLeft&&p.style.marginRight&&p.style.marginLeft===p.style.marginRight&&(v.hspace=i(p.style.marginLeft)),p.style.marginTop&&p.style.marginBottom&&p.style.marginTop===p.style.marginBottom&&(v.vspace=i(p.style.marginTop)),p.style.borderWidth&&(v.border=i(p.style.borderWidth)),v.style=b.dom.serializeStyle(b.dom.parseStyle(b.dom.getAttrib(p,"style")))),o=b.windowManager.open({title:"Insert/edit image",data:v,bodyType:"tabpanel",body:[{title:"General",type:"form",items:y},{title:"Advanced",type:"form",pack:"start",items:[{label:"Style",name:"style",type:"textbox",onchange:n},{type:"form",layout:"grid",packV:"start",columns:2,padding:0,alignH:["left","right"],defaults:{type:"textbox",maxWidth:50,onchange:m},items:[{label:"Vertical space",name:"vspace"},{label:"Horizontal space",name:"hspace"},{label:"Border",name:"border"}]}]}],onSubmit:h})):o=b.windowManager.open({title:"Insert/edit image",data:v,body:y,onSubmit:h})}b.on("preInit",function(){function a(a){var b=a.attr("class");return b&&/\bimage\b/.test(b)}function c(b){return function(c){function e(a){a.attr("contenteditable",b?"true":null)}for(var f,g=c.length;g--;)f=c[g],a(f)&&(f.attr("contenteditable",b?"false":null),d.each(f.getAll("figcaption"),e))}}b.parser.addNodeFilter("figure",c(!0)),b.serializer.addNodeFilter("figure",c(!1))}),b.addButton("image",{icon:"image",tooltip:"Insert/edit image",onclick:h(i),stateSelector:"img:not([data-mce-object],[data-mce-placeholder]),figure.image"}),b.addMenuItem("image",{icon:"image",text:"Image",onclick:h(i),context:"insert",prependToContext:!0}),b.addCommand("mceImage",h(i))}),function(){}}),d("0")()}(); \ No newline at end of file +!function(l){"use strict";var i,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=function(e){return!1!==e.settings.image_dimensions},u=function(e){return!0===e.settings.image_advtab},m=function(e){return e.getParam("image_prepend_url","")},n=function(e){return e.getParam("image_class_list")},r=function(e){return!1!==e.settings.image_description},a=function(e){return!0===e.settings.image_title},o=function(e){return!0===e.settings.image_caption},c=function(e){return e.getParam("image_list",!1)},s=function(e){return e.getParam("images_upload_url",!1)},g=function(e){return e.getParam("images_upload_handler",!1)},f=function(e){return e.getParam("images_upload_url")},p=function(e){return e.getParam("images_upload_handler")},h=function(e){return e.getParam("images_upload_base_path")},v=function(e){return e.getParam("images_upload_credentials")},b="undefined"!=typeof l.window?l.window:Function("return this;")(),y=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:b,r=0;rc?a=c:a0?3*d:d),f=.3086,g=.6094,h=.082,c(b,[f*(1-e)+e,g*(1-e),h*(1-e),0,0,f*(1-e),g*(1-e)+e,h*(1-e),0,0,f*(1-e),g*(1-e),h*(1-e)+e,0,0,0,0,0,1,0,0,0,0,0,1])}function g(b,d){var e,f,g,h,i;return d=a(d,-180,180)/180*Math.PI,e=Math.cos(d),f=Math.sin(d),g=.213,h=.715,i=.072,c(b,[g+e*(1-g)+f*-g,h+e*-h+f*-h,i+e*-i+f*(1-i),0,0,g+e*-g+.143*f,h+e*(1-h)+.14*f,i+e*-i+f*-.283,0,0,g+e*-g+f*-(1-g),h+e*-h+f*h,i+e*(1-i)+f*i,0,0,0,0,0,1,0,0,0,0,0,1])}function h(b,d){return d=a(255*d,-255,255),c(b,[1,0,0,0,d,0,1,0,0,d,0,0,1,0,d,0,0,0,1,0,0,0,0,0,1])}function i(b,d,e,f){return d=a(d,0,2),e=a(e,0,2),f=a(f,0,2),c(b,[d,0,0,0,0,0,e,0,0,0,0,0,f,0,0,0,0,0,1,0,0,0,0,0,1])}function j(b,e){return e=a(e,0,1),c(b,d([.393,.769,.189,0,0,.349,.686,.168,0,0,.272,.534,.131,0,0,0,0,0,1,0,0,0,0,0,1],e))}function k(b,e){return e=a(e,0,1),c(b,d([.33,.34,.33,0,0,.33,.34,.33,0,0,.33,.34,.33,0,0,0,0,0,1,0,0,0,0,0,1],e))}var l=[0,.01,.02,.04,.05,.06,.07,.08,.1,.11,.12,.14,.15,.16,.17,.18,.2,.21,.22,.24,.25,.27,.28,.3,.32,.34,.36,.38,.4,.42,.44,.46,.48,.5,.53,.56,.59,.62,.65,.68,.71,.74,.77,.8,.83,.86,.89,.92,.95,.98,1,1.06,1.12,1.18,1.24,1.3,1.36,1.42,1.48,1.54,1.6,1.66,1.72,1.78,1.84,1.9,1.96,2,2.12,2.25,2.37,2.5,2.62,2.75,2.87,3,3.2,3.4,3.6,3.8,4,4.3,4.7,4.9,5,5.5,6,6.5,6.8,7,7.3,7.5,7.8,8,8.4,8.7,9,9.4,9.6,9.8,10];return{identity:b,adjust:d,multiply:c,adjustContrast:e,adjustBrightness:h,adjustSaturation:f,adjustHue:g,adjustColors:i,adjustSepia:j,adjustGrayscale:k}}),g("e",["q","d","t"],function(a,b,c){function d(c,d){function e(a,b){var c,d,e,f,g,h=a.data,i=b[0],j=b[1],k=b[2],l=b[3],m=b[4],n=b[5],o=b[6],p=b[7],q=b[8],r=b[9],s=b[10],t=b[11],u=b[12],v=b[13],w=b[14],x=b[15],y=b[16],z=b[17],A=b[18],B=b[19];for(g=0;gc?a=c:a2)&&(i=i<.5?.5:2,k=!0),(j<.5||j>2)&&(j=j<.5?.5:2,k=!0);var l=f(a,i,j);return k?l.then(function(a){return e(a,b,c)}):l}function f(b,e,f){return new a(function(a){var g=d.getWidth(b),h=d.getHeight(b),i=Math.floor(g*e),j=Math.floor(h*f),k=c.create(i,j),l=c.get2dContext(k);l.drawImage(b,0,0,g,h,0,0,i,j),a(k)})}return{scale:e}}),g("f",["q","d","u"],function(a,b,c){function d(c,d){var e=c.toCanvas(),f=a.create(e.width,e.height),g=a.get2dContext(f),h=0,i=0;return d=d<0?360+d:d,90!=d&&270!=d||a.resize(f,f.height,f.width),90!=d&&180!=d||(h=f.width),270!=d&&180!=d||(i=f.height),g.translate(h,i),g.rotate(d*Math.PI/180),g.drawImage(e,0,0),b.fromCanvas(f,c.getType())}function e(c,d){var e=c.toCanvas(),f=a.create(e.width,e.height),g=a.get2dContext(f);return"v"==d?(g.scale(1,-1),g.drawImage(e,0,-f.height)):(g.scale(-1,1),g.drawImage(e,-f.width,0)),b.fromCanvas(f,c.getType())}function f(c,d,e,f,g){var h=c.toCanvas(),i=a.create(f,g),j=a.get2dContext(i);return j.drawImage(h,-d,-e),b.fromCanvas(i,c.getType())}function g(a,d,e){return c.scale(a.toCanvas(),d,e).then(function(c){return b.fromCanvas(c,a.getType())})}return{rotate:d,flip:e,crop:f,resize:g}}),g("2",["e","f"],function(a,b){var c=function(b){return a.invert(b)},d=function(b){return a.sharpen(b)},e=function(b){return a.emboss(b)},f=function(b,c){return a.gamma(b,c)},g=function(b,c){return a.exposure(b,c)},h=function(b,c,d,e){return a.colorize(b,c,d,e)},i=function(b,c){return a.brightness(b,c)},j=function(b,c){return a.hue(b,c)},k=function(b,c){return a.saturate(b,c)},l=function(b,c){return a.contrast(b,c)},m=function(b,c){return a.grayscale(b,c)},n=function(b,c){return a.sepia(b,c)},o=function(a,c){return b.flip(a,c)},p=function(a,c,d,e,f){return b.crop(a,c,d,e,f)},q=function(a,c,d){return b.resize(a,c,d)},r=function(a,c){return b.rotate(a,c)};return{invert:c,sharpen:d,emboss:e,brightness:i,hue:j,saturate:k,contrast:l,grayscale:m,sepia:n,colorize:h,gamma:f,exposure:g,flip:o,crop:p,resize:q,rotate:r}}),h("g",tinymce.util.Tools.resolve),g("3",["g"],function(a){return a("tinymce.Env")}),g("4",["g"],function(a){return a("tinymce.PluginManager")}),g("5",["g"],function(a){return a("tinymce.util.Delay")}),g("6",["g"],function(a){return a("tinymce.util.Promise")}),g("7",["g"],function(a){return a("tinymce.util.Tools")}),g("8",["g"],function(a){return a("tinymce.util.URI")}),g("9",[],function(){function a(a){function b(a){return/^[0-9\.]+px$/.test(a)}var c,d;return c=a.style.width,d=a.style.height,c||d?b(c)&&b(d)?{w:parseInt(c,10),h:parseInt(d,10)}:null:(c=a.width,d=a.height,c&&d?{w:parseInt(c,10),h:parseInt(d,10)}:null)}function b(a,b){var c,d;b&&(c=a.style.width,d=a.style.height,(c||d)&&(a.style.width=b.w+"px",a.style.height=b.h+"px",a.removeAttribute("data-mce-style")),c=a.width,d=a.height,(c||d)&&(a.setAttribute("width",b.w),a.setAttribute("height",b.h)))}function c(a){return{w:a.naturalWidth,h:a.naturalHeight}}return{getImageSize:a,setImageSize:b,getNaturalImageSize:c}}),h("12",Array),h("13",Error),g("w",["12","13"],function(a,b){var c=function(){},d=function(a,b){return function(){return a(b.apply(null,arguments))}},e=function(a){return function(){return a}},f=function(a){return a},g=function(a,b){return a===b},h=function(b){for(var c=new a(arguments.length-1),d=1;d-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e=300?c.handleHttpError(b.status):a.resolve(b.blob)})}var f=function(a,b){var c=a.indexOf("?")===-1?"?":"&";return/[?&]apiKey=/.test(a)||!b?a:a+c+"apiKey="+encodeURIComponent(b)},g=function(b,e){return d.requestUrlAsBlob(f(b,e),{"Content-Type":"application/json;charset=UTF-8","tiny-api-key":e}).then(function(b){return b.status<200||b.status>=300?c.handleServiceErrorResponse(b.status,b.blob):a.resolve(b.blob)})},h=function(a,b){return b?g(a,b):e(a)};return{getUrl:h}}),g("j",["g"],function(a){return a("tinymce.dom.DOMUtils")}),g("k",["g"],function(a){return a("tinymce.ui.Container")}),g("l",["g"],function(a){return a("tinymce.ui.Factory")}),g("m",["g"],function(a){return a("tinymce.ui.Form")}),g("x",["g"],function(a){return a("tinymce.geom.Rect")}),g("y",["g"],function(a){return a("tinymce.ui.Control")}),g("z",["g"],function(a){return a("tinymce.ui.DragHelper")}),g("15",["g"],function(a){return a("tinymce.dom.DomQuery")}),g("16",["g"],function(a){return a("tinymce.util.Observable")}),g("17",["g"],function(a){return a("tinymce.util.VK")}),g("10",["15","z","x","7","16","17"],function(a,b,c,d,e,f){var g=0;return function(h,i,j,k,l){function m(a,b){return{x:b.x+a.x,y:b.y+a.y,w:b.w,h:b.h}}function n(a,b){return{x:b.x-a.x,y:b.y-a.y,w:b.w,h:b.h}}function o(){return n(j,h)}function p(a,b,d,e){var f,g,i,k,l;f=b.x,g=b.y,i=b.w,k=b.h,f+=d*a.deltaX,g+=e*a.deltaY,i+=d*a.deltaW,k+=e*a.deltaH,i<20&&(i=20),k<20&&(k=20),l=h=c.clamp({x:f,y:g,w:i,h:k},j,"move"==a.name),l=n(j,l),y.fire("updateRect",{rect:l}),v(l)}function q(){function c(a){var c;return new b(D,{document:k.ownerDocument,handle:D+"-"+a.name,start:function(){c=h},drag:function(b){p(a,c,b.deltaX,b.deltaY)}})}a('
    ').appendTo(k),d.each(B,function(b){a("#"+D,k).append('