diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php index 54a51a1a55148aee7edeb0d7fb509d7175b408e4..ee730b9be8157e2d794ff954d9e173676cf4b32b 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php @@ -132,7 +132,7 @@ class Category extends \Magento\Framework\Data\Form\Element\Multiselect 'id' => 'add_category_button', 'label' => $newCategoryCaption, 'title' => $newCategoryCaption, - 'onclick' => 'jQuery("#new-category").trigger("openModal")', + 'onclick' => 'jQuery("#new-category").modal("openModal")', 'disabled' => $this->getDisabled(), ] ); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/new-category-dialog.js b/app/code/Magento/Catalog/view/adminhtml/web/js/new-category-dialog.js index 552e6a3a70ee729d5bcc8367b3f50312d5061adf..d70f2e17193afe202495b505a22cf26416dbdae7 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/new-category-dialog.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/new-category-dialog.js @@ -111,7 +111,7 @@ define([ $('#new_category_name, #new_category_parent-suggest').val(''); $suggest.val(''); clearParentCategory(); - widget.element.trigger('closeModal'); + $(widget.element).modal('closeModal'); } else { $('#new_category_messages').html(data.messages); } diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php index a3a2fbda293c06f45808c5087ec56c5d1eeb45de..21c1d2ec4994eac7671fd832e4c5e3e0751313d4 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php @@ -5,56 +5,84 @@ */ namespace Magento\Cms\Test\Unit\Ui\Component\Listing\Column; +use Magento\Cms\Ui\Component\Listing\Column\PageActions; + class PageActionsTest extends \PHPUnit_Framework_TestCase { public function testPrepareItemsByPageId() { + $pageId = 1; // Create Mocks and SUT $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); /** @var \PHPUnit_Framework_MockObject_MockObject $urlBuilderMock */ $urlBuilderMock = $this->getMockBuilder('Magento\Framework\UrlInterface') ->disableOriginalConstructor() ->getMock(); - $inputUrl = 'href/url/for/edit/action'; /** @var \Magento\Cms\Ui\Component\Listing\Column\PageActions $model */ $model = $objectManager->getObject( 'Magento\Cms\Ui\Component\Listing\Column\PageActions', [ 'urlBuilder' => $urlBuilderMock, - 'url' => $inputUrl ] ); // Define test input and expectations - $items = ['data' => ['items' => [['page_id' => 1]]]]; - $fullUrl = 'full-url-including-base.com/href/url/for/edit/action'; - $name = 'item_name'; - - $editArray = [ - 'href' => $fullUrl, - 'label' => __('Edit'), - 'hidden' => true + $items = [ + 'data' => [ + 'items' => [ + [ + 'page_id' => $pageId + ] + ] + ] ]; + $name = 'item_name'; $expectedItems = [ [ - 'page_id' => 1, - $name => ['edit' => $editArray] + 'page_id' => $pageId, + $name => [ + 'edit' => [ + 'href' => 'test/url/edit', + 'label' => __('Edit'), + ], + 'delete' => [ + 'href' => 'test/url/delete', + 'label' => __('Delete'), + 'confirm' => [ + 'title' => __('Delete "${ $.$data.title }"'), + 'message' => __('Are you sure you wan\'t to delete a "${ $.$data.title }" record?') + ], + ] + ], ] ]; // Configure mocks and object data - $urlBuilderMock->expects($this->once()) + $urlBuilderMock->expects($this->any()) ->method('getUrl') - ->with($inputUrl, ['page_id' => 1]) - ->willReturn($fullUrl); + ->willReturnMap( + [ + [ + PageActions::CMS_URL_PATH_EDIT, + [ + 'page_id' => $pageId + ], + 'test/url/edit', + ], + [ + PageActions::CMS_URL_PATH_DELETE, + [ + 'page_id' => $pageId + ], + 'test/url/delete', + ], + ] + ); $model->setName($name); $model->prepareDataSource($items); // Run test - $this->assertEquals( - $expectedItems, - $items['data']['items'] - ); + $this->assertEquals($expectedItems, $items['data']['items']); } } diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php index d93cc635d9e8b716382fd682929cefc4d6e24049..617af42cb6122136cf933ade622dc169846d075b 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php @@ -18,7 +18,9 @@ class BlockActions extends Column /** * Url path */ - const URL_PATH = 'cms/block/edit'; + const URL_PATH_EDIT = 'cms/block/edit'; + const URL_PATH_DELETE = 'cms/block/delete'; + const URL_PATH_DETAILS = 'cms/block/details'; /** * @var UrlInterface @@ -45,6 +47,10 @@ class BlockActions extends Column parent::__construct($context, $uiComponentFactory, $components, $data); } + /** + * @param array $items + * @return array + */ /** * Prepare Data Source * @@ -58,8 +64,35 @@ class BlockActions extends Column if (isset($item['block_id'])) { $item[$this->getData('name')] = [ 'edit' => [ - 'href' => $this->urlBuilder->getUrl(static::URL_PATH, ['block_id' => $item['block_id']]), - 'label' => __('Edit'), + 'href' => $this->urlBuilder->getUrl( + static::URL_PATH_EDIT, + [ + 'block_id' => $item['block_id'] + ] + ), + 'label' => __('Edit') + ], + 'details' => [ + 'href' => $this->urlBuilder->getUrl( + static::URL_PATH_DETAILS, + [ + 'block_id' => $item['block_id'] + ] + ), + 'label' => __('Details') + ], + 'delete' => [ + 'href' => $this->urlBuilder->getUrl( + static::URL_PATH_DELETE, + [ + 'block_id' => $item['block_id'] + ] + ), + 'label' => __('Delete'), + 'confirm' => [ + 'title' => __('Delete "${ $.$data.title }"'), + 'message' => __('Are you sure you wan\'t to delete a "${ $.$data.title }" record?') + ] ] ]; } diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php index 59edfeded291c2bd809dfa28f4ee9bf0d3a3b1ff..c28a8d4d07093cbd44506b57a0688615e18e78fe 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -17,7 +17,8 @@ use Magento\Framework\UrlInterface; class PageActions extends Column { /** Url path */ - const CMS_URL_PATH = 'cms/page/edit'; + const CMS_URL_PATH_EDIT = 'cms/page/edit'; + const CMS_URL_PATH_DELETE = 'cms/page/delete'; /** @var UrlBuilder */ protected $actionUrlBuilder; @@ -25,10 +26,6 @@ class PageActions extends Column /** @var UrlInterface */ protected $urlBuilder; - /** @var string */ - private $url; - - /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory @@ -36,7 +33,6 @@ class PageActions extends Column * @param UrlInterface $urlBuilder * @param array $components * @param array $data - * @param string $url */ public function __construct( ContextInterface $context, @@ -44,12 +40,10 @@ class PageActions extends Column UrlBuilder $actionUrlBuilder, UrlInterface $urlBuilder, array $components = [], - array $data = [], - $url = self::CMS_URL_PATH + array $data = [] ) { $this->urlBuilder = $urlBuilder; $this->actionUrlBuilder = $actionUrlBuilder; - $this->url = $url; parent::__construct($context, $uiComponentFactory, $components, $data); } @@ -63,15 +57,23 @@ class PageActions extends Column { if (isset($dataSource['data']['items'])) { foreach ($dataSource['data']['items'] as & $item) { + $name = $this->getData('name'); if (isset($item['page_id'])) { - $item[$this->getData('name')]['edit'] = [ - 'href' => $this->urlBuilder->getUrl($this->url, ['page_id' => $item['page_id']]), - 'label' => __('Edit'), - 'hidden' => true + $item[$name]['edit'] = [ + 'href' => $this->urlBuilder->getUrl(self::CMS_URL_PATH_EDIT, ['page_id' => $item['page_id']]), + 'label' => __('Edit') + ]; + $item[$name]['delete'] = [ + 'href' => $this->urlBuilder->getUrl(self::CMS_URL_PATH_DELETE, ['page_id' => $item['page_id']]), + 'label' => __('Delete'), + 'confirm' => [ + 'title' => __('Delete "${ $.$data.title }"'), + 'message' => __('Are you sure you wan\'t to delete a "${ $.$data.title }" record?') + ] ]; } if (isset($item['identifier'])) { - $item[$this->getData('name')]['preview'] = [ + $item[$name]['preview'] = [ 'href' => $this->actionUrlBuilder->getUrl( $item['identifier'], isset($item['_first_store_id']) ? $item['_first_store_id'] : null, diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index 44f4c1deea21702fdaca687e632e77d087536173..2936cb163fc6f91713a4ebeaa1ae0650b8976702 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -61,6 +61,9 @@ <container name="columns_controls"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="columnsData" xsi:type="array"> + <item name="provider" xsi:type="string">cms_block_listing.cms_block_listing.cms_block_columns</item> + </item> <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item> <item name="displayArea" xsi:type="string">dataGridActions</item> </item> @@ -225,13 +228,17 @@ <massaction name="listing_massaction"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="selectProvider" xsi:type="string">cms_block_listing.cms_block_listing.cms_block_columns.ids</item> <item name="displayArea" xsi:type="string">bottom</item> <item name="actions" xsi:type="array"> <item name="delete" xsi:type="array"> - <item name="confirm" xsi:type="string" translate="true">Are you sure you want to perform this action?</item> <item name="type" xsi:type="string">delete</item> <item name="label" xsi:type="string" translate="true">Delete</item> <item name="url" xsi:type="string">cms/block/massDelete</item> + <item name="confirm" xsi:type="array"> + <item name="title" xsi:type="string" translate="true">Delete items</item> + <item name="message" xsi:type="string" translate="true">Are you sure you wan't to delete selected items?</item> + </item> </item> </item> <item name="indexField" xsi:type="string">block_id</item> @@ -245,6 +252,7 @@ <item name="provider" xsi:type="string">cms_block_listing.cms_block_listing.listing_top.bookmarks</item> <item name="namespace" xsi:type="string">current.paging</item> </item> + <item name="selectProvider" xsi:type="string">cms_block_listing.cms_block_listing.cms_block_columns.ids</item> <item name="displayArea" xsi:type="string">bottom</item> <item name="options" xsi:type="array"> <item name="20" xsi:type="array"> @@ -275,10 +283,14 @@ <columns name="cms_block_columns"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="storageConfig" xsi:type="array"> + <item name="provider" xsi:type="string">cms_block_listing.cms_block_listing.listing_top.bookmarks</item> + <item name="namespace" xsi:type="string">current</item> + </item> <item name="childDefaults" xsi:type="array"> + <item name="controlVisibility" xsi:type="boolean">true</item> <item name="actionField" xsi:type="string">actions</item> <item name="clickAction" xsi:type="string">edit</item> - <item name="appendTo" xsi:type="string">cms_block_listing.cms_block_listing.listing_top.columns_controls</item> <item name="storageConfig" xsi:type="array"> <item name="provider" xsi:type="string">cms_block_listing.cms_block_listing.listing_top.bookmarks</item> <item name="root" xsi:type="string">columns.${ $.index }</item> @@ -294,14 +306,14 @@ </item> <item name="config" xsi:type="array"> <item name="indexField" xsi:type="string">block_id</item> - <item name="appendTo" xsi:type="boolean">false</item> + <item name="controlVisibility" xsi:type="boolean">false</item> </item> </argument> </column> <column name="block_id"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -314,7 +326,7 @@ <column name="title"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -326,7 +338,7 @@ <column name="identifier"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -397,7 +409,9 @@ <column name="actions" class="Magento\Cms\Ui\Component\Listing\Column\BlockActions"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="draggable" xsi:type="boolean">false</item> <item name="dataType" xsi:type="string">actions</item> + <item name="indexField" xsi:type="string">block_id</item> <item name="align" xsi:type="string">left</item> <item name="label" xsi:type="string" translate="true">Action</item> <item name="data_type" xsi:type="string">actions</item> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml index 7d154b524cdacc874bbf5a52daf8dc7dcda81d65..5df70e67bcf95379186052a5575a0793fbf0ede3 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml @@ -61,6 +61,9 @@ <container name="columns_controls"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="columnsData" xsi:type="array"> + <item name="provider" xsi:type="string">cms_page_listing.cms_page_listing.cms_page_columns</item> + </item> <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item> <item name="displayArea" xsi:type="string">dataGridActions</item> </item> @@ -237,13 +240,17 @@ <massaction name="listing_massaction"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="selectProvider" xsi:type="string">cms_page_listing.cms_page_listing.cms_page_columns.ids</item> <item name="displayArea" xsi:type="string">bottom</item> <item name="actions" xsi:type="array"> <item name="delete" xsi:type="array"> - <item name="confirm" xsi:type="string" translate="true">Delete selected items?</item> <item name="type" xsi:type="string">delete</item> <item name="label" xsi:type="string" translate="true">Delete</item> <item name="url" xsi:type="string">cms/page/massDelete</item> + <item name="confirm" xsi:type="array"> + <item name="title" xsi:type="string" translate="true">Delete items</item> + <item name="message" xsi:type="string" translate="true">Are you sure you wan't to delete selected items?</item> + </item> </item> <item name="disable" xsi:type="array"> <item name="type" xsi:type="string">disable</item> @@ -267,6 +274,7 @@ <item name="provider" xsi:type="string">cms_page_listing.cms_page_listing.listing_top.bookmarks</item> <item name="namespace" xsi:type="string">current.paging</item> </item> + <item name="selectProvider" xsi:type="string">cms_page_listing.cms_page_listing.cms_page_columns.ids</item> <item name="displayArea" xsi:type="string">bottom</item> <item name="options" xsi:type="array"> <item name="20" xsi:type="array"> @@ -297,10 +305,14 @@ <columns name="cms_page_columns"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="storageConfig" xsi:type="array"> + <item name="provider" xsi:type="string">cms_page_listing.cms_page_listing.listing_top.bookmarks</item> + <item name="namespace" xsi:type="string">current</item> + </item> <item name="childDefaults" xsi:type="array"> <item name="actionField" xsi:type="string">actions</item> <item name="clickAction" xsi:type="string">edit</item> - <item name="appendTo" xsi:type="string">cms_page_listing.cms_page_listing.listing_top.columns_controls</item> + <item name="controlVisibility" xsi:type="boolean">true</item> <item name="storageConfig" xsi:type="array"> <item name="provider" xsi:type="string">cms_page_listing.cms_page_listing.listing_top.bookmarks</item> <item name="root" xsi:type="string">columns.${ $.index }</item> @@ -315,15 +327,16 @@ <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/multiselect</item> </item> <item name="config" xsi:type="array"> + <item name="draggable" xsi:type="boolean">false</item> <item name="indexField" xsi:type="string">page_id</item> - <item name="appendTo" xsi:type="string"></item> + <item name="controlVisibility" xsi:type="boolean">false</item> </item> </argument> </column> <column name="page_id"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -336,7 +349,7 @@ <column name="title"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -348,7 +361,7 @@ <column name="identifier"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -373,7 +386,7 @@ <column name="store_id" class="Magento\Store\Ui\Component\Listing\Column\Store"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="bodyTmpl" xsi:type="string">ui/grid/cells/html</item> @@ -480,7 +493,7 @@ <column name="meta_keywords"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -493,7 +506,7 @@ <column name="meta_description"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/sortable</item> + <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> @@ -506,7 +519,9 @@ <column name="actions" class="Magento\Cms\Ui\Component\Listing\Column\PageActions"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> + <item name="draggable" xsi:type="boolean">false</item> <item name="dataType" xsi:type="string">actions</item> + <item name="indexField" xsi:type="string">page_id</item> <item name="align" xsi:type="string">left</item> <item name="label" xsi:type="string" translate="true">Action</item> <item name="data_type" xsi:type="string">actions</item> diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 0f86b821eeed054a234713c3db93a056fedeb4ab..21762022d84e565b42a9808389c03c6777d0d5ae 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -413,18 +413,6 @@ </item> </argument> </field> - <field name="country_id"> - <argument name="data" xsi:type="array"> - <item name="config" xsi:type="array"> - <item name="dataType" xsi:type="string">text</item> - <item name="formElement" xsi:type="string">select</item> - <item name="source" xsi:type="string">address</item> - <item name="validation" xsi:type="array"> - <item name="required-entry" xsi:type="boolean">true</item> - </item> - </item> - </argument> - </field> <field name="region"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -437,7 +425,6 @@ </field> <field name="region_id"> <argument name="data" xsi:type="array"> - <item name="customEntry" xsi:type="string">region</item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> <item name="formElement" xsi:type="string">select</item> diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product.js b/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product.js index c5437123aea58b42d4c3cb41d21a076709d3a7eb..b7badba6e79c142c8c1a92e0d60cdb41500507d4 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product.js +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product.js @@ -7,6 +7,7 @@ define([ 'jquery', 'mage/template', 'jquery/ui', + 'Magento_Ui/js/modal/modal', 'mage/translate', 'mage/adminhtml/grid' ], function ($, mageTemplate) { @@ -94,7 +95,7 @@ define([ }, /** - * Create dialog for show product + * Create modal for show product * * @private */ @@ -103,30 +104,14 @@ define([ selectedProductList = {}, popup = $('[data-role=add-product-dialog]'); - popup.dialog({ + popup.modal({ + type: 'slide', + innerScroll: true, title: $.mage.__('Add Products to Group'), - autoOpen: false, - minWidth: 980, - width: '75%', - modal: true, - resizable: true, - dialogClass: 'grouped', - position: { - my: 'left top', - at: 'center top', - of: 'body' - }, + modalClass: 'grouped', open: function () { - $(this).closest('.ui-dialog').addClass('ui-dialog-active'); - - var topMargin = $(this).closest('.ui-dialog').children('.ui-dialog-titlebar').outerHeight() + 55; - $(this).closest('.ui-dialog').css('margin-top', topMargin); - $(this).addClass('admin__scope-old'); // ToDo UI: remove with old styles removal }, - close: function () { - $(this).closest('.ui-dialog').removeClass('ui-dialog-active'); - }, buttons: [{ id: 'grouped-product-dialog-apply-button', text: $.mage.__('Add Selected Products'), @@ -137,14 +122,7 @@ define([ }); widget._resort(); widget._updateGridVisibility(); - $(this).dialog('close'); - } - }, { - id: 'grouped-product-dialog-cancel-button', - text: $.mage.__('Cancel'), - 'class': 'action-close', - click: function () { - $(this).dialog('close'); + popup.modal('closeModal'); } }] }); @@ -186,7 +164,7 @@ define([ $('[data-role=add-product]').on('click', function (event) { event.preventDefault(); - popup.dialog('open'); + popup.modal('openModal'); gridPopup.reload(); selectedProductList = {}; }); diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/edit/message.js b/app/code/Magento/Sales/view/adminhtml/web/order/edit/message.js index 5479b72127223e2f0b98730a965e94a97c845f9d..13511e83106674b301a992ca6a360e69c6c2b7b2 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/edit/message.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/edit/message.js @@ -7,7 +7,7 @@ define([ "jquery", "jquery/ui", - 'Magento_Ui/js/dialog/dialog', + 'Magento_Ui/js/modal/modal', "mage/translate" ], function($){ "use strict"; @@ -15,7 +15,7 @@ define([ options: { url: null, message: null, - dialog: null + modal: null }, /** @@ -26,10 +26,10 @@ define([ }, /** - * Show dialog + * Show modal */ showDialog: function() { - this.options.dialog.html(this.options.message).trigger('openDialog'); + this.options.dialog.html(this.options.message).modal('openModal'); }, /** @@ -40,11 +40,24 @@ define([ }, /** - * Prepare dialog + * Prepare modal * @protected */ _prepareDialog: function() { - this.options.dialog = $('<div class="ui-dialog-content ui-widget-content"></div>').dialog(); + var self = this; + + this.options.dialog = $('<div class="ui-dialog-content ui-widget-content"></div>').modal({ + type: 'popup', + modalClass: 'edit-order-popup', + title: $.mage.__('Edit Order'), + buttons: [{ + text: $.mage.__('Ok'), + 'class': 'action-primary', + click: function(){ + self.redirect(); + } + }] + }); } }); diff --git a/app/code/Magento/Ui/Component/Bookmark.php b/app/code/Magento/Ui/Component/Bookmark.php index cbb66e064806e28cfc15de7e9dcba59448b2d13c..7c8d54889f13ae6a1028dc2d61e6544f5ba464f1 100644 --- a/app/code/Magento/Ui/Component/Bookmark.php +++ b/app/code/Magento/Ui/Component/Bookmark.php @@ -75,12 +75,17 @@ class Bookmark extends AbstractComponent $bookmarks = $this->bookmarkManagement->loadByNamespace($namespace); /** @var \Magento\Ui\Api\Data\BookmarkInterface $bookmark */ foreach ($bookmarks->getItems() as $bookmark) { - $config['activeIndex'] = ($bookmark->isCurrent() ? $bookmark->getIdentifier() : 'default'); + $activeIndex = ($bookmark->isCurrent() ? $bookmark->getIdentifier() : false); + if ($bookmark->getIdentifier() == 'current') { $config['current'] = $bookmark->getConfig(); } else { $config['views'][$bookmark->getIdentifier()] = $bookmark->getConfig(); } + + if ($activeIndex !== false) { + $config['activeIndex'] = $activeIndex; + } } } diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php index f959e11ae83c85cf64e9e9502279005e44c2b738..2128543535b2b0d6823d60046ef60c21001913ce 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php @@ -23,6 +23,10 @@ class Save extends AbstractAction */ const CURRENT_IDENTIFIER = 'current'; + const ACTIVE_IDENTIFIER = 'activeIndex'; + + const VIEWS_IDENTIFIER = 'views'; + /** * @var BookmarkRepositoryInterface */ @@ -75,30 +79,37 @@ class Save extends AbstractAction { $bookmark = $this->bookmarkFactory->create(); $data = $this->_request->getParam('data'); - if (isset($data['views'])) { - foreach ($data['views'] as $identifier => $data) { - $updateBookmark = $this->checkBookmark($identifier); - if ($updateBookmark !== false) { - $bookmark = $updateBookmark; - } + $action = key($data); + switch($action) { + case self::ACTIVE_IDENTIFIER: + $this->updateCurrentBookmark($data[$action]); + break; + case self::CURRENT_IDENTIFIER: $this->updateBookmark( $bookmark, - $identifier, - (isset($data['label']) ? $data['label'] : ''), - $data + $action, + $bookmark->getTitle(), + $data[$action] ); - } - } else { - $identifier = isset($data['activeIndex']) - ? $data['activeIndex'] - : (isset($data[self::CURRENT_IDENTIFIER]) ? self::CURRENT_IDENTIFIER : ''); - $updateBookmark = $this->checkBookmark($identifier); - if ($updateBookmark !== false) { - $bookmark = $updateBookmark; - } - $this->updateBookmark($bookmark, $identifier, '', $data[$identifier]); + break; + + case self::VIEWS_IDENTIFIER: + foreach ($data[$action] as $identifier => $data) { + $this->updateBookmark( + $bookmark, + $identifier, + isset($data['label']) ? $data['label'] : '', + $data + ); + $this->updateCurrentBookmark($identifier); + } + + break; + + default: + throw new \LogicException(__('Unsupported bookmark action.')); } } @@ -114,20 +125,35 @@ class Save extends AbstractAction protected function updateBookmark(BookmarkInterface $bookmark, $identifier, $title, array $config = []) { $this->filterVars($config); + + $updateBookmark = $this->checkBookmark($identifier); + if ($updateBookmark !== false) { + $bookmark = $updateBookmark; + } + $bookmark->setUserId($this->userContext->getUserId()) ->setNamespace($this->_request->getParam('namespace')) ->setIdentifier($identifier) ->setTitle($title) - ->setConfig($config) - ->setCurrent($identifier !== self::CURRENT_IDENTIFIER); + ->setConfig($config); $this->bookmarkRepository->save($bookmark); + } + /** + * Update current bookmark + * + * @param string $identifier + * @return void + */ + protected function updateCurrentBookmark($identifier) + { $bookmarks = $this->bookmarkManagement->loadByNamespace($this->_request->getParam('namespace')); foreach ($bookmarks->getItems() as $bookmark) { if ($bookmark->getIdentifier() == $identifier) { - continue; + $bookmark->setCurrent(true); + } else { + $bookmark->setCurrent(false); } - $bookmark->setCurrent(false); $this->bookmarkRepository->save($bookmark); } } diff --git a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js index 882fd49f464af70702a9db2372a988a9462ab35c..4a0ceedf00035b14483671a2ec5caece43324907 100644 --- a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js +++ b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js @@ -122,6 +122,10 @@ define([ delete node.type; delete node.config; + if (children) { + node.initChildCount = _.size(children); + } + if (node.isTemplate) { node.isTemplate = false; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js index 84ea34f88934be1e8bc349a149482e3f297b49ee..14ba3ef49693ef7615b5d9052a94a2e60dbed5d2 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js @@ -47,8 +47,7 @@ define([ _.bindAll(this, 'reset'); this._super(); - - this.initialValue = this.getInititalValue(); + this.initialValue = this.getInitialValue(); this.value(this.initialValue); @@ -97,7 +96,7 @@ define([ * * @returns {*} Elements' value. */ - getInititalValue: function () { + getInitialValue: function () { var values = [this.value(), this.default], value; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/boolean.js b/app/code/Magento/Ui/view/base/web/js/form/element/boolean.js index 4782c853d210b11696c71f31d8d855a8e64eb4b0..efe82b0a13a1c4ee16cb71b7581e77ee41393fc8 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/boolean.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/boolean.js @@ -13,7 +13,7 @@ define([ * * @return {Boolean} */ - getInititalValue: function () { + getInitialValue: function () { return !!+this._super(); }, diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/date.js b/app/code/Magento/Ui/view/base/web/js/form/element/date.js index 140f063a315d00b0dd47a5c144503eb28c75b9cd..08f28b0c63722c92fb0e3b9cc580b1eaa1c3d107 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/date.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/date.js @@ -26,7 +26,7 @@ define([ * * @returns {String} */ - getInititalValue: function () { + getInitialValue: function () { var value = this._super(); if (value) { diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js b/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js index a6dc9e3b3afe600527a5b0fd470b05547b955789..6e7b2ac69a4ff61d6617faf2d0226084660890d5 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/multiselect.js @@ -20,7 +20,7 @@ define([ * * @returns {Number|String} */ - getInititalValue: function () { + getInitialValue: function () { var value = this._super(); return _.isString(value) ? value.split(',') : value; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/select.js b/app/code/Magento/Ui/view/base/web/js/form/element/select.js index 3034df1e2c67ad8154be1caa055fd97e0a45bbf2..225a6a3250d350bb55b90b4caeb5f8abc1ae9fa8 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/select.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/select.js @@ -14,9 +14,9 @@ define([ var inputNode = { parent: '${ $.$data.parentName }', type: 'form.input', - name: '<%= $data.index %>_input', - dataScope: '<%= $data.customEntry %>', - customScope: '<%= $data.customScope %>', + name: '${ $.$data.index }_input', + dataScope: '${ $.$data.customEntry }', + customScope: '${ $.$data.customScope }', sortOrder: { after: '${ $.$data.name }' }, @@ -157,6 +157,7 @@ define([ initFilter: function () { var filter = this.filterBy; + this.filter(this.default, filter.field); this.setLinks({ filter: filter.target }, 'imports'); @@ -181,7 +182,7 @@ define([ * * @returns {Number|String} */ - getInititalValue: function () { + getInitialValue: function () { var value = this._super(); if (value !== '') { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js index 33a095e64e806024affa49638b9fe8bb40566e79..e0b7525f007128ec86e7409519c1df011081aa63 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js @@ -4,24 +4,282 @@ */ define([ 'underscore', - './column' -], function (_, Column) { + 'mageUtils', + 'uiRegistry', + './column', + 'Magento_Ui/js/modal/confirm' +], function (_, utils, registry, Column, confirm) { 'use strict'; return Column.extend({ defaults: { - headerTmpl: 'ui/grid/columns/actions', - bodyTmpl: 'ui/grid/cells/actions' + bodyTmpl: 'ui/grid/cells/actions', + actions: [], + rows: [], + templates: { + actions: {} + }, + rowsProvider: '${ $.parentName }', + imports: { + rows: '${ $.rowsProvider }:rows' + }, + listens: { + rows: 'updateActions' + } }, - getDisplayed: function (actions) { - actions = _.filter(actions, function (action) { - return !('hidden' in action) || !action.hidden; + /** + * Initializes observable properties. + * + * @returns {ActionsColumn} Chainable. + */ + initObservable: function () { + this._super() + .observe('actions opened'); + + return this; + }, + + /** + * Returns specific action of a specified row + * or all action objects associated with it. + * + * @param {Number} rowIndex - Index of a row. + * @param {String} [actionIndex] - Action identifier. + * @returns {Array|Object} + */ + getAction: function (rowIndex, actionIndex) { + var rowActions = this.actions()[rowIndex]; + + return rowActions && actionIndex ? + rowActions[actionIndex] : + rowActions; + }, + + /** + * Returns visible actions for a specified row. + * + * @param {Number} rowIndex - Index of a row. + * @returns {Array} Visible actions. + */ + getVisibleActions: function (rowIndex) { + var rowActions = this.getAction(rowIndex); + + return _.filter(rowActions, this.isActionVisible, this); + }, + + /** + * Adds new action. If action with a specfied identifier + * already exists, than the original will be overrided. + * + * @param {String} index - Actions' identifier. + * @param {Object} action - Actions' data. + * @returns {ActionsColumn} Chainable. + */ + addAction: function (index, action) { + var actionTmpls = this.templates.actions; + + actionTmpls[index] = action; + + this.updateActions(); + + return this; + }, + + /** + * Recreates actions for each row. + * + * @returns {ActionsColumn} Chainable. + */ + updateActions: function () { + var rows = this.rows, + actions = rows.map(this._formatActions, this); + + this.actions(actions); + + return this; + }, + + /** + * Processes actions, setting additional information to them and + * evaluating ther properties as a string templates. + * + * @private + * @param {Object} row - Row object. + * @param {Number} rowIndex - Index of a row. + * @returns {Array} + */ + _formatActions: function (row, rowIndex) { + var rowActions = row[this.index] || {}, + recordId = row[this.indexField], + customActions = this.templates.actions; + + /** + * Actions iterator. + */ + function iterate(action, index) { + action = utils.extend({ + index: index, + rowIndex: rowIndex, + recordId: recordId + }, action); + + return utils.template(action, row, true); + } + + rowActions = _.mapObject(rowActions, iterate); + customActions = _.map(customActions, iterate); + + customActions.forEach(function (action) { + rowActions[action.index] = action; + }); + + return rowActions; + }, + + /** + * Applies specified action. + * + * @param {String} actionIndex - Actions' identifier. + * @param {Number} rowIndex - Index of a row. + * @returns {ActionsColumn} Chainable. + */ + applyAction: function (actionIndex, rowIndex) { + var action = this.getAction(rowIndex, actionIndex), + callback; + + if (!action.href && !action.callback) { + return this; + } + + callback = this._getCallback(action); + + action.confirm ? + this._confirm(action, callback) : + callback(); + + return this; + }, + + /** + * Creates action callback based on its' data. If action doesn't spicify + * a callback function than the default one will be used. + * + * @private + * @param {Object} action - Actions' object. + * @returns {Function} Callback function. + */ + _getCallback: function (action) { + var args = [action.index, action.recordId, action], + callback = action.callback; + + if (utils.isObject(callback)) { + args.unshift(callback.target); + + callback = registry.async(callback.provider); + } else if (typeof callback != 'function') { + callback = this.defaultCallback.bind(this); + } + + return function () { + callback.apply(null, args); + }; + }, + + /** + * Default action callback. Redirects to + * the specified in actions' data url. + * + * @param {String} actionIndex - Actions' identifier. + * @param {(Number|String)} recordId - Id of the record accociated + * with a specfied action. + * @param {Object} action - Actions' data. + */ + defaultCallback: function (actionIndex, recordId, action) { + window.location.href = action.href; + }, + + /** + * Shows actions' confirmation window. + * + * @param {Object} action - Actions' data. + * @param {Function} callback - Callback that will be + * invoked if action is confirmed. + */ + _confirm: function (action, callback) { + var confirmData = action.confirm; + + confirm({ + title: confirmData.title, + content: confirmData.message, + actions: { + confirm: callback + } }); + }, + + /** + * Checks if row has only one visible action. + * + * @param {Number} rowIndex - Row index. + * @returns {Boolean} + */ + isSingle: function (rowIndex) { + return this.getVisibleActions(rowIndex).length === 1; + }, + + /** + * Checks if row has more than one visible action. + * + * @param {Number} rowIndex - Row index. + * @returns {Boolean} + */ + isMultiple: function (rowIndex) { + return this.getVisibleActions(rowIndex).length > 1; + }, + + /** + * Checks if action should be displayed. + * + * @param {Object} action - Action object. + * @returns {Boolean} + */ + isActionVisible: function (action) { + return action.hidden !== true; + }, + + /** + * Opens or closes specific actions list. + * + * @param {Number} rowIndex - Index of a row, + * where actions are displayed. + * @returns {ActionsColumn} Chainable. + */ + toggleList: function (rowIndex) { + var state = false; + + if (rowIndex !== this.opened()) { + state = rowIndex; + } + + this.opened(state); + + return this; + }, - this.displayed = actions; + /** + * Closes actions list. + * + * @param {Number} rowIndex - Index of a row, + * where actions are displayed. + * @returns {ActionsColumn} + */ + closeList: function (rowIndex) { + if (this.opened() === rowIndex) { + this.opened(false); + } - return actions; + return this; } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js index f0d3202c7f6822eb2c97aa3f89499f18a7048c32..1eb359826b1cff89f423be340531dd34d52c8157 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js @@ -13,37 +13,111 @@ define([ defaults: { headerTmpl: 'ui/grid/columns/text', bodyTmpl: 'ui/grid/cells/text', - sortable: false, + sortable: true, + sorting: false, visible: true, + draggable: true, links: { - visible: '${ $.storageConfig.path }.visible' + visible: '${ $.storageConfig.path }.visible', + sorting: '${ $.storageConfig.path }.sorting' + }, + imports: { + exportSorting: 'sorting' + }, + listens: { + '${ $.provider }:params.sorting.field': 'onSortChange' + }, + modules: { + source: '${ $.provider }' } }, + /** + * Initializes observable properties. + * + * @returns {Column} Chainable. + */ initObservable: function () { this._super() - .observe('visible'); + .observe('visible dragging dragover sorting'); return this; }, - applyState: function (property, state) { - var storage = this.storage(), - namespace = this.storageConfig.root + '.' + property, - data, - value; + /** + * Applies specified stored state of a column or one of its' properties. + * + * @param {String} state - Defines what state should be used: saved or default. + * @param {String} [property] - Defines what columns' property should be applied. + * If not specfied, than all collumns stored properties will be used. + * @returns {Column} Chainable. + */ + applyState: function (state, property) { + var namespace = this.storageConfig.root; - if (state === 'default') { - data = storage.getDefault(); - } else if (state === 'last') { - data = storage.getSaved(); + if (property) { + namespace += '.' + property; } - value = utils.nested(data, namespace); + this.storage('applyState', state, namespace); - if (!_.isUndefined(value)) { - this.set(property, value); + return this; + }, + + /** + * Sets columns' sorting. If column is currently sorted, + * than its' direction will be toggled. + * + * @param {*} [enable=true] - If false, than sorting will + * be removed from a column. + * @returns {Column} Chainable. + */ + sort: function (enable) { + var direction; + + if (!this.sortable) { + return this; + } + + enable = enable !== false ? true : false; + + direction = enable ? + this.sorting() ? + this.toggleDirection() : + 'asc' : + false; + + this.sorting(direction); + + return this; + }, + + /** + * Exports sorting data to the dataProvider if + * sorting of a column is enabled. + * + * @param {(String|Boolean)} sorting - Columns' sorting state. + */ + exportSorting: function (sorting) { + if (!sorting) { + return; } + + this.source('set', 'params.sorting', { + field: this.index, + direction: sorting + }); + }, + + /** + * Toggles sorting direcction. + * + * @returns {String} New direction. + */ + toggleDirection: function () { + return this.sorting() === 'asc' ? + 'desc' : + 'asc'; }, getClickUrl: function (row) { @@ -61,16 +135,43 @@ define([ window.location.href = url; }, + /** + * Ment to preprocess data associated with a current columns' field. + * + * @param {*} data - Data to be preprocessed. + * @returns {String} + */ getLabel: function (data) { return data; }, + /** + * Returns path to the columns' header template. + * + * @returns {String} + */ getHeader: function () { return this.headerTmpl; }, + /** + * Returns path to the columns' body template. + * + * @returns {String} + */ getBody: function () { return this.bodyTmpl; + }, + + /** + * Listener of the providers' sorting state changes. + * + * @param {Srting} field - Field by which current sorting is performed. + */ + onSortChange: function (field) { + if (field !== this.index) { + this.sort(false); + } } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/date.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/date.js index 3393c0094a8e8853a5a95b18de6e4805a92ac723..90ace7d79af6ad4eb96d1c0bceb5140ccdc7afe1 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/date.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/date.js @@ -5,23 +5,40 @@ define([ 'mageUtils', 'moment', - './sortable' -], function (utils, moment, Sortable) { + './column' +], function (utils, moment, Column) { 'use strict'; - return Sortable.extend({ + return Column.extend({ defaults: { dateFormat: 'MMM D, YYYY h:mm:ss A' }, + /** + * Initializes components' static properties. + * + * @returns {DateColumn} Chainable. + */ initProperties: function () { this.dateFormat = utils.normalizeDate(this.dateFormat); return this._super(); }, - getLabel: function (data) { - return moment(data).isValid() ? moment(data).format(this.dateFormat) : ''; + /** + * Formats incoming date based on the 'dateFormat' property. + * + * @param {String} date - Date to be formatted. + * @returns {String} Formatted date. + */ + getLabel: function (date) { + date = moment(date); + + date = date.isValid() ? + date.format(this.dateFormat) : + ''; + + return date; } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js index e6b33d1e62854e72ea6d8ed311317140cf4ad11b..7ced03bfba9256fd0fa18591e4042f2018339d53 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js @@ -13,13 +13,13 @@ define([ defaults: { headerTmpl: 'ui/grid/columns/multiselect', bodyTmpl: 'ui/grid/cells/multiselect', + sortable: false, menuVisible: false, excludeMode: false, allSelected: false, indetermine: false, selected: [], excluded: [], - ns: '${ $.provider }:params', actions: [{ value: 'selectAll', label: $t('Select all') @@ -40,13 +40,9 @@ define([ }, listens: { - '${ $.ns }.filters': 'deselectAll', + '${ $.provider }:params.filters': 'deselectAll', selected: 'onSelectedChange', rows: 'onRowsChange' - }, - - modules: { - source: '${ $.provider }' } }, @@ -87,7 +83,10 @@ define([ }, /** - * Selects all grid records, even those that are not visible on the page. + * Selects all grid records, even those that + * are not visible on the page. + * + * @returns {Multiselect} Chainable. */ selectAll: function () { this.excludeMode(true); @@ -100,44 +99,54 @@ define([ /** * Deselects all grid records. + * + * @returns {Multiselect} Chainable. */ deselectAll: function () { this.excludeMode(false); this.clearExcluded() - .deselectPage(); - this.selected.removeAll(); + .selected.removeAll(); return this; }, /** * Selects or deselects all records. + * + * @returns {Multiselect} Chainable. */ toggleSelectAll: function () { - return this.allSelected() ? - this.deselectAll() : - this.selectAll(); + this.allSelected() ? + this.deselectAll() : + this.selectAll(); + + return this; }, /** * Selects all records on the current page. + * + * @returns {Multiselect} Chainable. */ selectPage: function () { - this.selected( - _.union(this.selected(), this.getIds()) - ); + var selected = _.union(this.selected(), this.getIds()); + + this.selected(selected); return this; }, /** * Deselects all records on the current page. + * + * @returns {Multiselect} Chainable. */ deselectPage: function () { - var currentPageIds = this.getIds(); + var pageIds = this.getIds(); + this.selected.remove(function (value) { - return currentPageIds.indexOf(value) !== -1; + return !!~pageIds.indexOf(value); }); return this; @@ -209,18 +218,17 @@ define([ }, /** - * Exports selections to the data provider. + * Returns selections data. + * + * @returns {Object} */ - exportSelections: function () { - var data = {}, - type; - - type = this.excludeMode() ? 'excluded' : 'selected'; - - data[type] = this[type](); - data.total = this.totalSelected(); - - this.source('set', 'config.multiselect', data); + getSelections: function () { + return { + excluded: this.excluded(), + selected: this.selected(), + total: this.totalSelected(), + excludeMode: this.excludeMode() + }; }, /** @@ -231,27 +239,27 @@ define([ */ isActionRelevant: function (actionId) { var pageIds = this.getIds().length, - multiplePages = pageIds < this.totalRecords(); + multiplePages = pageIds < this.totalRecords(), + result = true; switch (actionId) { case 'selectPage': - - return multiplePages && !this.isPageSelected(true); + result = multiplePages && !this.isPageSelected(true); + break; case 'deselectPage': - - return multiplePages && this.isPageSelected(); + result = multiplePages && this.isPageSelected(); + break; case 'selectAll': - - return !this.allSelected(); + result = !this.allSelected(); + break; case 'deselectAll': - - return this.totalSelected() > 0; + result = this.totalSelected() > 0; } - return true; + return result; }, /** @@ -286,6 +294,8 @@ define([ /** * Updates values of the 'allSelected' * and 'indetermine' properties. + * + * @returns {Multiselect} Chainable. */ updateState: function () { var selected = this.selected().length, @@ -309,15 +319,14 @@ define([ }, /** - * Callback method to handle change of the selected items. + * Callback method to handle changes of selected items. * - * @param {Array} selected - List of the currently selected items. + * @param {Array} selected - An array of currently selected items. */ onSelectedChange: function (selected) { this.updateExcluded(selected) .countSelected() - .updateState() - .exportSelections(); + .updateState(); }, /** @@ -325,12 +334,12 @@ define([ * based on "selectMode" property. */ onRowsChange: function () { - var newSelected; + var newSelections; if (this.excludeMode()) { - newSelected = _.union(this.getIds(true), this.selected()); + newSelections = _.union(this.getIds(true), this.selected()); - this.selected(newSelected); + this.selected(newSelections); } } }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/select.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/select.js index 54185ef6617aa82740e0bb7f76f1650798361f93..46cbff1cf1dfd610ea5a3cd016304493cf880fde 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/select.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/select.js @@ -3,21 +3,30 @@ * See COPYING.txt for license details. */ define([ - './sortable' -], function (Sortable) { + './column' +], function (Column) { 'use strict'; - return Sortable.extend({ - getLabel: function (data) { + return Column.extend({ + /** + * Retrieves label associated with a provided value. + * + * @param {(String|Number)} value - Value of the option. + * @returns {String} + */ + getLabel: function (value) { var options = this.options || [], label = ''; - data = data || ''; + value = value || ''; + + /*eslint-disable eqeqeq*/ options.some(function (item) { label = item.label; - return item.value == data; + return item.value == value; }); + /*eslint-enable eqeqeq*/ return label; } diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/sortable.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/sortable.js deleted file mode 100644 index d54971607c37b33ce3a56455d7b7f8bb18aef05b..0000000000000000000000000000000000000000 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/sortable.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright © 2015 Magento. All rights reserved. - * See COPYING.txt for license details. - */ -define([ - './column' -], function (Column) { - 'use strict'; - - return Column.extend({ - defaults: { - sortable: true, - sorting: false, - classes: { - 'asc': '_ascend', - 'desc': '_descend' - }, - links: { - sorting: '${ $.storageConfig.path }.sorting' - }, - imports: { - setSortClass: 'sorting', - push: 'sorting' - }, - listens: { - '${ $.provider }:params.sorting.field': 'onSortChange' - }, - modules: { - source: '${ $.provider }' - } - }, - - initObservable: function () { - this._super() - .observe('sorting sortClass'); - - return this; - }, - - sort: function (enabled) { - var direction; - - direction = enabled !== false ? - this.sorting() ? - this.toggleDirection() : - 'asc' : - false; - - this.sorting(direction); - }, - - push: function (sorting) { - if (!sorting) { - return; - } - - this.source('set', 'params.sorting', { - field: this.index, - direction: sorting - }); - }, - - toggleDirection: function () { - return this.sorting() === 'asc' ? - 'desc' : - 'asc'; - }, - - setSortClass: function (sorting) { - var sortClass = this.classes[sorting] || ''; - - this.sortClass(sortClass); - }, - - onSortChange: function (field) { - if (field !== this.index) { - this.sort(false); - } - } - }); -}); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js index 78a115f2498a3068ca02baa8e07922013a91424a..286fe4307408feeaf678c3e778af0adf6a539edb 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js @@ -12,6 +12,22 @@ define([ ], function (_, utils, registry, Storage, Collapsible, layout) { 'use strict'; + /** + * Removes 'current' namespace from a 'path' string. + * + * @param {String} path + * @returns {String} Path without namespace. + */ + function removeStateNs(path) { + path = typeof path == 'string' ? path.split('.') : ''; + + if (path[0] === 'current') { + path.shift(); + } + + return path.join('.'); + } + return Collapsible.extend({ defaults: { template: 'ui/grid/controls/bookmarks/bookmarks', @@ -29,7 +45,7 @@ define([ }, newView: { label: 'New View', - index: '${ Date.now() }', + index: '_${ Date.now() }', editing: true, isNew: true } @@ -59,8 +75,8 @@ define([ */ initialize: function () { utils.limit(this, 'saveSate', 2000); - utils.limit(this, 'checkChanges', 200); utils.limit(this, '_defaultPolyfill', 1000); + utils.limit(this, 'checkChanges', 50); this._super() .initViews(); @@ -223,7 +239,9 @@ define([ */ applyView: function (view) { if (typeof view === 'string') { - view = this.elems.findWhere({index: view}); + view = this.elems.findWhere({ + index: view + }); } view.active(true); @@ -234,6 +252,34 @@ define([ return this; }, + /** + * Applies specified views' data on a current data object. + * + * @param {String} state - Defines what state shultd be used: default or saved. + * @param {String} [path] - Path to the property whose value + * will be inserted to a current data object. + * @returns {Bookmarks} Chainable. + */ + applyState: function (state, path) { + var view, + value; + + view = state === 'default' ? + this.defaultView : + this.activeView(); + + path = removeStateNs(path); + value = view.getData(path); + + if (!_.isUndefined(value)) { + path = path ? 'current.' + path : 'current'; + + this.set(path, value); + } + + return this; + }, + /** * Saves current data state. * @@ -260,24 +306,6 @@ define([ return this; }, - /** - * Retrieves last saved data of a current view. - * - * @returns {Object} - */ - getSaved: function () { - return this.activeView().getData(); - }, - - /** - * Retrieves default data. - * - * @returns {Object} - */ - getDefault: function () { - return this.defaultView.getData(); - }, - /** * Defines default data if it wasn't gathered previously. * Assumes that if theres is no views available, diff --git a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/view.js b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/view.js index b42fedfe78dd725567fbb7ed3f7f48b196533af5..afd5d4276de442947da6755cf208f191268226d3 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/view.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/view.js @@ -58,12 +58,19 @@ define([ }, /** - * Retrieves current data. + * Retrieves copied views' data. * - * @returns {Object} + * @param {String} [path] - Path to the specific property. + * @returns {*} */ - getData: function () { - return utils.copy(this.data.items); + getData: function (path) { + var data = this.data.items; + + if (path) { + data = utils.nested(data, path); + } + + return utils.copy(data); }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/controls/columns.js b/app/code/Magento/Ui/view/base/web/js/grid/controls/columns.js index f48a86abab1c2937c1a0076137fa7539fc41f2ca..765e437334285090378e1db15d8ac33753da9ab8 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/controls/columns.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/controls/columns.js @@ -3,10 +3,11 @@ * See COPYING.txt for license details. */ define([ + 'underscore', 'mageUtils', 'mage/translate', 'Magento_Ui/js/lib/collapsible' -], function (utils, $t, Collapsible) { +], function (_, utils, $t, Collapsible) { 'use strict'; return Collapsible.extend({ @@ -15,32 +16,59 @@ define([ minVisible: 1, maxVisible: 30, viewportSize: 18, + columnsData: { + container: 'elems' + }, + imports: { + addColumns: '${ $.columnsData.provider }:${ $.columnsData.container }' + }, templates: { headerMsg: $t('${ $.visible } out of ${ $.total } visible') } }, /** - * Action Reset + * Resets columns visibility to theirs default state. + * + * @returns {Columns} Chainable. */ reset: function () { - this.elems.each('applyState', 'visible', 'default'); + this.elems.each('applyState', 'default', 'visible'); return this; }, /** - * Action Cancel + * Applies last saved state of columns visibility. + * + * @returns {Columns} Chainable. */ cancel: function () { - this.elems.each('applyState', 'visible', 'last'); + this.elems.each('applyState', 'saved', 'visible'); return this; }, /** - * Helper, which helps to stop resizing. - * viewportSize limits number of elements. + * Adds columns whose visibility can be controlled to the component. + * + * @param {Array} columns - Elements array that will be added to component. + * @returns {Columns} Chainable. + */ + addColumns: function (columns) { + columns = _.where(columns, { + controlVisibility: true + }); + + this.insertChild(columns); + + return this; + }, + + /** + * Defines whether child elements array length + * is greater than the 'viewportSize' property. + * * @returns {Boolean} */ hasOverflow: function () { @@ -51,6 +79,7 @@ define([ * Helper, checks * - if less than one item choosen * - if more then viewportMaxSize choosen + * * @param {Object} elem * @returns {Boolean} */ @@ -63,7 +92,8 @@ define([ }, /** - * Helper, returns number of visible checkboxes + * Counts number of visible columns. + * * @returns {Number} */ countVisible: function () { @@ -72,8 +102,7 @@ define([ /** * Compile header message from headerMessage setting. - * Expects Underscore template format - * @param {String} text - underscore-format template + * * @returns {String} */ getHeaderMessage: function () { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/dnd.js b/app/code/Magento/Ui/view/base/web/js/grid/dnd.js new file mode 100644 index 0000000000000000000000000000000000000000..d1eb5ee827c40825b4f7df3620bb7215d4e21b68 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/dnd.js @@ -0,0 +1,473 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'ko', + 'jquery', + 'underscore', + 'Magento_Ui/js/lib/class' +], function (ko, $, _, Class) { + 'use strict'; + + var isTouchDevice = typeof document.ontouchstart !== 'undefined', + transformProp; + + /** + * Defines supported css 'transform' property. + * + * @returns {String|Undefined} + */ + transformProp = (function () { + var style = document.body.style, + base = 'Transform', + vendors = ['webkit', 'moz', 'ms', 'o'], + vi = vendors.length, + property; + + if (typeof style.transform != 'undefined') { + return 'transform'; + } + + while (vi--) { + property = vendors[vi] + base; + + if (typeof style[property] != 'undefined') { + return property; + } + } + })(); + + /** + * Returns first touch data if it's available. + * + * @param {(MouseEvent|TouchEvent)} e - Event object. + * @returns {Object} + */ + function getTouch(e) { + return e.touches ? e.touches[0] : e; + } + + /** + * Moves specified DOM element to the x and y coordinates. + * + * @param {HTMLElement} elem - Element to be relocated. + * @param {Number} x - X coordinate. + * @param {Number} y - Y coordinate. + */ + function locate(elem, x, y) { + var value = 'translate(' + x + 'px,' + y + 'px)'; + + elem.style[transformProp] = value; + } + + /*eslint-disable no-extra-parens*/ + /** + * Checks if specified coordinate is inside of the provided area. + * + * @param {Number} x - X coordinate. + * @param {Number} y - Y coordinate. + * @param {Object} area - Object which represents area. + * @returns {Boolean} + */ + function isInside(x, y, area) { + return ( + area && + x >= area.left && x <= area.right && + y >= area.top && y <= area.bottom + ); + } + /*eslint-enable no-extra-parens*/ + + /** + * Calculates distance between two points. + * + * @param {Number} x1 - X coordinate of a first point. + * @param {Number} y1 - Y coordinate of a first point. + * @param {Number} x2 - X coordinate of a second point. + * @param {Number} y2 - Y coordinate of a second point. + * @returns {Number} Distance between points. + */ + function distance(x1, y1, x2, y2) { + var dx = x2 - x1, + dy = y2 - y1; + + dx *= dx; + dy *= dy; + + return Math.sqrt(dx + dy); + } + + /** + * Returns viewModel associated with a provided DOM element. + * + * @param {HTMLElement} elem + * @returns {Object|Array} + */ + function getModel(elem) { + return ko.contextFor(elem).$data; + } + + return Class.extend({ + defaults: { + noSelectClass: '_no-select', + hiddenClass: '_hidden', + fixedX: false, + fixedY: true, + minDistance: 2, + columns: [] + }, + + /** + * Initializes Dnd component. + * + * @returns {Dnd} Chainable. + */ + initialize: function () { + _.bindAll(this, 'onMouseMove', 'onMouseUp', 'onMouseDown'); + + this.$body = $('body'); + + this._super() + .initListeners(); + + return this; + }, + + /** + * Binds necessary events listeners. + * + * @returns {Dnd} Chainbale. + */ + initListeners: function () { + var addListener = document.addEventListener; + + if (isTouchDevice) { + addListener('touchmove', this.onMouseMove, false); + addListener('touchend', this.onMouseUp, false); + addListener('touchleave', this.onMouseUp, false); + } else { + addListener('mousemove', this.onMouseMove, false); + addListener('mouseup', this.onMouseUp, false); + } + + return this; + }, + + /** + * Sets specified column as a draggable element. + * + * @param {HTMLTableHeaderCellElement} column - Columns header element. + * @returns {Dnd} Chainable. + */ + addColumn: function (column) { + this.columns.push(column); + + isTouchDevice ? + column.addEventListener('touchstart', this.onMouseDown, false) : + column.addEventListener('mousedown', this.onMouseDown, false); + + return this; + }, + + /** + * Defines specified table element as a main container. + * + * @param {HTMLTableElement} table + * @returns {Dnd} Chainable. + */ + setTable: function (table) { + this.table = table; + + return this; + }, + + /** + * Defines specified table element as a draggable table. + * Only this element will be moved across the screen. + * + * @param {HTMLTableElement} dragTable + * @returns {Dnd} Chainable. + */ + setDragTable: function (dragTable) { + this.dragTable = dragTable; + + return this; + }, + + /** + * Calculates coordinates of draggable elements. + * + * @returns {Dnd} Chainbale. + */ + _cacheCoords: function () { + var container = this.table.getBoundingClientRect(), + bodyRect = document.body.getBoundingClientRect(), + grabbed = this.grabbed, + dragElem = grabbed.elem, + cells = _.toArray(dragElem.parentNode.cells), + rect; + + this.coords = this.columns.map(function (column) { + var data; + + rect = column.getBoundingClientRect(); + + data = { + index: cells.indexOf(column), + target: column, + orig: rect, + left: rect.left - bodyRect.left, + right: rect.right - bodyRect.left, + top: rect.top - bodyRect.top, + bottom: container.bottom - bodyRect.top + }; + + if (column === dragElem) { + this.dragArea = data; + + grabbed.shiftX = rect.left - grabbed.x; + grabbed.shiftY = rect.top - grabbed.y; + } + + return data; + }, this); + + return this; + }, + + /** + * Coppies dimensions of a grabbed column + * to a draggable grid. + * + * @param {HTMLTableHeaderCellElement} elem - Grabbed column. + * @returns {Dnd} Chainable. + */ + _copyDimensions: function (elem) { + var dragTable = this.dragTable, + dragBody = dragTable.tBodies[0], + dragTrs = dragBody ? dragBody.children : [], + origTrs = _.toArray(this.table.tBodies[0].children), + columnIndex = _.toArray(elem.parentNode.cells).indexOf(elem), + origTd, + dragTr; + + dragTable.style.width = elem.offsetWidth + 'px'; + dragTable.tHead.firstElementChild.cells[0].style.height = elem.offsetHeight + 'px'; + + origTrs.forEach(function (origTr, rowIndex) { + origTd = origTr.cells[columnIndex]; + dragTr = dragTrs[rowIndex]; + + if (origTd && dragTr) { + dragTr.cells[0].style.height = origTd.offsetHeight + 'px'; + } + }); + + return this; + }, + + /** + * Matches provided coordinates to available areas. + * + * @param {Number} x - X coordinate of a mouse pointer. + * @param {Number} y - Y coordinate of a mouse pointer. + * @returns {Object|Undefined} Matched area. + */ + _getDropArea: function (x, y) { + return _.find(this.coords, function (area) { + return isInside(x, y, area); + }); + }, + + /** + * Updates state of hovered areas. + * + * @param {Number} x - X coordinate of a mouse pointer. + * @param {Number} y - Y coordinate of a mouse pointer. + */ + _updateAreas: function (x, y) { + var leavedArea = this.dropArea, + area = this.dropArea = this._getDropArea(x, y); + + if (leavedArea) { + this.dragleave(leavedArea); + } + + if (area && area.target !== this.dragArea.target) { + this.dragenter(area); + } + }, + + /** + * Grab action handler. + * + * @param {Number} x - X coordinate of a grabbed point. + * @param {Number} y - Y coordinate of a grabbed point. + * @param {HTMLElement} elem - Grabbed elemenet. + */ + grab: function (x, y, elem) { + this.initDrag = true; + this.grabbed = { + x: x, + y: y, + elem: elem + }; + + this.$body.addClass(this.noSelectClass); + }, + + /** + * Dragstart action handler. + * + * @param {HTMLTableHeaderCellElement} elem - Element which is dragging. + */ + dragstart: function (elem) { + this.initDrag = false; + this.dropArea = false; + this.dragging = true; + + getModel(elem).dragging(true); + + this._cacheCoords() + ._copyDimensions(elem); + + $(this.dragTable).removeClass(this.hiddenClass); + }, + + /** + * Drag action handler. Locates draggable + * grid at a specified coordinates. + * + * @param {Number} x - X coordinate. + * @param {Number} y - Y coordinate. + */ + drag: function (x, y) { + var grabbed = this.grabbed, + dragArea = this.dragArea, + posX = x + grabbed.shiftX, + posY = y + grabbed.shiftY; + + if (this.fixedX) { + x = dragArea.left; + posX = dragArea.orig.left; + } + + if (this.fixedY) { + y = dragArea.top; + posY = dragArea.orig.top; + } + + locate(this.dragTable, posX, posY); + + if (!isInside(x, y, this.dropArea)) { + this._updateAreas(x, y); + } + }, + + /** + * Dragenter action handler. + * + * @param {Object} dropArea + */ + dragenter: function (dropArea) { + var direction = this.dragArea.index < dropArea.index ? + 'left' : + 'right'; + + getModel(dropArea.target).dragover(direction); + }, + + /** + * Dragleave action handler. + * + * @param {Object} dropArea + */ + dragleave: function (dropArea) { + getModel(dropArea.target).dragover(false); + }, + + /** + * Dragend action handler. + * + * @param {Object} dragArea + */ + dragend: function (dragArea) { + var dropArea = this.dropArea, + dragElem = dragArea.target; + + this.dragging = false; + + $(this.dragTable).addClass(this.hiddenClass); + + getModel(dragElem).dragging(false); + + if (dropArea && dropArea.target !== dragElem) { + this.drop(dropArea, dragArea); + } + }, + + /** + * Drop action handler. + * + * @param {Object} dropArea + * @param {Object} dragArea + */ + drop: function (dropArea, dragArea) { + var dropModel = getModel(dropArea.target), + dragModel = getModel(dragArea.target); + + getModel(this.table).insertChild(dragModel, dropModel); + dropModel.dragover(false); + }, + + /** + * Documents' 'mousemove' event handler. + * + * @param {(MouseEvent|TouchEvent)} e - Event object. + */ + onMouseMove: function (e) { + var grab = this.grabbed, + touch = getTouch(e), + x = touch.pageX, + y = touch.pageY; + + if (this.initDrag || this.dragging) { + e.preventDefault(); + } + + if (this.initDrag && distance(x, y, grab.x, grab.y) >= this.minDistance) { + this.dragstart(grab.elem); + } + + if (this.dragging) { + this.drag(x, y); + } + }, + + /** + * Documents' 'mouseup' event handler. + */ + onMouseUp: function () { + if (this.initDrag || this.dragging) { + this.initDrag = false; + this.$body.removeClass(this.noSelectClass); + } + + if (this.dragging) { + this.dragend(this.dragArea); + } + }, + + /** + * Columns' 'mousedown' event handler. + * + * @param {(MouseEvent|TouchEvent)} e - Event object. + */ + onMouseDown: function (e) { + var touch = getTouch(e); + + this.grab(touch.pageX, touch.pageY, e.currentTarget); + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js index bcd26c0f6fa5ed3f641f455747094a401641a5fd..558e6629a166b5e47676af7513415add275e54c6 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js @@ -5,8 +5,9 @@ define([ 'underscore', 'mageUtils', + 'Magento_Ui/js/core/renderer/layout', 'Magento_Ui/js/lib/collapsible' -], function (_, utils, Collapsible) { +], function (_, utils, layout, Collapsible) { 'use strict'; function extractPreview(elem) { @@ -39,15 +40,15 @@ define([ filters: { placeholder: true }, - listens: { - active: 'extractPreviews', - applied: 'cancel extractActive' - }, links: { applied: '${ $.storageConfig.path }' }, exports: { applied: '${ $.provider }:params.filters' + }, + listens: { + active: 'updatePreviews', + applied: 'cancel extractActive' } }, @@ -57,6 +58,8 @@ define([ * @returns {Filters} Chainable. */ initialize: function () { + this._processedColumns = {}; + this._super() .cancel() .extractActive(); @@ -182,12 +185,12 @@ define([ }, /** - * Extract previews of a specified filters. + * Updates previews of a specified filters. * * @param {Array} filters - Filters to be processed. * @returns {Filters} Chainable. */ - extractPreviews: function (filters) { + updatePreviews: function (filters) { var previews = filters.map(extractPreview); this.previews(_.compact(previews)); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/listing.js b/app/code/Magento/Ui/view/base/web/js/grid/listing.js index fd7d70f9f7fd13f62fb111a70eacc1957655017b..299560ba2bfe5eaa6a4e06319e7388135e6ee941 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/listing.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/listing.js @@ -3,23 +3,54 @@ * See COPYING.txt for license details. */ define([ + 'underscore', 'uiComponent', - 'Magento_Ui/js/lib/spinner' -], function (Component, loader) { + 'Magento_Ui/js/lib/spinner', + 'Magento_Ui/js/core/renderer/layout' +], function (_, Component, loader, layout) { 'use strict'; return Component.extend({ defaults: { template: 'ui/grid/listing', + positions: false, + storageConfig: { + positions: '${ $.storageConfig.path }.positions' + }, + dndConfig: { + name: '${ $.name }_dnd', + component: 'Magento_Ui/js/grid/dnd', + containerTmpl: 'ui/grid/dnd/listing', + enabled: true + }, imports: { rows: '${ $.provider }:data.items' }, listens: { + elems: 'setPositions', '${ $.provider }:reload': 'showLoader', '${ $.provider }:reloaded': 'hideLoader' + }, + modules: { + dnd: '${ $.dndConfig.name }' } }, + /** + * Initializes Listing component. + * + * @returns {Listing} Chainable. + */ + initialize: function () { + this._super(); + + if (this.dndConfig.enabled) { + this.initDnd(); + } + + return this; + }, + /** * Initializes observable properties. * @@ -32,6 +63,90 @@ define([ return this; }, + /** + * Creates drag&drop widget instance. + * + * @returns {Listing} Chainable. + */ + initDnd: function () { + layout([this.dndConfig]); + + return this; + }, + + /** + * Called when another element was added to current component. + * + * @returns {Listing} Chainable. + */ + initElement: function () { + var currentCount = this.elems().length, + totalCount = this.initChildCount; + + if (totalCount === currentCount) { + this.initPositions(); + } + + return this._super(); + }, + + /** + * Defines initial order of child elements. + * + * @returns {Listing} Chainable. + */ + initPositions: function () { + var link = { + positions: this.storageConfig.positions + }; + + this.on('positions', this.applyPositions.bind(this)); + + this.setLinks(link, 'imports') + .setLinks(link, 'exports'); + + return this; + }, + + /** + * Updates current state of child positions. + * + * @returns {Listing} Chainable. + */ + setPositions: function () { + var positions = {}; + + this.elems.each(function (elem, index) { + positions[elem.index] = index; + }); + + this.set('positions', positions); + + return this; + }, + + /** + * Reseorts child elements array according to provided positions. + * + * @param {Object} positions - Object where key represents child + * index and value is its' position. + * @returns {Listing} Chainable. + */ + applyPositions: function (positions) { + var sorting; + + sorting = this.elems.map(function (elem) { + return { + elem: elem, + position: positions[elem.index] + }; + }); + + this.insertChild(sorting); + + return this; + }, + /** * Hides loader. */ diff --git a/app/code/Magento/Ui/view/base/web/js/grid/massactions.js b/app/code/Magento/Ui/view/base/web/js/grid/massactions.js index 35c792feaba23e40c19c68f2609171c798983297..21e86a6d640b2f43aab310774f09ce31d69583de 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/massactions.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/massactions.js @@ -4,37 +4,178 @@ */ define([ 'underscore', + 'uiRegistry', 'mageUtils', - 'Magento_Ui/js/lib/collapsible' -], function (_, utils, Collapsible) { + 'Magento_Ui/js/lib/collapsible', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/modal/alert', + 'mage/translate' +], function (_, registry, utils, Collapsible, confirm, alert, $t) { 'use strict'; return Collapsible.extend({ defaults: { template: 'ui/grid/actions', - noItems: 'You haven\'t selected any items!' + selectProvider: '', + actions: [], + noItemsMsg: $t('You haven\'t selected any items!'), + modules: { + selections: '${ $.selectProvider }' + } }, - applyAction: function (action) { - var proceed = true, - selections = this.source.get('config.multiselect'); + /** + * Initializes observable properties. + * + * @returns {Massactions} Chainable. + */ + initObservable: function () { + this._super() + .observe('actions'); + + return this; + }, - if (!selections || !selections.total) { - proceed = false; + /** + * Applies specified action. + * + * @param {String} actionIndex - Actions' identifier. + * @returns {Massactions} Chainable. + */ + applyAction: function (actionIndex) { + var data = this.getSelections(), + action, + callback; - alert(this.noItems); - } + if (!data.total) { + alert({ + content: this.noItemsMsg + }); - if (proceed && action.confirm) { - proceed = window.confirm(action.confirm); + return this; } - if (proceed) { - utils.submit({ - url: action.url, - data: selections + action = this.getAction(actionIndex), + callback = this._getCallback(action, data); + + action.confirm ? + this._confirm(action, callback) : + callback(); + + return this; + }, + + /** + * Retrieves selections data from the selections provider. + * + * @returns {Object|Undefined} + */ + getSelections: function () { + var provider = this.selections(), + selections = provider && provider.getSelections(); + + return selections; + }, + + /** + * Retrieves action object associated with a specified index. + * + * @param {String} actionIndex - Actions' identifier. + * @returns {Object} Action object. + */ + getAction: function (actionIndex) { + return _.findWhere(this.actions(), { + type: actionIndex + }); + }, + + /** + * Adds new action. If action with a specfied identifier + * already exists, than the original one will be overrided. + * + * @param {Object} action - Action object. + * @returns {Massactions} Chainable. + */ + addAction: function (action) { + var actions = this.actions(), + index = _.findIdnex(actions, { + type: action.type }); + + ~index ? + actions[index] = action : + actions.push(action); + + this.actions(actions); + + return this; + }, + + /** + * Creates action callback based on its' data. If action doesn't spicify + * a callback function than the default one will be used. + * + * @private + * @param {Object} action - Actions' object. + * @param {Object} selections - Selections data. + * @returns {Function} Callback function. + */ + _getCallback: function (action, selections) { + var callback = action.callback, + args = [action, selections]; + + if (utils.isObject(callback)) { + args.unshift(callback.target); + + callback = registry.async(callback.provider); + } else if (typeof callback != 'function') { + callback = this.defaultCallback.bind(this); } + + return function () { + callback.apply(null, args); + }; + }, + + /** + * Default action callback. Sends selections data + * via POST request. + * + * @param {Object} data - Selections data. + * @param {Object} action - Action data. + */ + defaultCallback: function (data, action) { + var selections = {}; + + if (data.excludeMode) { + selections.excluded = data.excluded; + } else { + selections.selected = data.selected; + } + + utils.submit({ + url: action.url, + data: selections + }); + }, + + /** + * Shows actions' confirmation window. + * + * @param {Object} action - Actions' data. + * @param {Function} callback - Callback that will be + * invoked if action is confirmed. + */ + _confirm: function (action, callback) { + var confirmData = action.confirm; + + confirm({ + title: confirmData.title, + content: confirmData.message, + actions: { + confirm: callback + } + }); } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/paging.js b/app/code/Magento/Ui/view/base/web/js/grid/paging.js index e04597379afa39323ca1a5700b7d5a14e06017c9..0fd512c3ffc48a3e3b840dbb2d8ef29d2ef0d829 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/paging.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/paging.js @@ -23,9 +23,10 @@ define([ template: 'ui/grid/paging', pageSize: 20, current: 1, + selectProvider: '', imports: { - totalSelected: '${ $.provider }:config.multiselect.total', + totalSelected: '${ $.selectProvider }:totalSelected', totalRecords: '${ $.provider }:data.totalRecords' }, diff --git a/app/code/Magento/Ui/view/base/web/js/lib/component/links.js b/app/code/Magento/Ui/view/base/web/js/lib/component/links.js index 94df6c5d22c3cecb1d53de18eca10c00c01e89b5..deecc3fa9a14acdaf5f59b073a7d0b067c90c7be 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/component/links.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/component/links.js @@ -81,22 +81,19 @@ define([ } function setLinked(map, data) { - var hasLink, - match; - + var match; + if (!map) { return; } - hasLink = map.some(function (item) { - match = item; - - return !item.linked && - item.target === data.target && - item.property === data.property; + match = _.findWhere(map, { + linked: false, + target: data.target, + property: data.property }); - if (hasLink) { + if (match) { match.linked = data; data.linked = match; } @@ -106,6 +103,8 @@ define([ var direction = data.direction, map = maps[direction]; + data.linked = false; + (map[property] = map[property] || []).push(data); direction = direction === 'imports' ? 'exports' : 'imports'; @@ -165,7 +164,7 @@ define([ }); }); }); - + return this; }, diff --git a/app/code/Magento/Ui/view/base/web/js/lib/component/manip.js b/app/code/Magento/Ui/view/base/web/js/lib/component/manip.js index cc82f645030e071a202ae68fca9ccb282ac27de6..a91b3cd38b611151d50068a2c41fc94a818e4085 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/component/manip.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/component/manip.js @@ -10,45 +10,17 @@ define([ ], function (ko, _, utils, registry) { 'use strict'; - function getIndex(container, target) { - var result; - - container.some(function (item, index) { - result = index; - - return item && (item.name === target || item === target); - }); - - return result; - } - function compact(container) { return container.filter(utils.isObject); } - function reserve(container, elem, position) { - var offset = position, - target; - - if (_.isObject(position)) { - target = position.after || position.before; - offset = getIndex(container, target); - - if (position.after) { - ++offset; - } - } - - offset = utils.formatOffset(container, offset); - - container[offset] ? - container.splice(offset, 0, elem) : - container[offset] = elem; - - return offset; - } - return { + /** + * Retrieves requested region. + * Creates region if it was not created yet + * + * @returns {ObservableArray}. + */ getRegion: function (name) { var regions = this.regions = this.regions || {}; @@ -59,23 +31,65 @@ define([ return regions[name]; }, + /** + * Replaces specified regions' data with a provided one. + * Creates region if it was not created yet. + * + * @param {Array} items - New regions' data. + * @param {String} name - Name of the region. + * @returns {Component} Chainable. + */ updateRegion: function (items, name) { var region = this.getRegion(name); region(items); + + return this; }, /** * Requests specified components to insert * them into 'elems' array starting from provided position. * - * @param {String} elem - Name of the component to insert. + * @param {String} elems - Name of the component to insert. * @param {Number} [position=-1] - Position at which to insert elements. * @returns {Component} Chainable. */ - insertChild: function (elem, position) { - reserve(this._elems, elem, position); - registry.get(elem, this._insert); + insertChild: function (elems, position) { + var container = this._elems, + update = false, + newItems = [], + newItem; + + if (Array.isArray(elems)) { + newItems = elems.map(function (item) { + newItem = item.elem ? + utils.insert(item.elem, container, item.position) : + utils.insert(item, container, position); + + return newItem; + }); + } else { + newItems.push(utils.insert(elems, container, position)); + } + + newItems.forEach(function (item) { + if (!item) { + return; + } + + if (item === true) { + update = true; + } else { + _.isString(item) ? + registry.get(item, this._insert) : + this._insert(item); + } + }, this); + + if (update) { + this._update(); + } return this; }, diff --git a/app/code/Magento/Ui/view/base/web/js/lib/ko/bind/after-render.js b/app/code/Magento/Ui/view/base/web/js/lib/ko/bind/after-render.js new file mode 100644 index 0000000000000000000000000000000000000000..2297f2d5cf1764d0198b791ec6011f3b2debf2b8 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/lib/ko/bind/after-render.js @@ -0,0 +1,22 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'ko' +], function (ko) { + 'use strict'; + + ko.bindingHandlers.afterRender = { + /** + * Binding init callback. + */ + init: function (element, valueAccessor, allBindings, viewModel) { + var callback = valueAccessor(); + + if (typeof callback === 'function') { + callback(element, viewModel); + } + } + }; +}); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/ko/initialize.js b/app/code/Magento/Ui/view/base/web/js/lib/ko/initialize.js index a8bb4a15077772cc66f78d7f85a00f1e94383161..2758b3f8c2391b60ff9fefa818279fc9af09a295 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/ko/initialize.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/ko/initialize.js @@ -14,11 +14,11 @@ define([ './bind/optgroup', './bind/fadeVisible', './bind/mage-init', + './bind/after-render', './extender/observable_array' -], function(ko, templateEngine) { +], function (ko, templateEngine) { 'use strict'; ko.setTemplateEngine(templateEngine); ko.applyBindings(); - }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js b/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js index adf6b9176b4004d6d55c52cffe1fc2398cb13207..4bf6b21f686f8365c90515b2919c108b5fda2d8d 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js @@ -7,23 +7,23 @@ define([ 'underscore', './storage', './events' -], function(utils, _, Storage, Events) { +], function (utils, _, Storage, Events) { 'use strict'; - function async(name, registry, method) { + function async(name, registry, method) { var args = _.toArray(arguments).slice(3); if (_.isString(method)) { registry.get(name, function (component) { - component[method].apply(component, args); + component[method].apply(component, args); }); } else if (_.isFunction(method)) { - registry.get(name, method); + registry.get(name, method); } else if (!args.length) { return registry.get(name); } - } - + } + function Registry() { this.storage = new Storage(); this.events = new Events(this.storage); @@ -35,9 +35,9 @@ define([ /** * Retrieves data from registry. * - * @params {(String|Array)} elems - + * @param {(String|Array)} elems - * An array of elements' names or a string of names divided by spaces. - * @params {Function} [callback] - + * @param {Function} [callback] - * Callback function that will be triggered * when all of the elements are registered. * @returns {Array|*|Undefined} @@ -45,7 +45,7 @@ define([ * or an element itself if only is requested. * If callback function is specified then returns 'undefined'. */ - get: function(elems, callback) { + get: function (elems, callback) { var records; elems = utils.stringToArray(elems) || []; @@ -64,8 +64,8 @@ define([ /** * Sets data to registry. * - * @params {String} elems - Elements' name. - * @params {*} value - Value that will be assigned to the element. + * @param {String} elem - Elements' name. + * @param {*} value - Value that will be assigned to the element. * @returns {registry} Chainable. */ set: function (elem, value) { @@ -77,7 +77,7 @@ define([ /** * Removes specified elements from a storage. - * @params {(String|Array)} elems - + * @param {(String|Array)} elems - * An array of elements' names or a string of names divided by spaces. * @returns {registry} Chainable. */ @@ -92,7 +92,7 @@ define([ /** * Checks whether specified elements has been registered. * - * @params {(String|Array)} elems - + * @param {(String|Array)} elems - * An array of elements' names or a string of names divided by spaces. * @returns {Boolean} */ 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 new file mode 100644 index 0000000000000000000000000000000000000000..33be3c8041dea6c2f0586124bc3f1e8120020cef --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/modal/alert.js @@ -0,0 +1,40 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'jquery/ui', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function ($, _) { + 'use strict'; + + $.widget('mage.alert', $.mage.confirm, { + options: { + modalClass: 'confirm', + title: $.mage.__('Attention'), + actions: { + always: function () {} + }, + buttons: [{ + text: $.mage.__('OK'), + class: 'action-secondary', + click: function () { + this.closeModal(true); + } + }] + }, + closeModal: function () { + this.options.actions.always(); + this.element.bind('confirmclosed', _.bind(this._remove, this)); + + return this._super(); + } + }); + + return function (config) { + return $('<div></div>').html(config.content).alert(config); + }; +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..7bad2023b649b18edeaf710780d8cf0e73a5f6b9 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/modal/confirm.js @@ -0,0 +1,66 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'jquery/ui', + 'Magento_Ui/js/modal/modal', + 'mage/translate' +], function ($, _) { + 'use strict'; + + $.widget('mage.confirm', $.mage.modal, { + options: { + modalClass: 'confirm', + title: '', + actions: { + always: function(){}, + confirm: function(){}, + cancel: function(){} + }, + buttons: [{ + text: $.mage.__('Cancel'), + class: 'action-tertiary', + click: function(){ + this.closeModal(); + } + }, { + text: $.mage.__('OK'), + class: 'action-secondary', + click: function() { + this.closeModal(true); + } + }] + }, + _create: function() { + this._super(); + this.modal.find(this.options.modalCloseBtn).off().on('click', _.bind(this.closeModal, this, false)); + this.openModal(); + }, + _remove: function() { + this.modal.remove(); + }, + openModal: function() { + return this._super(); + }, + closeModal: function(result) { + result = result || false; + + if (result) { + this.options.actions.confirm(); + } else { + this.options.actions.cancel(); + } + this.options.actions.always(); + this.element.bind('confirmclosed', _.bind(this._remove, this)); + + return this._super(); + } + }); + + return function (config) { + return $('<div></div>').html(config.content).confirm(config); + }; +}); diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modalToggle.js b/app/code/Magento/Ui/view/base/web/js/modal/modalToggle.js new file mode 100644 index 0000000000000000000000000000000000000000..65714ef029cf6ba25199057fc8b64ab03515b01f --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/modal/modalToggle.js @@ -0,0 +1,28 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/modal/modal' +], function($){ + 'use strict'; + + return function(config, el) { + var widget = $(config.content).modal(config); + + $(el).on(config.toggleEvent, function() { + var state = widget.data('mage-modal').options.isOpen; + + if (state) { + widget.modal('closeModal'); + } else { + widget.modal('openModal'); + } + + return false; + }); + + return widget; + }; +}); \ No newline at end of file diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/actions.html b/app/code/Magento/Ui/view/base/web/templates/grid/actions.html index 685936aa8430382b59ff1b065c74e244aadcd24a..89bf5ba4061d17358994f8a2c3cf9d8158d90d51 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/actions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/actions.html @@ -18,8 +18,8 @@ <ul class="action-menu" data-bind="css: {'_active': opened}, - foreach: {data: actions, as: 'action'}"> - <li data-bind="click: $parent.applyAction.bind($parent, action)"> + foreach: actions"> + <li data-bind="click: $parent.applyAction.bind($parent, type)"> <span class="action-menu-item" data-bind="text: label"></span> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html index 67f195377c669e5ba824a8be13603c69fa2aa0bf..85fba7ed0c6c2d5c725486bd30bf69b65c4ea213 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html @@ -6,25 +6,47 @@ --> <td data-bind="visible: visible" class="data-grid-actions-cell"> - <!-- ko if: getDisplayed(row[field.index]).length > 1 --> - <div class="action-select-wrap _active"> - <button class="action-select"> + <!-- ko if: isSingle($parentContext.$index()) --> + <!-- ko foreach: getVisibleActions($parentContext.$index()) --> + <a + class="action-menu-item" + data-bind=" + attr: { + href: $data.href + }, + click: $parent.applyAction.bind($parent, index, rowIndex), + text: $data.label"></a> + <!-- /ko --> + <!-- /ko --> + + <!-- ko if: isMultiple($parentContext.$index()) --> + <div + class="action-select-wrap" + data-bind=" + css : { + '_active' : opened() === $parentContext.$index() + }, + outerClick: closeList.bind($data, $parentContext.$index())"> + <button class="action-select" data-bind="click: toggleList.bind($data, $parentContext.$index())"> <span data-bind="text: $t('Select')"></span> </button> - <ul class="action-menu _active"> + <ul + class="action-menu" + data-bind=" + css: {'_active': opened() === $parentContext.$index()}"> + <!-- ko foreach: getVisibleActions($parentContext.$index()) --> <li> <a class="action-menu-item" - data-bind="attr: {href: displayed[0].href}, - text: displayed[0].label"></a> + data-bind=" + attr: { + href: $data.href + }, + click: $parent.applyAction.bind($parent, index, rowIndex), + text: $data.label"></a> </li> + <!-- /ko --> </ul> </div> <!-- /ko --> - <!-- ko ifnot: getDisplayed(row[field.index]).length > 1 --> - <a - class="action-menu-item" - data-bind="attr: {href: displayed[0].href}, - text: displayed[0].label"></a> - <!-- /ko --> </td> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/html.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/html.html index 27d6bfcf6216b5a5d6ace4d652e681814b99bcaf..166a4f1f9b7e9a7d25b6a973f95e462019e9234e 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/html.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/html.html @@ -5,7 +5,9 @@ */ --> <td - data-bind="visible: visible, - click: isClickable(row) ? redirect.bind($data, getClickUrl(row)) : false, - html: getLabel(row[field.index])" + data-bind=" + visible: visible, + css: { _dragging: dragging }, + click: isClickable(row) ? redirect.bind($data, getClickUrl(row)) : false, + html: getLabel(row[field.index])" data-action="grid-row-edit"></td> \ No newline at end of file diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html index 81e6b2c5bc3706a042daf1f6e2b3725e84b681c4..c11acd1f36ecea301095cea6258c9b1a65c781e7 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html @@ -10,9 +10,12 @@ <input class="admin__control-checkbox" type="checkbox" - data-bind="checked: selected, - value: row[indexField], - attr: {id: 'check' + row[indexField]}"> + data-bind=" + checked: selected, + value: row[indexField], + attr: { + id: 'check' + row[indexField] + }"> <label data-bind="attr: {for: 'check' + row[indexField]}"></label> </label> </td> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/text.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/text.html index 257b4b0585d45aec84541ae338780047e16e3852..09ecafbdfbab9bf193a52b0f28063a79693ba68d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/text.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/text.html @@ -4,8 +4,10 @@ * See COPYING.txt for license details. */ --> -<td - data-bind="visible: visible, - click: isClickable(row) ? redirect.bind($data, getClickUrl(row)) : false, - text: getLabel(row[field.index])" +<td + data-bind=" + visible: visible, + css: { _dragging: dragging }, + click: isClickable(row) ? redirect.bind($data, getClickUrl(row)) : false, + text: getLabel(row[field.index])" data-action="grid-row-edit"></td> \ No newline at end of file diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/actions.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/actions.html deleted file mode 100644 index 5d3aaa53d9bafc964ba9515d5fa6cb900de9c594..0000000000000000000000000000000000000000 --- a/app/code/Magento/Ui/view/base/web/templates/grid/columns/actions.html +++ /dev/null @@ -1,10 +0,0 @@ -<!-- -/** - * Copyright © 2015 Magento. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<th class="data-grid-th data-grid-actions-cell" data-bind="visible: visible"> - <span data-bind="text: label"></span> -</th> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/text.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/text.html index dc7b883659adb7cc690246acbc840cc12520ef6d..41207503828c1221929d6b0eb96914dcd45b5b00 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/columns/text.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/columns/text.html @@ -4,17 +4,21 @@ * See COPYING.txt for license details. */ --> - -<!-- ko if: sortable --> - <th - class="data-grid-th _sortable" - data-bind="css: sortClass, click: sort, visible: visible"> - <span data-bind="text: label"></span> - </th> -<!-- /ko --> - -<!-- ko ifnot: sortable --> - <th class="data-grid-th" data-bind="visible: visible"> - <span data-bind="text: label"></span> - </th> -<!-- /ko --> +<th + class="data-grid-th" + data-bind=" + afterRender: draggable && $parent.dndConfig.enabled ? + (function (elem) { $parent.dnd('addColumn', elem); }) : + false, + css: { + '_sortable': sortable, + '_draggable': draggable, + '_ascend': sorting() === 'asc', + '_descend': sorting() === 'desc', + '_dragover-left': dragover() === 'right', + '_dragover-right': dragover() === 'left' + }, + click: sort, + visible: visible"> + <span data-bind="text: label"></span> +</th> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/dnd/listing.html b/app/code/Magento/Ui/view/base/web/templates/grid/dnd/listing.html new file mode 100644 index 0000000000000000000000000000000000000000..38b622cdef6cfe21bfe25e9176b2ff90c8fe1a8b --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/grid/dnd/listing.html @@ -0,0 +1,42 @@ + +<!-- +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<table + class="data-grid _dragging-copy _hidden" + data-bind=" + afterRender: function (elem) { dnd('setDragTable', elem); } + "> + <thead data-part="head"> + <tr data-part="head.row"> + <!-- ko foreach: elems --> + <!-- ko if: dragging --> + <!-- ko template: getHeader() --><!-- /ko --> + <!-- /ko --> + <!-- /ko --> + </tr> + </thead> + <tbody data-part="body"> + <!-- ko if: hasData() --> + <!-- ko foreach: { data: rows, as: 'row' } --> + <tr data-part="body.row"> + <!-- ko foreach: { data: $parent.elems, as: 'field' } --> + <!-- ko if: dragging --> + <!-- ko template: getBody() --><!-- /ko --> + <!-- /ko --> + <!-- /ko --> + </tr> + <!-- /ko --> + <!-- /ko --> + + <!-- ko ifnot: hasData() --> + <tr class="data-grid-tr-no-data"> + <td data-bind="attr: { colspan: getColspan() }, + text: $t('We couldn\'t find any records.')"></td> + </tr> + <!-- /ko --> + </tbody> +</table> \ No newline at end of file diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/listing.html b/app/code/Magento/Ui/view/base/web/templates/grid/listing.html index c3de27d5c7e52f97647170b06fc6a3b1e8b67162..118136c29fd86c65a84adf1fc1b4cd676c67f2fc 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/listing.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/listing.html @@ -4,15 +4,20 @@ * See COPYING.txt for license details. */ --> - <div class="admin__data-grid-wrap"> - <table class="data-grid"> + <table + class="data-grid" + data-bind=" + afterRender: dndConfig.enabled ? + function (elem) { dnd('setTable', elem); } : + false, + css: { + 'data-grid-draggable': dndConfig.enabled + }"> <thead data-part="head"> <tr data-part="head.row"> <!-- ko foreach: elems --> - <!-- ko if: visible --> - <!-- ko template: getHeader() --><!-- /ko --> - <!-- /ko --> + <!-- ko template: getHeader() --><!-- /ko --> <!-- /ko --> </tr> </thead> @@ -21,9 +26,7 @@ <!-- ko foreach: { data: rows, as: 'row' } --> <tr data-part="body.row"> <!-- ko foreach: { data: $parent.elems, as: 'field' } --> - <!-- ko if: visible --> - <!-- ko template: getBody() --><!-- /ko --> - <!-- /ko --> + <!-- ko template: getBody() --><!-- /ko --> <!-- /ko --> </tr> <!-- /ko --> @@ -37,4 +40,7 @@ <!-- /ko --> </tbody> </table> + <!-- ko if: dndConfig.enabled --> + <!-- ko template: dndConfig.containerTmpl --><!-- /ko --> + <!-- /ko --> </div> \ No newline at end of file diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less index 27524d7d4d0854693596871cd21c5d8f05dd84b8..ff4c338c62faa30c58cd734cf4e1f0025fe74444 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less @@ -95,6 +95,9 @@ display: block; font-size: @font-size__s; padding: @search-global-input__padding-top @search-global-input__padding-side @search-global-input__padding-bottom; + &._active { + background-color: @color-blue-clear-sky; + } } .title { display: block; diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less index d3ef739a9435a77614ee1bcf3a302cfd274cc7ef..a243bf34f061e922823d54c49ac504676092250b 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less @@ -48,6 +48,10 @@ @data-grid-row-parent-marker__size: 1rem; +@data-grid-td__dragging__opacity: 95%; +@data-grid-dragging-copy__border-color: @color-blue-pure; +@data-grid-dragging-copy__border: 1px solid @data-grid-dragging-copy__border-color; + // .admin__data-grid-outer-wrap { @@ -106,6 +110,9 @@ &:nth-child(even) { td { background-color: @data-grid-td__even__background-color; + &._dragging { + background-color: fade(@data-grid-td__even__background-color, @data-grid-td__dragging__opacity); + } } } &.data-grid-tr-no-data { @@ -154,7 +161,21 @@ &:last-child { border-right-style: @data-grid-td__border-outer-style; } + &._dragging { + color: fade(@table__color, @data-grid-td__dragging__opacity); + background-color: fade(@data-grid-td__odd__background-color, @data-grid-td__dragging__opacity); + a { + color: fade(@link__color, @data-grid-td__dragging__opacity); + &:hover { + color: fade(@link__hover__color, @data-grid-td__dragging__opacity); + } + } + } + // Action select data grid styles (can be action-select-secondary in future) + .action-select-wrap { + position: static; + } .action-select { .link-pattern(); background-color: transparent; @@ -168,6 +189,9 @@ } &:after { border-color: @link__color transparent transparent transparent; + margin: .6rem 0 0 .7rem; + right: auto; + top: auto; } &:before { display: none; @@ -175,6 +199,8 @@ } .action-menu { left: auto; + right: auto; + top: auto; z-index: 1; } } @@ -189,11 +215,22 @@ &:first-child { border-left-color: @data-grid-th__border-color; } + &._dragover-left { + box-shadow: inset 3px 0 0px 0px @color-white; + } + &._dragover-right { + box-shadow: inset -3px 0 0px 0px @color-white; + } } .data-grid-th { color: @data-grid-th__color; padding: @data-grid-th__padding-vertical @data-grid-th__padding-horizontal; vertical-align: middle; + &._draggable { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } &._sortable { background-clip: padding-box; // Fix for border overlay in Firefox cursor: pointer; @@ -281,6 +318,45 @@ } } } + // Draggable columns + &._hidden { + display: none; + } + &.data-grid-draggable { + background-color: @color-black; + } + &._dragging-copy { + background-color: @color-white; + box-shadow: @component__box-shadow__base; + left: 0; + opacity: .95; + position: fixed; + top: 0; + z-index: @overlay__z-index; + .data-grid-th { + border: @data-grid-dragging-copy__border; + border-bottom: none; + } + .data-grid-th, + .data-grid-th._sortable { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + tbody { + tr { + &:last-child { + td { + border-bottom: @data-grid-dragging-copy__border; + } + } + } + td { + border-left: @data-grid-dragging-copy__border; + border-right: @data-grid-dragging-copy__border; + } + } + } } // Ascend & Descend sort marker @@ -349,4 +425,4 @@ &._col-xs { width: 1%; } -} +} \ No newline at end of file diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_classes.less b/app/design/adminhtml/Magento/backend/web/css/source/_classes.less index 7562f5de527b2a29ee016193b59c95d76b0912d2..ef23f9abd3aba56beb7a5f80218731c402d0081e 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_classes.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_classes.less @@ -19,3 +19,11 @@ .a-center { // ToDo UI: should be renamed to ._text-center text-align: center; } + +// No select +._no-select { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Grid.php b/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Grid.php index 4188aef15cd0565a483f3a08e31fdaf7156cd429..53402d17d2a4654409dd5a2b7d6937ad7c58422f 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Grid.php +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Grid.php @@ -6,8 +6,8 @@ namespace Magento\Cms\Test\Block\Adminhtml\Page; -use Magento\Ui\Test\Block\Adminhtml\DataGrid; use Magento\Mtf\Client\Locator; +use Magento\Ui\Test\Block\Adminhtml\DataGrid; /** * Backend Data Grid for managing "CMS Page" entities. @@ -66,8 +66,8 @@ class Grid extends DataGrid * * @var string */ - protected $previewCmsPage = '.action-menu-item'; - + protected $previewCmsPage = ".//a[contains(@class, 'action-menu-item') and text() = '%s']"; + /** * Search item and open it on Frontend. * @@ -77,10 +77,12 @@ class Grid extends DataGrid */ public function searchAndPreview(array $filter) { + $itemName = 'Preview'; $this->search($filter); $rowItem = $this->_rootElement->find($this->rowItem); if ($rowItem->isVisible()) { - $rowItem->find($this->previewCmsPage)->click(); + $rowItem->find('.action-select')->click(); + $rowItem->find(sprintf($this->previewCmsPage, $itemName), Locator::SELECTOR_XPATH)->click(); } else { throw new \Exception('Searched item was not found.'); } diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts.php b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts.php index d888fa31e8ef27194c3dfd21d829fb82223b2e1c..618e06a6391659476bd7fcf648193767afae8302 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts.php +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts.php @@ -29,7 +29,7 @@ class AssociatedProducts extends Tab * * @var string */ - protected $productSearchGrid = "./ancestor::body//div[div[contains(@data-role,'add-product-dialog')]]"; + protected $productSearchGrid = './/*[@data-role="modal"][.//*[@data-role="add-product-dialog"]]'; /** * Associated products list block @@ -54,7 +54,7 @@ class AssociatedProducts extends Tab { return $this->blockFactory->create( 'Magento\GroupedProduct\Test\Block\Adminhtml\Product\Grouped\AssociatedProducts\Search\Grid', - ['element' => $this->_rootElement->find($this->productSearchGrid, Locator::SELECTOR_XPATH)] + ['element' => $this->browser->find($this->productSearchGrid, Locator::SELECTOR_XPATH)] ); } diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/actions.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/actions.test.js new file mode 100644 index 0000000000000000000000000000000000000000..331dbef78d040a791fd30f9f58efa2e5153164b8 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/actions.test.js @@ -0,0 +1,100 @@ +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'Magento_Ui/js/grid/columns/actions' +], function (_, Actions) { + 'use strict'; + + describe('ui/js/grid/columns/actions', function () { + var model, + action; + + beforeEach(function () { + model = new Actions({ + index: 'actions', + name: 'listing_action', + indexField: 'id', + dataScope: '', + rows: [{ + identifier: 'row' + }] + }); + action = { + index: 'delete', + hidden: true, + rowIndex: 0, + callback: function() { + return true; + } + }; + }); + + it('Check addAction function', function () { + expect(model.addAction('delete', action)).toBe(model); + }); + + it('Check getAction function', function () { + var someAction = _.clone(action); + + someAction.index = 'edit'; + model.addAction('edit', someAction); + expect(model.getAction(0, 'edit')).toEqual(someAction); + }); + + it('Check getVisibleActions function', function () { + var someAction = _.clone(action); + + someAction.hidden = false; + someAction.index= 'view'; + model.addAction('delete', action); + model.addAction('view', someAction); + expect(model.getVisibleActions('0')).toEqual([someAction]); + }); + + it('Check updateActions function', function () { + expect(model.updateActions()).toEqual(model); + }); + + it('Check applyAction function', function () { + model.addAction('delete', action); + expect(model.applyAction('delete', 0)).toEqual(model); + }); + + it('Check isSingle and isMultiple function', function () { + var someAction = _.clone(action); + + action.hidden = false; + model.addAction('delete', action); + expect(model.isSingle(0)).toBeTruthy(); + someAction.hidden = false; + someAction.index = 'edit'; + model.addAction('edit', someAction); + expect(model.isSingle(0)).toBeFalsy(); + expect(model.isMultiple(0)).toBeTruthy(); + }); + + it('Check isActionVisible function', function () { + expect(model.isActionVisible(action)).toBeFalsy(); + action.hidden = false; + expect(model.isActionVisible(action)).toBeTruthy(); + }); + + it('Check toggleList function', function () { + model.toggleList(0); + expect(model.opened()).toEqual(0); + model.toggleList(0); + expect(model.opened()).toBeFalsy(); + }); + + it('Check closeList function', function () { + model.toggleList(0); + expect(model.opened()).toEqual(0); + model.closeList(0); + expect(model.opened()).toBeFalsy(); + }); + }); +}); \ No newline at end of file diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dialog/dialog.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/modal/modal.test.js similarity index 60% rename from dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dialog/dialog.test.js rename to dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/modal/modal.test.js index be1e6dfa867088f6174526b3d41c6fade7276b8a..b5e2837467bebad4e5e15bea402121a1be7e96ae 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dialog/dialog.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/modal/modal.test.js @@ -5,23 +5,23 @@ define([ 'jquery', - 'Magento_Ui/js/dialog/dialog' + 'Magento_Ui/js/modal/modal' ], function ($) { 'use strict'; - describe('ui/js/dialog/dialog', function () { + describe('ui/js/modal/modal', function () { var element = $('<div>some element</div>'), - dialog = element.dialog({}).data('mage-dialog'); + modal = element.modal({}).data('mage-modal'); - it('Check for dialog definition', function () { - expect(dialog).toBeDefined(); + it('Check for modal definition', function () { + expect(modal).toBeDefined(); }); it('Show/hide function check', function () { expect(element.trigger('openDialog')).toBe(element); expect(element.trigger('closeDialog')).toBe(element); }); it('Check for transition support', function () { - expect(dialog.whichTransitionEvent()).toBe('webkitTransitionEnd'); + expect(modal.whichTransitionEvent()).toBe('webkitTransitionEnd'); }); }); }); \ No newline at end of file diff --git a/lib/web/css/source/components/_modals.less b/lib/web/css/source/components/_modals.less index c7776d6684e7e5880247625d2032f0cd4b33fc02..df5c973eef2de55b58b941178d1acf677ee0a219 100644 --- a/lib/web/css/source/components/_modals.less +++ b/lib/web/css/source/components/_modals.less @@ -28,6 +28,8 @@ @modal-slide-header__padding-vertical: 2.1rem; +@modal-popup-confirm__width: 50rem; + // // Utilities // --------------------------------------------- @@ -181,6 +183,16 @@ padding-top: @modal-popup__padding; padding-bottom: @modal-popup__padding; } + &.confirm { + .modal-inner-wrap { + margin-left: -(@modal-popup-confirm__width/2); + left: 50%; + width: @modal-popup-confirm__width; + } + .modal-footer { + text-align: right; + } + } } // diff --git a/lib/web/css/source/lib/_navigation.less b/lib/web/css/source/lib/_navigation.less index 6bc9cb68b243392a50bf5026f8bc510146a1f71a..a0af0e4ebd85069255dd6e8c5d13c82206552855 100644 --- a/lib/web/css/source/lib/_navigation.less +++ b/lib/web/css/source/lib/_navigation.less @@ -329,7 +329,6 @@ margin: 0 !important; position: absolute; left: 0; - top: 100%; z-index: 1; .css(background, @_submenu-background-color); .css(border, @_submenu-border-width @_submenu-border-style @_submenu-border-color); @@ -370,6 +369,10 @@ top: 0 !important; left: 100% !important; } + .submenu-reverse{ + left: auto !important; + right: 100%; + } } &.more { position: relative; diff --git a/lib/web/mage/backend/suggest.js b/lib/web/mage/backend/suggest.js index dfb3ef72d8c4d8424c949ea44dfba20b3a8f01be..f4efd12693997985cdc08232778491db37a74848 100644 --- a/lib/web/mage/backend/suggest.js +++ b/lib/web/mage/backend/suggest.js @@ -178,16 +178,47 @@ _bind: function () { this._on($.extend({ keydown: function (event) { - var keyCode = $.ui.keyCode; + var keyCode = $.ui.keyCode, + suggestList, + hasSuggestedItems, + hasSelectedItems, + selectedItem; + switch (event.keyCode) { case keyCode.PAGE_UP: - case keyCode.PAGE_DOWN: case keyCode.UP: + if (!event.shiftKey) { + event.preventDefault(); + this._proxyEvents(event); + } + + suggestList = event.currentTarget.parentNode.getElementsByTagName('ul')[0]; + hasSuggestedItems = event.currentTarget.parentNode.getElementsByTagName('ul')[0].children.length >= 0; + if (hasSuggestedItems) { + selectedItem = $(suggestList.getElementsByClassName('_active')[0]).removeClass('_active').prev().addClass('_active'); + event.currentTarget.value = selectedItem.find("a").text(); + } + + break; + case keyCode.PAGE_DOWN: case keyCode.DOWN: if (!event.shiftKey) { event.preventDefault(); this._proxyEvents(event); } + + suggestList = event.currentTarget.parentNode.getElementsByTagName('ul')[0]; + hasSuggestedItems = event.currentTarget.parentNode.getElementsByTagName('ul')[0].children.length >= 0; + if(hasSuggestedItems){ + hasSelectedItems = suggestList.getElementsByClassName('_active').length === 0; + if(hasSelectedItems) { + selectedItem = $(suggestList.children[0]).addClass('_active'); + event.currentTarget.value = selectedItem.find("a").text(); + }else { + selectedItem = $(suggestList.getElementsByClassName('_active')[0]).removeClass('_active').next().addClass('_active'); + event.currentTarget.value = selectedItem.find("a").text(); + } + } break; case keyCode.TAB: if (this.isDropdownShown()) { @@ -197,6 +228,7 @@ break; case keyCode.ENTER: case keyCode.NUMPAD_ENTER: + if (this.isDropdownShown() && this._focused) { this._proxyEvents(event); event.preventDefault(); diff --git a/lib/web/mage/menu.js b/lib/web/mage/menu.js index 94055ff0a1ddbda398323f9d8e31a9060ac7bfd3..925ae9df33791441a2e54ed6457753a62cba0751 100644 --- a/lib/web/mage/menu.js +++ b/lib/web/mage/menu.js @@ -8,7 +8,7 @@ define([ "jquery/ui", "jquery/jquery.mobile.custom", "mage/translate" -], function($, mediaCheck){ +], function ($, mediaCheck) { 'use strict'; /** @@ -20,22 +20,30 @@ define([ expanded: false, delay: 300 }, + _create: function () { + var self = this; - _init: function() { + 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) { + if (this.options.expanded === true) { this.isExpanded(); } - if(this.options.responsive === true){ + if (this.options.responsive === true) { mediaCheck({ media: '(max-width: 640px)', - entry: $.proxy(function() { + entry: $.proxy(function () { this._toggleMobileMode(); }, this), - exit: $.proxy(function() { + exit: $.proxy(function () { this._toggleDesktopMode(); }, this) }); @@ -44,7 +52,7 @@ define([ this._assignControls()._listen(); }, - _assignControls: function() { + _assignControls: function () { this.controls = { toggleBtn: $('[data-action="toggle-nav"]'), swipeArea: $('.nav-sections') @@ -53,125 +61,125 @@ define([ return this; }, - _listen: function() { + _listen: function () { var controls = this.controls; var toggle = this.toggle; - this._on(controls.toggleBtn, { 'click' : toggle }); - this._on(controls.swipeArea, { 'swipeleft': toggle }); + this._on(controls.toggleBtn, {'click': toggle}); + this._on(controls.swipeArea, {'swipeleft': toggle}); }, - toggle: function() { + toggle: function () { if ($('html').hasClass('nav-open')) { $('html').removeClass('nav-open'); - setTimeout(function() { + setTimeout(function () { $('html').removeClass('nav-before-open'); - },300); + }, 300); } else { $('html').addClass('nav-before-open'); - setTimeout(function() { + setTimeout(function () { $('html').addClass('nav-open'); - },42); + }, 42); } }, //Add class for expanded option - isExpanded: function() { - var subMenus = this.element.find( this.options.menus ), + isExpanded: function () { + var subMenus = this.element.find(this.options.menus), expandedMenus = subMenus.find('ul'); expandedMenus.addClass('expanded'); }, - _activate: function( event ) { + _activate: function (event) { window.location.href = this.active.find('> a').attr('href'); this.collapseAll(event); }, - _keydown: function(event) { + _keydown: function (event) { var match, prev, character, skip, regex, - preventDefault = true; + preventDefault = true; - function escape( value ) { - return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + function escape(value) { + return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); } - if(this.active.closest('ul').attr('aria-expanded') != 'true') { + if (this.active.closest('ul').attr('aria-expanded') != 'true') { - switch ( event.keyCode ) { + switch (event.keyCode) { case $.ui.keyCode.PAGE_UP: - this.previousPage( event ); + this.previousPage(event); break; case $.ui.keyCode.PAGE_DOWN: - this.nextPage( event ); + this.nextPage(event); break; case $.ui.keyCode.HOME: - this._move( "first", "first", event ); + this._move("first", "first", event); break; case $.ui.keyCode.END: - this._move( "last", "last", event ); + this._move("last", "last", event); break; case $.ui.keyCode.UP: - this.previous( event ); + this.previous(event); break; case $.ui.keyCode.DOWN: - if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { - this.expand( event ); + if (this.active && !this.active.is(".ui-state-disabled")) { + this.expand(event); } break; case $.ui.keyCode.LEFT: - this.previous( event ); + this.previous(event); break; case $.ui.keyCode.RIGHT: - this.next( event ); + this.next(event); break; case $.ui.keyCode.ENTER: case $.ui.keyCode.SPACE: - this._activate( event ); + this._activate(event); break; case $.ui.keyCode.ESCAPE: - this.collapse( event ); + this.collapse(event); break; default: preventDefault = false; prev = this.previousFilter || ""; - character = String.fromCharCode( event.keyCode ); + character = String.fromCharCode(event.keyCode); skip = false; - clearTimeout( this.filterTimer ); + clearTimeout(this.filterTimer); - if ( character === prev ) { + 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() ); + 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 = 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) { + 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 ) { + if (match.length) { + this.focus(event, match); + if (match.length > 1) { this.previousFilter = character; - this.filterTimer = this._delay(function() { + this.filterTimer = this._delay(function () { delete this.previousFilter; - }, 1000 ); + }, 1000); } else { delete this.previousFilter; } @@ -180,65 +188,65 @@ define([ } } } else { - switch ( event.keyCode ) { + switch (event.keyCode) { case $.ui.keyCode.DOWN: - this.next( event ); + this.next(event); break; case $.ui.keyCode.UP: - this.previous( event ); + this.previous(event); break; case $.ui.keyCode.RIGHT: - if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { - this.expand( event ); + if (this.active && !this.active.is(".ui-state-disabled")) { + this.expand(event); } break; case $.ui.keyCode.ENTER: case $.ui.keyCode.SPACE: - this._activate( event ); + this._activate(event); break; case $.ui.keyCode.LEFT: case $.ui.keyCode.ESCAPE: - this.collapse( event ); + this.collapse(event); break; default: preventDefault = false; prev = this.previousFilter || ""; - character = String.fromCharCode( event.keyCode ); + character = String.fromCharCode(event.keyCode); skip = false; - clearTimeout( this.filterTimer ); + clearTimeout(this.filterTimer); - if ( character === prev ) { + 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() ); + 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 = 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) { + 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 ) { + if (match.length) { + this.focus(event, match); + if (match.length > 1) { this.previousFilter = character; - this.filterTimer = this._delay(function() { + this.filterTimer = this._delay(function () { delete this.previousFilter; - }, 1000 ); + }, 1000); } else { delete this.previousFilter; } @@ -248,27 +256,27 @@ define([ } } - if ( preventDefault ) { + if (preventDefault) { event.preventDefault(); } }, - _toggleMobileMode: function() { + _toggleMobileMode: function () { $(this.element).off('mouseenter mouseleave'); this._on({ - "click .ui-menu-item:has(a)": function( event ) { + "click .ui-menu-item:has(a)": function (event) { event.preventDefault(); - var target = $( event.target ).closest( ".ui-menu-item" ); - - if ( !target.hasClass('level-top') || !target.has( ".ui-menu" ).length ) { + var target = $(event.target).closest(".ui-menu-item"); + + if (!target.hasClass('level-top') || !target.has(".ui-menu").length) { window.location.href = target.find('> a').attr('href'); } } }); var subMenus = this.element.find('.level-top'); - $.each(subMenus, $.proxy(function(index, item) { + $.each(subMenus, $.proxy(function (index, item) { var category = $(item).find('> a span').not('.ui-menu-icon').text(), categoryUrl = $(item).find('> a').attr('href'), menu = $(item).find('> .ui-menu'); @@ -281,58 +289,79 @@ define([ .addClass('ui-menu-item all-category') .html(this.categoryLink); - if(menu.find('.all-category').length === 0) { + if (menu.find('.all-category').length === 0) { menu.prepend(this.categoryParent); } }, this)); }, - _toggleDesktopMode: function() { + _toggleDesktopMode: function () { 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 ) { + "mousedown .ui-menu-item > a": function (event) { event.preventDefault(); }, - "click .ui-state-disabled > a": function( event ) { + "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 ); + "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() ) { + 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 ) { + 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 ] ); + 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 ); + if (this.active && this.active.parents(".ui-menu").length === 1) { + clearTimeout(this.timer); } } } }, - "mouseenter .ui-menu-item": function( event ) { - var target = $( event.currentTarget ); + "mouseenter .ui-menu-item": function (event) { + var target = $(event.currentTarget), + ulElement, + ulElementWidth, + width, + targetPageX, + rightBound; + + if (target.has('ul')) { + ulElement = target.find('ul'); + ulElementWidth = target.find('ul').outerWidth(true); + width = target.outerWidth() * 2; + targetPageX = target.offset().left; + rightBound = $(window).width(); + + if ((ulElementWidth + width + targetPageX) > rightBound) { + ulElement.addClass('submenu-reverse'); + } + if ((targetPageX - ulElementWidth) < 0) { + ulElement.removeClass('submenu-reverse'); + } + } + // 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 ); + target.siblings().children(".ui-state-active").removeClass("ui-state-active"); + this.focus(event, target); }, - "mouseleave": function( event ){ - this.collapseAll( event, true ); + "mouseleave": function (event) { + this.collapseAll(event, true); }, "mouseleave .ui-menu": "collapseAll" }); @@ -349,11 +378,14 @@ define([ }, 300); } }, - _delay: function(handler, delay) { - handler.apply(this, arguments); - - return setTimeout(function() {}, delay || 0); + var instance = this, + handlerProxy = function () { + return (typeof handler === "string" ? instance[handler] : handler) + .apply(instance, arguments); + }; + + return setTimeout(handlerProxy, delay || 0); } }); @@ -368,7 +400,7 @@ define([ breakpoint: 768 }, - _init: function() { + _init: function () { this._super(); var that = this, @@ -383,24 +415,24 @@ define([ this.setMaxItems(); //check responsive option - if(responsive == "onResize") { - $(window).on('resize', function() { - if($(window).width() > that.options.breakpoint) { - that._responsive(); - $('[responsive=more]').show(); + if (responsive == "onResize") { + $(window).on('resize', function () { + if ($(window).width() > that.options.breakpoint) { + that._responsive(); + $('[responsive=more]').show(); } else { that.element.children().show(); $('[responsive=more]').hide(); } }); - } else if(responsive == "onReload") { + } else if (responsive == "onReload") { this._responsive(); } }, - setupMoreMenu: function() { + setupMoreMenu: function () { var moreListItems = this.element.children().clone(), - moreLink = $('<a>'+ this.options.moreText +'</a>'); + moreLink = $('<a>' + this.options.moreText + '</a>'); moreListItems.hide(); @@ -418,15 +450,15 @@ define([ .attr('responsive', 'more') .append(this.moreListContainer) .menu({ - position : { - my : "right top", - at : "right bottom" + position: { + my: "right top", + at: "right bottom" } }) .insertAfter(this.element); }, - _responsive: function() { + _responsive: function () { var container = $(this.options.container), containerSize = container.width(), width = 0, @@ -434,7 +466,7 @@ define([ more = $('.ui-menu-more > li > ul > li a'); - items = items.map(function() { + items = items.map(function () { var item = {}; item.item = $(this); @@ -442,36 +474,36 @@ define([ return item; }); - $.each(items, function(index, item){ + $.each(items, function (index, item) { var itemText = items[index].item - .find('a:first') - .text(); + .find('a:first') + .text(); width += parseInt(items[index].itemSize, null); - if(width < containerSize) { + if (width < containerSize) { items[index].item.show(); - more.each(function() { - var text = $(this).text(); - if(text === itemText){ - $(this).parent().hide(); - } - }); - } else if(width > containerSize) { + more.each(function () { + var text = $(this).text(); + if (text === itemText) { + $(this).parent().hide(); + } + }); + } else if (width > containerSize) { items[index].item.hide(); - more.each(function() { - var text = $(this).text(); - if(text === itemText){ - $(this).parent().show(); - } - }); + more.each(function () { + var text = $(this).text(); + if (text === itemText) { + $(this).parent().show(); + } + }); } }); }, - setMaxItems: function() { + setMaxItems: function () { var items = this.element.children('li'), itemsCount = items.length, maxItems = this.options.maxItems, @@ -480,23 +512,23 @@ define([ overflowItems.hide(); - overflowItems.each(function(){ + overflowItems.each(function () { var itemText = $(this).find('a:first').text(); $(this).hide(); - $('.ui-menu-more > li > ul > li a').each(function() { - var text = $(this).text(); - if(text === itemText){ - $(this).parent().show(); - } - }); + $('.ui-menu-more > li > ul > li a').each(function () { + var text = $(this).text(); + if (text === itemText) { + $(this).parent().show(); + } + }); }); } }); - + return { - menu: $.mage.menu, + menu: $.mage.menu, navigation: $.mage.navigation }; }); diff --git a/lib/web/mage/utils/arrays.js b/lib/web/mage/utils/arrays.js index 54fb9a6189a8df0335808a989c5be5b859643ed8..d931fc1c7118a76ac5fe4f3b2dbc9cf4df9756a7 100644 --- a/lib/web/mage/utils/arrays.js +++ b/lib/web/mage/utils/arrays.js @@ -8,6 +8,25 @@ define([ ], function (_, utils) { 'use strict'; + /** + * Defines index of an item in a specified container. + * + * @param {*} item - Item whose index should be defined. + * @param {Array} container - Container upon which to perform search. + * @returns {Number} + */ + function getIndex(item, container) { + var index = container.indexOf(item); + + if (~index) { + return index; + } + + return _.findIndex(container, function (value) { + return value && value.name === item; + }); + } + return { /** * Facade method to remove/add value from/to array @@ -62,21 +81,6 @@ define([ return this; }, - /** - * Extends an incoming array with a specified ammount of undefined values - * starting from a specified position. - * - * @param {Array} container - Array to be extended. - * @param {Number} size - Ammount of values to be added. - * @param {Number} [offset=0] - Position at which to start inserting values. - * @returns {Array} Modified array. - */ - reserve: function (container, size, offset) { - container.splice(offset || 0, 0, new Array(size)); - - return _.flatten(container); - }, - /** * Compares multiple arrays without tracking order of their elements. * @@ -93,7 +97,64 @@ define([ }); }, - formatOffset: function(elems, offset) { + /** + * Inserts specified item into container at specified position. + * + * @param {*} item - Item to be inserted into container. + * @param {Array} container - Container of items. + * @param {*} [position=-1] - Position at which item should be inserted. + * Position can represent: + * - specific index in container + * - item which might already be present in container + * - structure with one of these properties: after, before + * @returns {Boolean|*} + * - true if element has changed its' position + * - false if nothing has changed + * - inserted value if it wasn't present in container + */ + insert: function (item, container, position) { + var currentIndex = getIndex(item, container), + newIndex, + target; + + if (typeof position === 'undefined') { + position = -1; + } else if (typeof position === 'string') { + position = isNaN(+position) ? position : +position; + } + + newIndex = position; + + if (~currentIndex) { + target = container.splice(currentIndex, 1)[0]; + + if (typeof item === 'string') { + item = target; + } + } + + if (typeof position !== 'number') { + target = position.after || position.before || position; + + newIndex = getIndex(target, container); + + if (~newIndex && (position.after || newIndex >= currentIndex)) { + newIndex++; + } + } + + if (newIndex < 0) { + newIndex += container.length + 1; + } + + container[newIndex] ? + container.splice(newIndex, 0, item) : + container[newIndex] = item; + + return !~currentIndex ? item : currentIndex !== newIndex; + }, + + formatOffset: function (elems, offset) { if (utils.isEmpty(offset)) { offset = -1; } diff --git a/lib/web/mage/utils/objects.js b/lib/web/mage/utils/objects.js index 7cd4924e492a39ee5609bb7e25c9bdef887c2ebb..4aa5d7216c6f3b4c4496071da48516e7d029e78b 100644 --- a/lib/web/mage/utils/objects.js +++ b/lib/web/mage/utils/objects.js @@ -207,7 +207,12 @@ define([ return result; }, - extend: function (target) { + /** + * Performs deep extend of specified objects. + * + * @returns {Object|Array} Extended object. + */ + extend: function () { var args = _.toArray(arguments); args.unshift(true); @@ -215,15 +220,36 @@ define([ return $.extend.apply($, args); }, + /** + * Performs a deep clone of a specified object. + * + * @param {(Object|Array)} data - Data that should be copied. + * @returns {Object|Array} Cloned object. + */ copy: function (data) { - return this.extend({}, data); + var result = data, + isArray = Array.isArray(data), + placeholder; + + if (this.isObject(data) || isArray) { + placeholder = isArray ? [] : {}; + result = this.extend(placeholder, data); + } + + return result; }, - isObject: function (data) { + /** + * Checks if provided value is a plain object. + * + * @param {*} value - Value to be checked. + * @returns {Boolean} + */ + isObject: function (value) { var objProto = Object.prototype; - return typeof data == 'object' ? - objProto.toString.call(data) === '[object Object]' : + return typeof value == 'object' ? + objProto.toString.call(value) === '[object Object]' : false; } }; diff --git a/lib/web/mage/utils/strings.js b/lib/web/mage/utils/strings.js index 185951886aaf3271e84cfc4d79d16016aec1d5ef..914ca72c1bdf63a6525eca1fc2c2c7372e60ade3 100644 --- a/lib/web/mage/utils/strings.js +++ b/lib/web/mage/utils/strings.js @@ -7,7 +7,29 @@ define([ ], function (_) { 'use strict'; + var jsonRe = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; + return { + /** + * Attempts to convert string to one of the primitive values, + * or to parse it as a valid json object. + * + * @param {String} str - String to be processed. + * @returns {*} + */ + castString: function (str) { + try { + str = str === 'true' ? true : + str === 'false' ? false : + str === 'null' ? null : + +str + '' === str ? +str : + jsonRe.test(str) ? JSON.parse(str) : + str; + } catch (e) {} + + return str; + }, + /** * Splits string by separator if it's possible, * otherwise returns the incoming value. @@ -62,6 +84,13 @@ define([ return value === '' || _.isUndefined(value) || _.isNull(value); }, + /** + * Adds 'prefix' to the 'part' value if it was provided. + * + * @param {String} prefix + * @param {String} part + * @returns {String} + */ fullPath: function (prefix, part) { return prefix ? prefix + '.' + part : part; } diff --git a/lib/web/mage/utils/template.js b/lib/web/mage/utils/template.js index a2b8cd462f6669405c84a39a89785cec54cb472b..d76f0e890f665582b06071a7ab1253717eb3656a 100644 --- a/lib/web/mage/utils/template.js +++ b/lib/web/mage/utils/template.js @@ -4,8 +4,9 @@ */ define([ 'underscore', - 'mage/utils/objects' -], function (_, utils) { + 'mage/utils/objects', + 'mage/utils/strings' +], function (_, utils, stringUtils) { 'use strict'; var tmplSettings = _.templateSettings, @@ -28,6 +29,7 @@ define([ })(); if (hasStringTmpls) { + /*eslint-disable no-unused-vars, no-eval*/ /** * Evaluates template string using ES6 templates. * @@ -38,6 +40,7 @@ define([ template = function (tmpl, $) { return eval('`' + tmpl + '`'); }; + /*eslint-enable no-unused-vars, no-eval*/ } else { /** * Fallback function used when ES6 templates are not supported. @@ -52,7 +55,9 @@ define([ tmplSettings.interpolate = interpolate; - tmpl = _.template(tmpl, {variable: '$'})(data); + tmpl = _.template(tmpl, { + variable: '$' + })(data); tmplSettings.interpolate = cached; @@ -63,7 +68,7 @@ define([ /** * Checks if provided value contains template syntax. * - * @param {*} value - Value to be check. + * @param {*} value - Value to be checked. * @returns {Boolean} */ function isTemplate(value) { @@ -76,9 +81,12 @@ define([ * * @param {String} tmpl - Template string. * @param {Object} data - Data object used in a template. - * @returns {String} Compiled template. + * @param {Boolean} [castString=false] - Flag that indicates whether template + * should be casted after evaluation to a value of another type or + * that it should be leaved as a string. + * @returns {*} Compiled template. */ - function render(tmpl, data) { + function render(tmpl, data, castString) { var last = tmpl; data = Object.create(data); @@ -93,7 +101,9 @@ define([ last = tmpl; } - return tmpl; + return castString ? + stringUtils.castString(tmpl) : + tmpl; } return { @@ -102,7 +112,10 @@ define([ * * @param {Object|String} tmpl * @param {Object} [data] - Data object to match with template. - * @returns {Object|String} + * @param {Boolean} [castString=false] - Flag that indicates whether template + * should be casted after evaluation to a value of another type or + * that it should be leaved as a string. + * @returns {*} * * @example Template defined as a string. * var source = { foo: 'Random Stuff', bar: 'Some' }; @@ -114,6 +127,8 @@ define([ * var tmpl = { * key: {'${ $.$data.bar }': '${ $.$data.foo }'}, * foo: 'bar', + * x1: 2, x2: 5, + * delta: '${ $.x2 - $.x1 }', * baz: 'Upper ${ $.foo.toUpperCase() }' * }; * @@ -121,19 +136,25 @@ define([ * => { * key: {'Some': 'Random Stuff'}, * foo: 'bar', + * x1: 2, x2: 5, + * delta: 3, * baz: 'Upper BAR' * }; */ - template: function (tmpl, data) { + template: function (tmpl, data, castString) { + var iterate; + if (typeof tmpl === 'string') { - return render(tmpl, data); + return render(tmpl, data, castString); } - tmpl = utils.extend({}, tmpl); - + tmpl = utils.copy(tmpl); tmpl.$data = data || {}; - _.each(tmpl, function iterate(value, key, list) { + /** + * Template iterator function. + */ + iterate = function (value, key, list) { if (key === '$data') { return; } @@ -146,11 +167,13 @@ define([ } if (isTemplate(value)) { - list[key] = render(value, tmpl); - } else if (_.isObject(value)) { + list[key] = render(value, tmpl, castString); + } else if (utils.isObject(value) || Array.isArray(value)) { _.each(value, iterate); } - }); + }; + + _.each(tmpl, iterate); delete tmpl.$data;