diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml index a793127f3..8bf009ff2 100644 --- a/.github/codeql-config.yml +++ b/.github/codeql-config.yml @@ -3,5 +3,4 @@ paths-ignore: - 'js/prototype/prototype.js' - 'js/mage/adminhtml/wysiwyg/tiny_mce/setup.js' - 'js/validation.js' - - 'js/extjs/ext-tree.js' - '**/*.test.js' diff --git a/.phpstan.baseline.neon b/.phpstan.baseline.neon index 7cb26cc1a..9228c2a8a 100644 --- a/.phpstan.baseline.neon +++ b/.phpstan.baseline.neon @@ -4350,12 +4350,6 @@ parameters: count: 4 path: app/design/adminhtml/default/default/template/api/roles.phtml - - - message: '#^Variable \$this might not be defined\.$#' - identifier: variable.undefined - count: 9 - path: app/design/adminhtml/default/default/template/api/rolesedit.phtml - - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined @@ -4404,12 +4398,6 @@ parameters: count: 4 path: app/design/adminhtml/default/default/template/api/users.phtml - - - message: '#^Variable \$this might not be defined\.$#' - identifier: variable.undefined - count: 11 - path: app/design/adminhtml/default/default/template/api2/attribute/resource.phtml - - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined @@ -4488,18 +4476,6 @@ parameters: count: 5 path: app/design/adminhtml/default/default/template/bundle/product/edit/bundle/option/search.phtml - - - message: '#^Variable \$this might not be defined\.$#' - identifier: variable.undefined - count: 12 - path: app/design/adminhtml/default/default/template/catalog/category/checkboxes/tree.phtml - - - - message: '#^Variable \$this might not be defined\.$#' - identifier: variable.undefined - count: 19 - path: app/design/adminhtml/default/default/template/catalog/category/widget/tree.phtml - - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined @@ -4962,12 +4938,6 @@ parameters: count: 1 path: app/design/adminhtml/default/default/template/resetforgottenpassword.phtml - - - message: '#^Variable \$this might not be defined\.$#' - identifier: variable.undefined - count: 5 - path: app/design/adminhtml/default/default/template/review/add.phtml - - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined diff --git a/app/code/core/Mage/Admin/Model/Session.php b/app/code/core/Mage/Admin/Model/Session.php index 6b1e930a6..7377ffadf 100644 --- a/app/code/core/Mage/Admin/Model/Session.php +++ b/app/code/core/Mage/Admin/Model/Session.php @@ -7,7 +7,7 @@ * @package Mage_Admin * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2018-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -23,17 +23,9 @@ * @method $this setActiveTabId(int $value) * @method $this unsActiveTabId() * @method $this setAttributeData(array|false $data) - * @method string getDeletedPath() - * @method $this setDeletedPath(string $value) * @method bool getIndirectLogin() * @method $this setIndirectLogin(bool $value) * @method $this setIsFirstVisit(bool $value) - * @method bool getIsTreeWasExpanded() - * @method $this setIsTreeWasExpanded(bool $value) - * @method int getLastEditedCategory() - * @method $this setLastEditedCategory(int $value) - * @method string getLastViewedStore() - * @method $this setLastViewedStore(string $value) * @method bool getUserPasswordChanged() * @method $this setUserPasswordChanged(bool $value) * @method bool hasSyncProcessStopWatch() diff --git a/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php b/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php index 428701fb9..2cdcf3ca8 100644 --- a/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php +++ b/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -99,7 +100,9 @@ protected function _getNodeJson($node, $level = 0) } } } - if (!empty($item['children'])) { + if (empty($item['children'])) { + unset($item['children']); + } else { usort($item['children'], [$this, '_sortTree']); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Abstract.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Abstract.php index ee702e58f..0d008a95b 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Abstract.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Abstract.php @@ -7,17 +7,40 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ /** - * Category abstract block + * Category tree abstract block * * @category Mage * @package Mage_Adminhtml + * + * @method setRecursionLevel(?int $value) */ class Mage_Adminhtml_Block_Catalog_Category_Abstract extends Mage_Adminhtml_Block_Template { + /** + * Default number of children levels when loading a tree node + */ + public const DEFAULT_RECURSION_LEVEL = 3; + + /** + * Whether to load product count when calling self::getCategoryCollection() + * + * @var bool + */ + protected $_withProductCount = true; + + /** + * Return the number of children levels when loading a tree node + */ + public function getRecursionLevel(): int + { + return (int) ($this->getDataByKey('recursion_level') ?? self::DEFAULT_RECURSION_LEVEL); + } + /** * Retrieve current category instance * @@ -28,27 +51,72 @@ public function getCategory() return Mage::registry('category'); } + /** + * Retrieve current category's ID, or Mage_Catalog_Model_Category::TREE_ROOT_ID + * + * @return ?int + */ public function getCategoryId() { if ($this->getCategory()) { - return $this->getCategory()->getId(); + return $this->getCategory()->getId() + ? (int) $this->getCategory()->getId() + : null; + } else { + return Mage_Catalog_Model_Category::TREE_ROOT_ID; } - return Mage_Catalog_Model_Category::TREE_ROOT_ID; } + /** + * Retrieve current category's name + * + * @return ?string + */ public function getCategoryName() { return $this->getCategory()->getName(); } + /** + * Retrieve current category's path + * + * @return ?string + */ public function getCategoryPath() { if ($this->getCategory()) { return $this->getCategory()->getPath(); } - return Mage_Catalog_Model_Category::TREE_ROOT_ID; + return (string) Mage_Catalog_Model_Category::TREE_ROOT_ID; + } + + /** + * Return default store ID + * + * @return int + */ + protected function _getDefaultStoreId() + { + return Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID; } + /** + * Return current store requested in URL + * + * @return Mage_Core_Model_Store + * @throws Mage_Core_Model_Store_Exception + */ + public function getStore() + { + $storeId = (int) $this->getRequest()->getParam('store', $this->_getDefaultStoreId()); + return Mage::app()->getStore($storeId); + } + + /** + * Check if store has a root category + * + * @return bool + */ public function hasStoreRootCategory() { $root = $this->getRoot(); @@ -58,43 +126,69 @@ public function hasStoreRootCategory() return false; } - public function getStore() + /** + * Return ids of root categories as array + * + * @return list + */ + public function getRootIds() { - $storeId = (int) $this->getRequest()->getParam('store'); - return Mage::app()->getStore($storeId); + if (!$this->hasData('root_ids')) { + $this->setData('root_ids', Mage::getResourceModel('catalog/category')->getRootIds()); + } + return $this->getDataByKey('root_ids'); } - public function getRoot($parentNodeCategory = null, $recursionLevel = 3) + /** + * Get and register category tree root + * + * Root node will be store's root category, or Mage_Catalog_Model_Category::TREE_ROOT_ID if no store is requested + * If $parentNodeCategory is given, call will be forwarded to self::getNode() and root will not be registered + * + * @param Mage_Catalog_Model_Category|int|string $parentNodeCategory + * @param int $recursionLevel how many levels to load + * @return Varien_Data_Tree_Node|null + */ + public function getRoot($parentNodeCategory = null, $recursionLevel = null) { - if (!is_null($parentNodeCategory) && $parentNodeCategory->getId()) { - return $this->getNode($parentNodeCategory, $recursionLevel); + if (!is_null($recursionLevel)) { + $this->setRecursionLevel($recursionLevel); + } + if (!is_null($parentNodeCategory)) { + if (!$parentNodeCategory instanceof Mage_Catalog_Model_Category) { + $parentNodeCategory = Mage::getModel('catalog/category')->load($parentNodeCategory); + } + if ($parentNodeCategory->getId()) { + return $this->getNode($parentNodeCategory); + } + } + if ($this->getCategory()) { + return $this->getRootByIds($this->getCategory()->getPathIds()); } $root = Mage::registry('root'); if (is_null($root)) { - $storeId = (int) $this->getRequest()->getParam('store'); - - if ($storeId) { - $store = Mage::app()->getStore($storeId); + $store = $this->getStore(); + if ($store->getId()) { $rootId = $store->getRootCategoryId(); + if ($this->getRecursionLevel() !== 0) { + $this->setRecursionLevel($this->getRecursionLevel() + 1); + } } else { $rootId = Mage_Catalog_Model_Category::TREE_ROOT_ID; } $tree = Mage::getResourceSingleton('catalog/category_tree') - ->load(null, $recursionLevel); - - if ($this->getCategory()) { - $tree->loadEnsuredNodes($this->getCategory(), $tree->getNodeById($rootId)); - } + ->load(null, $this->getRecursionLevel()); $tree->addCollectionData($this->getCategoryCollection()); - $root = $tree->getNodeById($rootId); - if ($root && $rootId != Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $root->setIsVisible(true); - } elseif ($root && $root->getId() == Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $root->setName(Mage::helper('catalog')->__('Root')); + if ($root) { + if ($rootId == Mage_Catalog_Model_Category::TREE_ROOT_ID) { + $root->setName(Mage::helper('catalog')->__('Root')); + } else { + $root->setIsVisible(true); + } } Mage::register('root', $root); @@ -104,80 +198,251 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) } /** - * Get and register categories root by specified categories IDs + * Get and register category tree root by specified category IDs * * IDs can be arbitrary set of any categories ids. * Tree with minimal required nodes (all parents and neighbours) will be built. - * If ids are empty, default tree with depth = 2 will be returned. + * If ids are empty, default tree with depth = $recursionLevel will be returned. * - * @param array $ids - * @return mixed|Varien_Data_Tree_Node|null + * @param array $ids list of category ids + * @param int $recursionLevel how many levels to load + * @return Varien_Data_Tree_Node|null */ - public function getRootByIds($ids) + public function getRootByIds($ids, $recursionLevel = null) { + if (!is_null($recursionLevel)) { + $this->setRecursionLevel($recursionLevel); + } + if (!is_array($ids) || empty($ids)) { + return $this->getRoot(); + } $root = Mage::registry('root'); - if ($root === null) { - $categoryTreeResource = Mage::getResourceSingleton('catalog/category_tree'); - $ids = $categoryTreeResource->getExistingCategoryIdsBySpecifiedIds($ids); - $tree = $categoryTreeResource->loadByIds($ids); - $rootId = Mage_Catalog_Model_Category::TREE_ROOT_ID; - $root = $tree->getNodeById($rootId); - if ($root && $root->getId() == Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $root->setName(Mage::helper('catalog')->__('Root')); + if (is_null($root)) { + $store = $this->getStore(); + if ($store->getId()) { + $rootId = $store->getRootCategoryId(); + if ($this->getRecursionLevel() !== 0) { + $this->setRecursionLevel($this->getRecursionLevel() + 1); + } + } else { + $rootId = Mage_Catalog_Model_Category::TREE_ROOT_ID; } + $tree = Mage::getResourceSingleton('catalog/category_tree') + ->loadByIds($ids, false, false, $this->getRecursionLevel()); + $tree->addCollectionData($this->getCategoryCollection()); + $root = $tree->getNodeById($rootId); + + if ($root) { + if ($rootId == Mage_Catalog_Model_Category::TREE_ROOT_ID) { + $root->setName(Mage::helper('catalog')->__('Root')); + } else { + $root->setIsVisible(true); + } + } + Mage::register('root', $root); } return $root; } - public function getNode($parentNodeCategory, $recursionLevel = 2) + /** + * Get category tree with specified category as the root + * + * @param Mage_Catalog_Model_Category $parentNodeCategory + * @param int $recursionLevel how many levels to load + * @return Varien_Data_Tree_Node|null + */ + public function getNode($parentNodeCategory, $recursionLevel = null) { + if (!is_null($recursionLevel)) { + $this->setRecursionLevel($recursionLevel); + } + $tree = Mage::getResourceModel('catalog/category_tree'); $nodeId = $parentNodeCategory->getId(); $node = $tree->loadNode($nodeId); - $node->loadChildren($recursionLevel); - - if ($node && $nodeId != Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $node->setIsVisible(true); - } elseif ($node && $node->getId() == Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $node->setName(Mage::helper('catalog')->__('Root')); - } + $node->loadChildren($this->getRecursionLevel()); $tree->addCollectionData($this->getCategoryCollection()); + if ($node) { + if ($nodeId == Mage_Catalog_Model_Category::TREE_ROOT_ID) { + $node->setName(Mage::helper('catalog')->__('Root')); + } else { + $node->setIsVisible(true); + } + } + return $node; } - public function getSaveUrl(array $args = []) + /** + * Load category collection for adding data to tree nodes + * + * @return Mage_Catalog_Model_Resource_Category_Collection + */ + public function getCategoryCollection() { - $params = ['_current' => true]; - $params = array_merge($params, $args); - return $this->getUrl('*/*/save', $params); + $collection = $this->getData('category_collection'); + if (is_null($collection)) { + $store = $this->getStore(); + + /** @var Mage_Catalog_Model_Resource_Category_Collection $collection */ + $collection = Mage::getModel('catalog/category')->getCollection(); + + $collection->addAttributeToSelect('name') + ->addAttributeToSelect('is_active') + ->setProductStoreId($this->getStore()->getId()) + ->setLoadProductCount($this->_withProductCount) + ->setStoreId($this->getStore()->getId()); + + $this->setData('category_collection', $collection); + } + return $collection; } - public function getEditUrl() + /** + * Get category children as array + * + * @param Mage_Catalog_Model_Category|int|string $parentNodeCategory + * @return array + */ + public function getTree($parentNodeCategory = null) { - return $this->getUrl('*/catalog_category/edit', ['_current' => true, 'store' => null, '_query' => false, 'id' => null, 'parent' => null]); + $rootArray = $this->_getNodeJson($this->getRoot($parentNodeCategory)); + return $rootArray['children'] ?? []; } /** - * Return ids of root categories as array + * Get category children as JSON * + * @param Mage_Catalog_Model_Category|int|string $parentNodeCategory + * @return string + */ + public function getTreeJson($parentNodeCategory = null) + { + $rootArray = $this->_getNodeJson($this->getRoot($parentNodeCategory)); + return Mage::helper('core')->jsonEncode($rootArray['children'] ?? []); + } + + /** + * Get JSON of a tree node or an associative array + */ + public function getNodeJson(Varien_Data_Tree_Node|array $node, int $level = 0): array + { + return $this->_getNodeJson($node, $level); + } + + /** + * Get JSON of a tree node or an associative array + * + * @param Varien_Data_Tree_Node|array $node + * @param int $level * @return array */ - public function getRootIds() + protected function _getNodeJson($node, $level = 0) + { + // create a node from data array + if (is_array($node)) { + $node = new Varien_Data_Tree_Node($node, 'entity_id', new Varien_Data_Tree()); + } + + $item = [ + 'id' => (int) $node->getId(), + 'text' => $this->buildNodeName($node), + 'type' => 'folder', + 'cls' => 'folder', + 'store' => (int) $this->getStore()->getId(), + 'path' => $node->getData('path'), + ]; + + $item['cls'] .= $node->getIsActive() ? ' active-category' : ' no-active-category'; + + if ($node->hasChildren() || $level < $this->getRecursionLevel() || $this->getRecursionLevel() === 0) { + $item['children'] = []; + foreach ($node->getChildren() as $child) { + $item['children'][] = $this->_getNodeJson($child, $level + 1); + } + } + + if ($this->getCategory() && $this->getCategoryId() === (int) $node->getId()) { + $item['checked'] = true; + } elseif ($node->getChecked()) { + $item['checked'] = true; + } + + $isParent = $this->_isParentSelectedCategory($node); + if ($isParent || $node->getLevel() < 2) { + $item['expanded'] = true; + } + + return $item; + } + + /** + * Get category name + * + * @param Varien_Object $node + * @return string + */ + public function buildNodeName($node) { - $ids = $this->getData('root_ids'); - if (is_null($ids)) { - $ids = []; - foreach (Mage::app()->getGroups() as $store) { - $ids[] = $store->getRootCategoryId(); + $result = $this->escapeHtml($node->getName()); + if ($this->_withProductCount) { + $result .= " ({$node->getProductCount()})"; + } + return $result; + } + + /** + * Check if the node contains children categories that are selected + * + * @param Varien_Object $node + * @return bool + */ + protected function _isParentSelectedCategory($node) + { + if ($node && $this->getCategory()) { + $pathIds = $this->getCategory()->getPathIds(); + if (in_array($node->getId(), $pathIds)) { + return true; } - $this->setData('root_ids', $ids); } - return $ids; + + return false; + } + + /** + * Returns URL for loading tree + * + * @param ?bool $expanded + * @return string + */ + public function getLoadTreeUrl($expanded = null) + { + return $this->getUrl('*/*/categoriesJson', [ + 'expand_all' => $expanded, + ]); + } + + /** + * @return string + */ + public function getSaveUrl(array $args = []) + { + return $this->getUrl('*/*/save', [ + '_current' => true, '_query' => false, ...$args, + ]); + } + + /** + * @return string + */ + public function getEditUrl() + { + return $this->getUrl('*/catalog_category/edit'); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Checkboxes/Tree.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Checkboxes/Tree.php index d13320d2a..caa9e82fb 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Checkboxes/Tree.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Checkboxes/Tree.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -17,13 +17,11 @@ * @category Mage * @package Mage_Adminhtml */ -class Mage_Adminhtml_Block_Catalog_Category_Checkboxes_Tree extends Mage_Adminhtml_Block_Catalog_Category_Tree +class Mage_Adminhtml_Block_Catalog_Category_Checkboxes_Tree extends Mage_Adminhtml_Block_Catalog_Category_Abstract { + /** @var list */ protected $_selectedIds = []; - /** - * @return $this - */ #[\Override] protected function _prepareLayout() { @@ -31,62 +29,61 @@ protected function _prepareLayout() return $this; } + /** + * @return list + */ public function getCategoryIds() { return $this->_selectedIds; } + /** + * @param array $ids + * @return $this + */ public function setCategoryIds($ids) { if (empty($ids)) { $ids = []; } elseif (!is_array($ids)) { - $ids = [(int) $ids]; + $ids = [$ids]; + } + foreach ($ids as $key => &$id) { + $id = (int) $id; + if ($id <= 0) { + unset($ids[$key]); + } } - $this->_selectedIds = $ids; + $this->_selectedIds = array_unique($ids); return $this; } #[\Override] - protected function _getNodeJson($node, $level = 1) + public function getRoot($parentNodeCategory = null, $recursionLevel = null) { - $item = []; - $item['text'] = $this->escapeHtml($node->getName()); - - if ($this->_withProductCount) { - $item['text'] .= ' (' . $node->getProductCount() . ')'; - } - $item['id'] = $node->getId(); - $item['path'] = $node->getData('path'); - $item['cls'] = 'folder ' . ($node->getIsActive() ? 'active-category' : 'no-active-category'); - $item['allowDrop'] = false; - $item['allowDrag'] = false; - - if ($node->hasChildren()) { - $item['children'] = []; - foreach ($node->getChildren() as $child) { - $item['children'][] = $this->_getNodeJson($child, $level + 1); - } - } - - if (empty($item['children']) && (int) $node->getChildrenCount() > 0) { - $item['children'] = []; - } - - if (!empty($item['children'])) { - $item['expanded'] = true; + if ($parentNodeCategory === null && $this->getCategoryIds()) { + return $this->getRootByIds($this->getCategoryIds(), $recursionLevel); + } else { + return parent::getRoot($parentNodeCategory, $recursionLevel); } + } + #[\Override] + protected function _getNodeJson($node, $level = 1) + { + $item = parent::_getNodeJson($node, $level); if (in_array($node->getId(), $this->getCategoryIds())) { $item['checked'] = true; } - return $item; } #[\Override] - public function getRoot($parentNodeCategory = null, $recursionLevel = 3) + protected function _isParentSelectedCategory($node) { - return $this->getRootByIds($this->getCategoryIds()); + $allChildrenIds = array_keys($node->getAllChildNodes()); + $selectedChildren = array_intersect($this->getCategoryIds(), $allChildrenIds); + + return count($selectedChildren) > 0; } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit.php index ae3448706..4d56e8360 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -26,5 +27,6 @@ public function __construct() parent::__construct(); $this->setTemplate('catalog/category/edit.phtml'); + $this->setUseAjax(true); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit/Form.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit/Form.php index 06d70f8da..087a5885f 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Edit/Form.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2023 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -50,7 +50,7 @@ protected function _prepareLayout() $this->getLayout()->createBlock('adminhtml/widget_button') ->setData([ 'label' => Mage::helper('catalog')->__('Save Category'), - 'onclick' => "categorySubmit('" . $this->getSaveUrl() . "', true)", + 'onclick' => "categorySubmit('{$this->getSaveUrl()}')", 'class' => 'save', ]), ); @@ -63,7 +63,7 @@ protected function _prepareLayout() $this->getLayout()->createBlock('adminhtml/widget_button') ->setData([ 'label' => Mage::helper('catalog')->__('Delete Category'), - 'onclick' => "categoryDelete('" . $this->getUrl('*/*/delete', ['_current' => true]) . "', true, {$categoryId})", + 'onclick' => "categoryDelete('{$this->getDeleteUrl()}')", 'class' => 'delete', ]), ); @@ -71,13 +71,12 @@ protected function _prepareLayout() // Reset button if (!$category->isReadonly()) { - $resetPath = $categoryId ? '*/*/edit' : '*/*/add'; $this->setChild( 'reset_button', $this->getLayout()->createBlock('adminhtml/widget_button') ->setData([ 'label' => Mage::helper('catalog')->__('Reset'), - 'onclick' => "categoryReset('" . $this->getUrl($resetPath, ['_current' => true]) . "',true)", + 'onclick' => "categoryReset('{$this->getResetUrl()}')", ]), ); } @@ -85,6 +84,9 @@ protected function _prepareLayout() return parent::_prepareLayout(); } + /** + * @return string + */ public function getStoreConfigurationUrl() { $storeId = (int) $this->getRequest()->getParam('store'); @@ -98,11 +100,17 @@ public function getStoreConfigurationUrl() return $this->getUrl('*/system_store', $params); } + /** + * @return string + */ public function getDeleteButtonHtml() { return $this->getChildHtml('delete_button'); } + /** + * @return string + */ public function getSaveButtonHtml() { if ($this->hasStoreRootCategory()) { @@ -111,6 +119,9 @@ public function getSaveButtonHtml() return ''; } + /** + * @return string + */ public function getResetButtonHtml() { if ($this->hasStoreRootCategory()) { @@ -169,16 +180,23 @@ public function removeAdditionalButton($alias) return $this; } + /** + * @return string + */ public function getTabsHtml() { return $this->getChildHtml('tabs'); } + /** + * @return string + */ public function getHeader() { if ($this->hasStoreRootCategory()) { if ($this->getCategoryId()) { - return $this->getCategoryName(); + $categoryIdText = Mage::helper('catalog')->__('ID: %s', $this->getCategoryId()); + return $this->getCategoryName() . " ($categoryIdText)"; } else { $parentId = (int) $this->getRequest()->getParam('parent'); if ($parentId && ($parentId != Mage_Catalog_Model_Category::TREE_ROOT_ID)) { @@ -191,11 +209,24 @@ public function getHeader() return Mage::helper('catalog')->__('Set Root Category for Store'); } + /** + * @return string + */ public function getDeleteUrl(array $args = []) { - $params = ['_current' => true]; - $params = array_merge($params, $args); - return $this->getUrl('*/*/delete', $params); + return $this->getUrl('*/*/delete', [ + '_current' => true, '_query' => false, ...$args, + ]); + } + + /** + * @return string + */ + public function getResetUrl(array $args = []) + { + return $this->getUrl($this->getCategory()->getId() ? '*/*/edit' : '*/*/add', [ + '_current' => true, '_query' => false, ...$args, + ]); } /** @@ -210,17 +241,68 @@ public function getRefreshPathUrl(array $args = []) return $this->getUrl('*/*/refreshPath', $params); } + /** + * @deprecated use self::getProductsInfoJson() + */ public function getProductsJson() { $products = $this->getCategory()->getProductsPosition(); - if (!empty($products)) { - return Mage::helper('core')->jsonEncode($products); + return Mage::helper('core')->jsonEncode((object) $products); + } + + /** + * Return JSON for category edit product grid + * + * @return string + */ + public function getProductsInfoJson() + { + $gridBlock = $this->getLayout()->getBlock('category.product.grid'); + if ($gridBlock && $gridJsObject = $gridBlock->getJsObjectName()) { + $products = $this->getCategory()->getProductsPosition(); + return Mage::helper('core')->jsonEncode([ + 'gridJsObjectName' => $gridJsObject, + 'products' => (object) $products, + ]); } return '{}'; } + /** + * Return JSON for category edit page + */ + public function getCategoryInfoJson(): string + { + /** @var Mage_Adminhtml_Block_Catalog_Category_Tree */ + $treeBlock = $this->getLayout()->getBlock('category.tree'); + + $categories = Mage::getResourceSingleton('catalog/category_tree') + ->setStoreId($this->getCategory()->getStoreId()) + ->loadBreadcrumbsArray($this->getCategory()->getPath()); + + foreach ($categories as $key => $category) { + $categories[$key] = $treeBlock->getNodeJson($category); + } + + if (($last = array_key_last($categories)) !== null) { + $categories[$last]['checked'] = true; + } + + return Mage::helper('core')->jsonEncode([ + 'store_id' => (int) $this->getCategory()->getStoreId(), + 'category_id' => (int) $this->getCategory()->getId(), + 'can_add_sub' => (bool) $treeBlock->canAddSubCategory(), + 'breadcrumbs' => $categories, + ]); + } + + /** + * Check is Request from AJAX + * + * @return bool + */ public function isAjax() { - return Mage::app()->getRequest()->isXmlHttpRequest() || Mage::app()->getRequest()->getParam('isAjax'); + return Mage::app()->getRequest()->isAjax(); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Tree.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Tree.php index a214d5e04..eff800f42 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Tree.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Tree.php @@ -7,61 +7,51 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ /** - * Categories tree block + * Manage Categories tree block * * @category Mage * @package Mage_Adminhtml */ class Mage_Adminhtml_Block_Catalog_Category_Tree extends Mage_Adminhtml_Block_Catalog_Category_Abstract { - protected $_withProductCount; - public function __construct() { parent::__construct(); $this->setTemplate('catalog/category/tree.phtml'); - $this->setUseAjax(true); - $this->_withProductCount = true; } #[\Override] protected function _prepareLayout() { - $addUrl = $this->getUrl('*/*/add', [ - '_current' => true, - 'id' => null, - '_query' => false, - ]); + $addUrl = $this->getUrl('*/*/add', ['_current' => true, '_query' => false, 'id' => null]); $this->setChild( 'add_sub_button', $this->getLayout()->createBlock('adminhtml/widget_button') ->setData([ 'label' => Mage::helper('catalog')->__('Add Subcategory'), - 'onclick' => "addNew('" . $addUrl . "', false)", + 'onclick' => "addNew('$addUrl', false)", 'class' => 'add', 'id' => 'add_subcategory_button', - 'style' => $this->canAddSubCategory() ? '' : 'display: none;', + 'disabled' => !$this->canAddSubCategory(), ]), ); - if ($this->canAddRootCategory()) { - $this->setChild( - 'add_root_button', - $this->getLayout()->createBlock('adminhtml/widget_button') - ->setData([ - 'label' => Mage::helper('catalog')->__('Add Root Category'), - 'onclick' => "addNew('" . $addUrl . "', true)", - 'class' => 'add', - 'id' => 'add_root_category_button', - ]), - ); - } + $this->setChild( + 'add_root_button', + $this->getLayout()->createBlock('adminhtml/widget_button') + ->setData([ + 'label' => Mage::helper('catalog')->__('Add Root Category'), + 'onclick' => "addNew('$addUrl', true)", + 'class' => 'add' . ($this->canAddRootCategory() ? '' : ' no-display'), + 'id' => 'add_root_category_button', + ]), + ); $this->setChild( 'store_switcher', @@ -72,71 +62,33 @@ protected function _prepareLayout() return parent::_prepareLayout(); } - protected function _getDefaultStoreId() - { - return Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID; - } - - public function getCategoryCollection() - { - $storeId = $this->getRequest()->getParam('store', $this->_getDefaultStoreId()); - $collection = $this->getData('category_collection'); - if (is_null($collection)) { - $collection = Mage::getModel('catalog/category')->getCollection(); - - /** @var Mage_Catalog_Model_Resource_Category_Collection $collection */ - $collection->addAttributeToSelect('name') - ->addAttributeToSelect('is_active') - ->setProductStoreId($storeId) - ->setLoadProductCount($this->_withProductCount) - ->setStoreId($storeId); - - $this->setData('category_collection', $collection); - } - return $collection; - } - + /** + * @return string + */ public function getAddRootButtonHtml() { return $this->getChildHtml('add_root_button'); } + /** + * @return string + */ public function getAddSubButtonHtml() { return $this->getChildHtml('add_sub_button'); } - public function getExpandButtonHtml() - { - return $this->getChildHtml('expand_button'); - } - - public function getCollapseButtonHtml() - { - return $this->getChildHtml('collapse_button'); - } - + /** + * @return string + */ public function getStoreSwitcherHtml() { return $this->getChildHtml('store_switcher'); } - public function getLoadTreeUrl($expanded = null) - { - $params = ['_current' => true, 'id' => null,'store' => null]; - if ((is_null($expanded) && Mage::getSingleton('admin/session')->getIsTreeWasExpanded()) - || $expanded == true - ) { - $params['expand_all'] = true; - } - return $this->getUrl('*/*/categoriesJson', $params); - } - - public function getNodesUrl() - { - return $this->getUrl('*/catalog_category/jsonTree'); - } - + /** + * @return string + */ public function getSwitchTreeUrl() { return $this->getUrl( @@ -145,65 +97,56 @@ public function getSwitchTreeUrl() ); } - public function getIsWasExpanded() - { - return Mage::getSingleton('admin/session')->getIsTreeWasExpanded(); - } - + /** + * @return string + */ public function getMoveUrl() { return $this->getUrl('*/catalog_category/move', ['store' => $this->getRequest()->getParam('store')]); } - public function getTree($parenNodeCategory = null) - { - $rootArray = $this->_getNodeJson($this->getRoot($parenNodeCategory)); - return $rootArray['children'] ?? []; - } - - public function getTreeJson($parenNodeCategory = null) - { - $rootArray = $this->_getNodeJson($this->getRoot($parenNodeCategory)); - return Mage::helper('core')->jsonEncode($rootArray['children'] ?? []); - } - /** - * Get JSON of array of categories, that are breadcrumbs for specified category path + * Returns root node and sets 'checked' flag (if necessary) * - * @param string $path - * @param string $javascriptVarName - * @return string + * @return Varien_Data_Tree_Node */ - public function getBreadcrumbsJavascript($path, $javascriptVarName) - { - if (empty($path)) { - return ''; - } - - $categories = Mage::getResourceSingleton('catalog/category_tree') - ->setStoreId($this->getStore()->getId())->loadBreadcrumbsArray($path); - if (empty($categories)) { - return ''; - } - foreach ($categories as $key => $category) { - $categories[$key] = $this->_getNodeJson($category); + public function getRootNode() + { + $root = $this->getRoot(); + if ($root && $this->getCategory()) { + if ($selected = $root->getTree()->getNodeById($this->getCategoryId())) { + $selected->setChecked(true); + } elseif ($parent = $root->getTree()->getNodeById($this->getCategory()->getParentId())) { + $parent->setChecked(true); + } } - return - ''; + return $root; } /** - * Get JSON of a tree node or an associative array - * - * @param Varien_Data_Tree_Node|array $node - * @param int $level - * @return array + * Get root category information */ + public function getRootTreeParameters(): array + { + $root = $this->getRootNode(); + return [ + 'data' => $this->getTree(), + 'parameters' => [ + 'text' => $this->buildNodeName($root), + 'allowDrag' => false, + 'allowDrop' => (bool) $root->getIsVisible(), + 'id' => (int) $root->getId(), + 'store_id' => (int) $this->getStore()->getId(), + 'category_id' => (int) $this->getCategory()->getId(), + 'checked' => (bool) $root->getChecked(), + 'root_visible' => (bool) $root->getIsVisible(), + 'can_add_root' => (bool) $this->canAddRootCategory(), + 'expanded' => $this->getRecursionLevel() === 0, + ], + ]; + } + + #[\Override] protected function _getNodeJson($node, $level = 0) { // create a node from data array @@ -211,62 +154,23 @@ protected function _getNodeJson($node, $level = 0) $node = new Varien_Data_Tree_Node($node, 'entity_id', new Varien_Data_Tree()); } - $item = []; - $item['text'] = $this->buildNodeName($node); + $item = parent::_getNodeJson($node, $level); - /* $rootForStores = Mage::getModel('core/store') - ->getCollection() - ->loadByCategoryIds(array($node->getEntityId())); */ - $rootForStores = in_array($node->getEntityId(), $this->getRootIds()); + $allowMove = (bool) $this->_isCategoryMoveable($node); + $isRoot = in_array($node->getEntityId(), $this->getRootIds()); - $item['id'] = $node->getId(); - $item['store'] = (int) $this->getStore()->getId(); - $item['path'] = $node->getData('path'); - - $item['cls'] = 'folder ' . ($node->getIsActive() ? 'active-category' : 'no-active-category'); - //$item['allowDrop'] = ($level<3) ? true : false; - $allowMove = $this->_isCategoryMoveable($node); - $item['allowDrop'] = $allowMove; // disallow drag if it's first level and category is root of a store - $item['allowDrag'] = $allowMove && !($node->getLevel() == 1 && $rootForStores); - - if ((int) $node->getChildrenCount() > 0) { - $item['children'] = []; - } - - $isParent = $this->_isParentSelectedCategory($node); - - if ($node->hasChildren()) { - $item['children'] = []; - if (!($this->getUseAjax() && $node->getLevel() > 1 && !$isParent)) { - foreach ($node->getChildren() as $child) { - $item['children'][] = $this->_getNodeJson($child, $level + 1); - } - } - } - - if ($isParent || $node->getLevel() < 2) { - $item['expanded'] = true; - } + $item['allowDrag'] = $allowMove && !$isRoot && $node->getLevel() > 1; return $item; } /** - * Get category name + * Check if the node can be moved * * @param Varien_Object $node - * @return string + * @return bool */ - public function buildNodeName($node) - { - $result = $this->escapeHtml($node->getName()); - if ($this->_withProductCount) { - $result .= ' (' . $node->getProductCount() . ')'; - } - return $result; - } - protected function _isCategoryMoveable($node) { $options = new Varien_Object([ @@ -282,65 +186,52 @@ protected function _isCategoryMoveable($node) return $options->getIsMoveable(); } - protected function _isParentSelectedCategory($node) - { - if ($node && $this->getCategory()) { - $pathIds = $this->getCategory()->getPathIds(); - if (in_array($node->getId(), $pathIds)) { - return true; - } - } - - return false; - } - /** - * Check if page loaded by outside link to category edit + * Check availability of adding root category * * @return bool */ - public function isClearEdit() + public function canAddRootCategory() { - return (bool) $this->getRequest()->getParam('clear'); + if ($this->getStore()->getId() !== 0) { + return false; + } + + $options = new Varien_Object(['is_allow' => true]); + Mage::dispatchEvent('adminhtml_catalog_category_tree_can_add_root_category', [ + 'category' => $this->getCategory(), + 'options' => $options, + 'store' => $this->getStore()->getId(), + ]); + + return (bool) $options->getIsAllow(); } /** - * Check availability of adding root category + * Check availability of adding sub category * * @return bool */ - public function canAddRootCategory() + public function canAddSubCategory() { $options = new Varien_Object(['is_allow' => true]); - Mage::dispatchEvent( - 'adminhtml_catalog_category_tree_can_add_root_category', - [ - 'category' => $this->getCategory(), - 'options' => $options, - 'store' => $this->getStore()->getId(), - ], - ); + Mage::dispatchEvent('adminhtml_catalog_category_tree_can_add_sub_category', [ + 'category' => $this->getCategory(), + 'options' => $options, + 'store' => $this->getStore()->getId(), + ]); - return $options->getIsAllow(); + return (bool) $options->getIsAllow(); } /** - * Check availability of adding sub category + * Check if page loaded by outside link to category edit * * @return bool + * @deprecated */ - public function canAddSubCategory() + public function isClearEdit() { - $options = new Varien_Object(['is_allow' => true]); - Mage::dispatchEvent( - 'adminhtml_catalog_category_tree_can_add_sub_category', - [ - 'category' => $this->getCategory(), - 'options' => $options, - 'store' => $this->getStore()->getId(), - ], - ); - - return $options->getIsAllow(); + return (bool) $this->getRequest()->getParam('clear'); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Widget/Chooser.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Widget/Chooser.php index 23a27b6f1..65803b55e 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Widget/Chooser.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Category/Widget/Chooser.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -17,7 +17,7 @@ * @category Mage * @package Mage_Adminhtml */ -class Mage_Adminhtml_Block_Catalog_Category_Widget_Chooser extends Mage_Adminhtml_Block_Catalog_Category_Tree +class Mage_Adminhtml_Block_Catalog_Category_Widget_Chooser extends Mage_Adminhtml_Block_Catalog_Category_Abstract { protected $_selectedCategories = []; @@ -40,7 +40,10 @@ public function __construct() */ public function setSelectedCategories($selectedCategories) { - $this->_selectedCategories = $selectedCategories; + if (!is_array($selectedCategories)) { + $selectedCategories = [$selectedCategories]; + } + $this->_selectedCategories = array_filter($selectedCategories); return $this; } @@ -113,6 +116,7 @@ protected function _getModelAttributeByEntityId($modelType, $attributeName, $ent } return $result; } + /** * Category Tree node onClick listener js function * @@ -124,33 +128,36 @@ public function getNodeClickListener() return $this->getData('node_click_listener'); } if ($this->getUseMassaction()) { - $js = ' - function (node, e) { - if (node.ui.toggleCheck) { - node.ui.toggleCheck(true); - } + return <<getId(); - $js = ' - function (node, e) { - ' . $chooserJsObject . '.setElementValue("category/" + node.attributes.id); - ' . $chooserJsObject . '.setElementLabel(node.text); - ' . $chooserJsObject . '.close(); + return <<getSelectedCategories()) { + return $this->getRootByIds($this->getSelectedCategories(), $recursionLevel); + } else { + return parent::getRoot($parentNodeCategory, $recursionLevel); } - return $js; } - /** - * Get JSON of a tree node or an associative array - * - * @param Varien_Data_Tree_Node|array $node - * @param int $level - * @return array - */ #[\Override] protected function _getNodeJson($node, $level = 0) { @@ -158,11 +165,22 @@ protected function _getNodeJson($node, $level = 0) if (in_array($node->getId(), $this->getSelectedCategories())) { $item['checked'] = true; } - $item['is_anchor'] = (int) $node->getIsAnchor(); + if ($this->getIsAnchorOnly() && !$node->getIsAnchor()) { + $item['selectable'] = false; + } + $item['is_anchor'] = (bool) $node->getIsAnchor(); $item['url_key'] = $node->getData('url_key'); return $item; } + #[\Override] + protected function _isParentSelectedCategory($node) + { + $allChildrenIds = array_keys($node->getAllChildNodes()); + $selectedChildren = array_intersect($this->getSelectedCategories(), $allChildrenIds); + return count($selectedChildren) > 0; + } + /** * Adds some extra params to categories collection * @@ -177,14 +195,15 @@ public function getCategoryCollection() /** * Tree JSON source URL * + * @param null $expanded deprecated * @return string */ #[\Override] public function getLoadTreeUrl($expanded = null) { return $this->getUrl('*/catalog_category_widget/categoriesJson', [ - '_current' => true, 'uniq_id' => $this->getId(), + 'is_anchor_only' => $this->getIsAnchorOnly(), 'use_massaction' => $this->getUseMassaction(), ]); } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main.php index 328c91ea9..6ac8bbb46 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -19,30 +19,22 @@ */ class Mage_Adminhtml_Block_Catalog_Product_Attribute_Set_Main extends Mage_Adminhtml_Block_Template { - /** - * Initialize template - */ #[\Override] protected function _construct() { + Mage::helper('core/js')->addTranslateData([ + 'All products of this set will be deleted! Type "confirm" to proceed.', + 'Cannot delete group. Please move configurable attributes to another group and try again.', + 'Cannot unassign configurable attribute', + ], 'catalog'); + $this->setTemplate('catalog/product/attribute/set/main.phtml'); + $this->setIsReadOnly(false); } - /** - * Prepare Global Layout - * - * @return $this - */ #[\Override] protected function _prepareLayout() { - $setId = $this->_getSetId(); - - $this->setChild( - 'group_tree', - $this->getLayout()->createBlock('adminhtml/catalog_product_attribute_set_main_tree_group'), - ); - $this->setChild( 'edit_set_form', $this->getLayout()->createBlock('adminhtml/catalog_product_attribute_set_main_formset'), @@ -51,17 +43,27 @@ protected function _prepareLayout() $this->setChild( 'delete_group_button', $this->getLayout()->createBlock('adminhtml/widget_button')->setData([ - 'label' => Mage::helper('catalog')->__('Delete Selected Group'), - 'onclick' => 'editSet.submit();', + 'id' => 'delete-group-button', + 'label' => Mage::helper('catalog')->__('Delete'), 'class' => 'delete', + 'disabled' => true, + ]), + ); + + $this->setChild( + 'rename_button', + $this->getLayout()->createBlock('adminhtml/widget_button')->setData([ + 'id' => 'rename-group-button', + 'label' => Mage::helper('catalog')->__('Rename'), + 'disabled' => true, ]), ); $this->setChild( 'add_group_button', $this->getLayout()->createBlock('adminhtml/widget_button')->setData([ - 'label' => Mage::helper('catalog')->__('Add New'), - 'onclick' => 'editSet.addGroup();', + 'id' => 'add-group-button', + 'label' => Mage::helper('catalog')->__('Add'), 'class' => 'add', ]), ); @@ -86,8 +88,8 @@ protected function _prepareLayout() $this->setChild( 'save_button', $this->getLayout()->createBlock('adminhtml/widget_button')->setData([ + 'id' => 'save-button', 'label' => Mage::helper('catalog')->__('Save Attribute Set'), - 'onclick' => 'editSet.save();', 'class' => 'save', ]), ); @@ -95,23 +97,12 @@ protected function _prepareLayout() $this->setChild( 'delete_button', $this->getLayout()->createBlock('adminhtml/widget_button')->setData([ + 'id' => 'delete-button', 'label' => Mage::helper('catalog')->__('Delete Attribute Set'), - 'onclick' => Mage::helper('core/js')->getDeleteConfirmJs( - $this->getUrlSecure('*/*/delete', ['id' => $setId]), - Mage::helper('catalog')->__('All products of this set will be deleted! Are you sure you want to delete this attribute set?'), - ), 'class' => 'delete', ]), ); - $this->setChild( - 'rename_button', - $this->getLayout()->createBlock('adminhtml/widget_button')->setData([ - 'label' => Mage::helper('catalog')->__('New Set Name'), - 'onclick' => 'editSet.rename()', - ]), - ); - return parent::_prepareLayout(); } @@ -119,10 +110,11 @@ protected function _prepareLayout() * Retrieve Attribute Set Group Tree HTML * * @return string + * @deprecated */ public function getGroupTreeHtml() { - return $this->getChildHtml('group_tree'); + return ''; } /** @@ -132,6 +124,9 @@ public function getGroupTreeHtml() */ public function getSetFormHtml() { + if ($this->getIsReadOnly()) { + $this->getChild('edit_set_form')->setIsReadOnly(true); + } return $this->getChildHtml('edit_set_form'); } @@ -149,20 +144,27 @@ protected function _getHeader() * Retrieve Attribute Set Save URL * * @return string + * @deprecated use self::getSaveUrl() */ public function getMoveUrl() { - return $this->getUrl('*/catalog_product_set/save', ['id' => $this->_getSetId()]); + return $this->getSaveUrl(); } /** - * Retrieve Attribute Set Group Save URL - * - * @return string + * Retrieve Attribute Set Save URL */ - public function getGroupUrl() + public function getSaveUrl(): string { - return $this->getUrl('*/catalog_product_group/save', ['id' => $this->_getSetId()]); + return $this->getUrl('*/*/save', ['id' => $this->_getSetId()]); + } + + /** + * Retrieve Attribute Set Delete URL + */ + public function getDeleteUrl(): string + { + return $this->getUrl('*/*/delete', ['id' => $this->_getSetId()]); } /** @@ -187,12 +189,13 @@ public function getGroupTreeJson() /** @var Mage_Eav_Model_Entity_Attribute_Group $node */ foreach ($groups as $node) { - $item = []; - $item['text'] = $node->getAttributeGroupName(); - $item['id'] = $node->getAttributeGroupId(); - $item['cls'] = 'folder'; - $item['allowDrop'] = true; - $item['allowDrag'] = true; + $item = [ + 'text' => $node->getAttributeGroupName(), + 'id' => $node->getAttributeGroupId(), + 'type' => 'folder', + 'allowDrop' => true, + 'allowDrag' => true, + ]; $nodeChildren = Mage::getResourceModel('catalog/product_attribute_collection') ->setAttributeGroupFilter($node->getId()) @@ -202,17 +205,26 @@ public function getGroupTreeJson() if ($nodeChildren->getSize() > 0) { $item['children'] = []; + /** @var Mage_Eav_Model_Entity_Attribute $child */ foreach ($nodeChildren->getItems() as $child) { - /** @var Mage_Eav_Model_Entity_Attribute $child */ + $isUserDefined = (bool) $child->getIsUserDefined(); + $isConfigurable = in_array($child->getAttributeId(), $configurable); + + $icon = match (true) { + !$isUserDefined => 'system-leaf', + $isConfigurable => 'configurable', + default => 'leaf', + }; + $attr = [ 'text' => $child->getAttributeCode(), 'id' => $child->getAttributeId(), - 'cls' => (!$child->getIsUserDefined()) ? 'system-leaf' : 'leaf', + 'cls' => $icon, 'allowDrop' => false, 'allowDrag' => true, - 'leaf' => true, - 'is_user_defined' => $child->getIsUserDefined(), - 'is_configurable' => (int) in_array($child->getAttributeId(), $configurable), + 'selectable' => false, + 'is_user_defined' => $isUserDefined, + 'is_configurable' => $isConfigurable, 'entity_id' => $child->getEntityAttributeId(), ]; @@ -298,6 +310,9 @@ public function getBackButtonHtml() */ public function getResetButtonHtml() { + if ($this->getIsReadOnly()) { + return ''; + } return $this->getChildHtml('reset_button'); } @@ -308,6 +323,9 @@ public function getResetButtonHtml() */ public function getSaveButtonHtml() { + if ($this->getIsReadOnly()) { + return ''; + } return $this->getChildHtml('save_button'); } @@ -318,7 +336,7 @@ public function getSaveButtonHtml() */ public function getDeleteButtonHtml() { - if ($this->getIsCurrentSetDefault()) { + if ($this->getIsCurrentSetDefault() || $this->getIsReadOnly()) { return ''; } return $this->getChildHtml('delete_button'); @@ -403,11 +421,6 @@ protected function _getSetData() return $this->_getAttributeSet(); } - /** - * Prepare HTML - * - * @return string - */ #[\Override] protected function _toHtml() { diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php index 09cb17cd0..353db24af 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -17,10 +17,6 @@ */ class Mage_Adminhtml_Block_Catalog_Product_Attribute_Set_Main_Formset extends Mage_Adminhtml_Block_Widget_Form { - /** - * Prepares attribute set form - * - */ #[\Override] protected function _prepareForm() { @@ -68,4 +64,16 @@ protected function _prepareForm() $this->setForm($form); return $this; } + + #[\Override] + protected function _initFormValues() + { + if ($this->getIsReadOnly()) { + $fieldset = $this->getForm()->getElement('set_name'); + foreach ($fieldset->getElements() as $element) { + $element->setDisabled(true); + } + } + return $this; + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Tree/Attribute.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Tree/Attribute.php deleted file mode 100644 index d616c3434..000000000 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Tree/Attribute.php +++ /dev/null @@ -1,25 +0,0 @@ -setTemplate('catalog/product/attribute/set/main/tree/attribute.phtml'); - } -} diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Tree/Group.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Tree/Group.php deleted file mode 100644 index 4f41a809d..000000000 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Tree/Group.php +++ /dev/null @@ -1,25 +0,0 @@ -setTemplate('catalog/product/attribute/set/main/tree/group.phtml'); - } -} diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php index d572ba4a0..329a7b540 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -17,14 +17,15 @@ * @category Mage * @package Mage_Adminhtml */ -class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Categories extends Mage_Adminhtml_Block_Catalog_Category_Tree +class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Categories extends Mage_Adminhtml_Block_Catalog_Category_Abstract { - protected $_categoryIds; - protected $_selectedNodes = null; - /** - * Specify template to use + * Cache for selected tree nodes + * + * list */ + protected $_selectedNodes = null; + public function __construct() { parent::__construct(); @@ -85,90 +86,29 @@ public function getRootNode() return $root; } - /** - * Returns root node - * - * @param Mage_Catalog_Model_Category|null $parentNodeCategory - * @param int $recursionLevel - * @return Varien_Data_Tree_Node - */ #[\Override] - public function getRoot($parentNodeCategory = null, $recursionLevel = 3) + public function getRoot($parentNodeCategory = null, $recursionLevel = null) { - if (!is_null($parentNodeCategory) && $parentNodeCategory->getId()) { - return $this->getNode($parentNodeCategory, $recursionLevel); - } - $root = Mage::registry('root'); - if (is_null($root)) { - $storeId = (int) $this->getRequest()->getParam('store'); - - if ($storeId) { - $store = Mage::app()->getStore($storeId); - $rootId = $store->getRootCategoryId(); - } else { - $rootId = Mage_Catalog_Model_Category::TREE_ROOT_ID; - } - - $ids = $this->getSelectedCategoriesPathIds($rootId); - $tree = Mage::getResourceSingleton('catalog/category_tree') - ->loadByIds($ids, false, false); - - if ($this->getCategory()) { - $tree->loadEnsuredNodes($this->getCategory(), $tree->getNodeById($rootId)); - } - - $tree->addCollectionData($this->getCategoryCollection()); - - $root = $tree->getNodeById($rootId); - - if ($root && $rootId != Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $root->setIsVisible(true); - if ($this->isReadonly()) { - $root->setDisabled(true); - } - } elseif ($root && $root->getId() == Mage_Catalog_Model_Category::TREE_ROOT_ID) { - $root->setName(Mage::helper('catalog')->__('Root')); - } - - Mage::register('root', $root); + if ($parentNodeCategory === null && $this->getCategoryIds()) { + return $this->getRootByIds($this->getCategoryIds(), $recursionLevel); + } else { + return parent::getRoot($parentNodeCategory, $recursionLevel); } - - return $root; } - /** - * Returns array with configuration of current node - * - * @param Varien_Data_Tree_Node $node - * @param int $level How deep is the node in the tree - * @return array - */ #[\Override] protected function _getNodeJson($node, $level = 1) { $item = parent::_getNodeJson($node, $level); - - if ($this->_isParentSelectedCategory($node)) { - $item['expanded'] = true; - } - if (in_array($node->getId(), $this->getCategoryIds())) { $item['checked'] = true; } - if ($this->isReadonly()) { $item['disabled'] = true; } - return $item; } - /** - * Returns whether $node is a parent (not exactly direct) of a selected node - * - * @param Varien_Data_Tree_Node $node - * @return bool - */ #[\Override] protected function _isParentSelectedCategory($node) { @@ -209,22 +149,11 @@ protected function _getSelectedNodes() * * @param int $categoryId * @return string + * @deprecated use self::getTreeJson() */ public function getCategoryChildrenJson($categoryId) { - $category = Mage::getModel('catalog/category')->load($categoryId); - $node = $this->getRoot($category, 1)->getTree()->getNodeById($categoryId); - - if (!$node || !$node->hasChildren()) { - return '[]'; - } - - $children = []; - foreach ($node->getChildren() as $child) { - $children[] = $this->_getNodeJson($child); - } - - return Mage::helper('core')->jsonEncode($children); + return $this->getTreeJson($categoryId); } /** @@ -236,7 +165,7 @@ public function getCategoryChildrenJson($categoryId) #[\Override] public function getLoadTreeUrl($expanded = null) { - return $this->getUrl('*/*/categoriesJson', ['_current' => true]); + return $this->getUrl('*/*/categoriesJson', ['_current' => ['id', 'store']]); } /** @@ -244,6 +173,7 @@ public function getLoadTreeUrl($expanded = null) * * @param mixed $rootId Root category Id for context * @return array + * @deprecated Mage_Catalog_Model_Resource_Category_Tree::loadByIds() already loads parent ids */ public function getSelectedCategoriesPathIds($rootId = false) { diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php index 69f12805b..b774fb0e1 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -104,7 +104,7 @@ public function getRowClickCallback() { if (!$this->getUseMassaction()) { $chooserJsObject = $this->getId(); - return ' + return <<getJsObjectName(), $js); - return $js; + JS; } /** diff --git a/app/code/core/Mage/Adminhtml/Block/Permissions/Tab/Rolesedit.php b/app/code/core/Mage/Adminhtml/Block/Permissions/Tab/Rolesedit.php index 7db899af7..0f5400132 100644 --- a/app/code/core/Mage/Adminhtml/Block/Permissions/Tab/Rolesedit.php +++ b/app/code/core/Mage/Adminhtml/Block/Permissions/Tab/Rolesedit.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -200,7 +200,9 @@ protected function _getNodeJson($node, $level = 0) } } } - if (!empty($item['children'])) { + if (empty($item['children'])) { + unset($item['children']); + } else { usort($item['children'], [$this, '_sortTree']); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Review/Add.php b/app/code/core/Mage/Adminhtml/Block/Review/Add.php index 4680d9417..a5591252d 100644 --- a/app/code/core/Mage/Adminhtml/Block/Review/Add.php +++ b/app/code/core/Mage/Adminhtml/Block/Review/Add.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -31,74 +31,15 @@ public function __construct() $this->_updateButton('reset', 'id', 'reset_button'); - $this->_formScripts[] = ' - toggleParentVis("add_review_form"); - toggleVis("save_button"); - toggleVis("reset_button"); - '; - - $this->_formInitScripts[] = ' - //getUrl('*/*/ratingItems') . '", {parameters:params, evalScripts: true, onComplete:function(){ $(\'save_button\').disabled = false; } }); - }, - - reqSuccess :function(o) { - var response = Ext.util.JSON.decode(o.responseText); - if( response.error ) { - alert(response.message); - } else if( response.id ){ - $("product_id").value = response.id; - - $("product_name").innerHTML = \'\' + response.name.escapeHTML() + \'\'; - } else if( response.message ) { - alert(response.message); - } - } - } - }(); - - Event.observe(window, \'load\', function(){ - if ($("select_stores")) { - Event.observe($("select_stores"), \'change\', review.updateRating); - } - }); - //]]> - '; + $this->_formInitScripts[] = <<getUrl('*/*/ratingItems')}', + productEditUrl: '{$this->getUrl('*/catalog_product/edit')}', + }); + JS; + $this->_formScripts[] = <<addFieldset('add_review_form', ['legend' => Mage::helper('review')->__('Review Details')]); + $fieldset = $form->addFieldset('add_review_form', ['legend' => Mage::helper('review')->__('Review Details'), 'class' => 'fieldset-wide']); $fieldset->addField('product_name', 'note', [ 'label' => Mage::helper('review')->__('Product'), @@ -79,7 +79,7 @@ protected function _prepareForm() 'name' => 'detail', 'title' => Mage::helper('review')->__('Review'), 'label' => Mage::helper('review')->__('Review'), - 'style' => 'height: 600px;', + 'style' => 'height:24em;', 'required' => true, ]); @@ -87,11 +87,6 @@ protected function _prepareForm() 'name' => 'product_id', ]); - /*$gridFieldset = $form->addFieldset('add_review_grid', array('legend' => Mage::helper('review')->__('Please select a product'))); - $gridFieldset->addField('products_grid', 'note', array( - 'text' => $this->getLayout()->createBlock('adminhtml/review_product_grid')->toHtml(), - ));*/ - $form->setMethod('post'); $form->setUseContainer(true); $form->setId('edit_form'); diff --git a/app/code/core/Mage/Adminhtml/Block/Review/Edit.php b/app/code/core/Mage/Adminhtml/Block/Review/Edit.php index 89774f76c..b6a42a112 100644 --- a/app/code/core/Mage/Adminhtml/Block/Review/Edit.php +++ b/app/code/core/Mage/Adminhtml/Block/Review/Edit.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -35,10 +35,9 @@ public function __construct() 'back', 'onclick', Mage::helper('core/js')->getSetLocationJs( - $this->getUrl( - '*/catalog_product/edit', - ['id' => $this->getRequest()->getParam('productId', false)], - ), + $this->getUrl('*/catalog_product/edit', [ + 'id' => $this->getRequest()->getParam('productId', false), + ]), ), ); } @@ -48,10 +47,9 @@ public function __construct() 'back', 'onclick', Mage::helper('core/js')->getSetLocationJs( - $this->getUrl( - '*/customer/edit', - ['id' => $this->getRequest()->getParam('customerId', false)], - ), + $this->getUrl('*/customer/edit', [ + 'id' => $this->getRequest()->getParam('customerId', false), + ]), ), ); } @@ -66,13 +64,10 @@ public function __construct() 'delete', 'onclick', Mage::helper('core/js')->getDeleteConfirmJs( - $this->getUrl( - '*/*/delete', - [ - $this->_objectId => $this->getRequest()->getParam($this->_objectId), - 'ret' => 'pending', - ], - ), + $this->getUrl('*/*/delete', [ + $this->_objectId => $this->getRequest()->getParam($this->_objectId), + 'ret' => 'pending', + ]), ), ); Mage::register('ret', 'pending'); @@ -84,29 +79,11 @@ public function __construct() Mage::register('review_data', $reviewData); } - $this->_formInitScripts[] = ' - var review = { - updateRating: function() { - elements = [ - $("select_stores"), - $("rating_detail").getElementsBySelector("input[type=\'radio\']") - ].flatten(); - $(\'save_button\').disabled = true; - new Ajax.Updater( - "rating_detail", - "' . $this->getUrl('*/*/ratingItems', ['_current' => true]) . '", - { - parameters:Form.serializeElements(elements), - evalScripts:true, - onComplete:function(){ $(\'save_button\').disabled = false; } - } - ); - } - } - Event.observe(window, \'load\', function(){ - Event.observe($("select_stores"), \'change\', review.updateRating); - }); - '; + $this->_formInitScripts[] = <<getUrl('*/*/ratingItems', ['_current' => true])}', + }); + JS; } /** diff --git a/app/code/core/Mage/Adminhtml/Block/Review/Product/Grid.php b/app/code/core/Mage/Adminhtml/Block/Review/Product/Grid.php index 8abbd75ef..a9b1555ba 100644 --- a/app/code/core/Mage/Adminhtml/Block/Review/Product/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Review/Product/Grid.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -23,7 +23,7 @@ public function __construct() { parent::__construct(); $this->setId('reviewProductGrid'); - $this->setRowClickCallback('review.gridRowClick'); + $this->setRowClickCallback('review.gridRowClick.bind(review)'); $this->setUseAjax(true); } diff --git a/app/code/core/Mage/Adminhtml/Block/Urlrewrite/Category/Tree.php b/app/code/core/Mage/Adminhtml/Block/Urlrewrite/Category/Tree.php index 78e7a00d3..ca64eb88f 100644 --- a/app/code/core/Mage/Adminhtml/Block/Urlrewrite/Category/Tree.php +++ b/app/code/core/Mage/Adminhtml/Block/Urlrewrite/Category/Tree.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -34,61 +35,78 @@ public function __construct() $this->setTemplate('urlrewrite/categories.phtml'); } + /** + * Return array with category IDs which the product is assigned to + * + * @return array + */ + protected function getCategoryIds() + { + $productId = Mage::app()->getRequest()->getParam('product'); + if ($productId && $this->_allowedCategoryIds === null) { + $this->_allowedCategoryIds = Mage::getModel('catalog/product')->setId($productId)->getCategoryIds(); + } + return $this->_allowedCategoryIds; + } + /** * Get categories tree as recursive array * * @param int $parentId * @param bool $asJson * @param int $recursionLevel - * @return array|string + * @return ($asJson is true ? string : array) */ - public function getTreeArray($parentId = null, $asJson = false, $recursionLevel = 3) + public function getTreeArray($parentId = null, $asJson = false, $recursionLevel = null) { - $productId = Mage::app()->getRequest()->getParam('product'); - if ($productId) { - $product = Mage::getModel('catalog/product')->setId($productId); - $this->_allowedCategoryIds = $product->getCategoryIds(); - unset($product); + if ($recursionLevel !== null) { + $this->setRecursionLevel($recursionLevel); } - $result = []; - if ($parentId) { - $category = Mage::getModel('catalog/category')->load($parentId); - if (!empty($category)) { - $tree = $this->_getNodesArray($this->getNode($category, $recursionLevel)); - if (!empty($tree) && !empty($tree['children'])) { - $result = $tree['children']; - } - } - } else { - $result = $this->_getNodesArray($this->getRoot(null, $recursionLevel)); - } + $result = $this->getTree($parentId); if ($asJson) { return Mage::helper('core')->jsonEncode($result); } - $this->_allowedCategoryIds = null; - return $result; } - /** - * Get categories collection - * - * @return Mage_Catalog_Model_Resource_Category_Collection - */ - public function getCategoryCollection() + #[\Override] + public function getRoot($parentNodeCategory = null, $recursionLevel = null) { - $collection = $this->_getData('category_collection'); - if (is_null($collection)) { - $collection = Mage::getModel('catalog/category')->getCollection() - ->addAttributeToSelect(['name', 'is_active']) - ->setLoadProductCount(true); - $this->setData('category_collection', $collection); + if ($parentNodeCategory === null && $this->getCategoryIds()) { + return $this->getRootByIds($this->getCategoryIds(), $recursionLevel); + } else { + return parent::getRoot($parentNodeCategory, $recursionLevel); } + } + + #[\Override] + protected function _getNodeJson($node, $level = 1) + { + $item = parent::_getNodeJson($node, $level); + $item['cls'] = str_replace('no-active-category', 'active-category', $item['cls']); + + $categoryIds = $this->getCategoryIds(); + if ($categoryIds !== null && !in_array($item['id'], $categoryIds)) { + $item['disabled'] = true; + } + if ($categoryIds === null && $node->getLevel() < 3) { + $item['expanded'] = true; + } + + return $item; + } - return $collection; + #[\Override] + protected function _isParentSelectedCategory($node) + { + if ($this->getCategoryIds() !== null) { + $children = array_keys($node->getAllChildNodes()); + return !empty(array_intersect($children, $this->getCategoryIds())); + } + return false; } /** @@ -96,42 +114,22 @@ public function getCategoryCollection() * * @param Varien_Data_Tree_Node $node * @return array + * @deprecated use self::_getNodeJson() */ protected function _getNodesArray($node) { - $result = [ - 'id' => (int) $node->getId(), - 'parent_id' => (int) $node->getParentId(), - 'children_count' => (int) $node->getChildrenCount(), - 'is_active' => (bool) $node->getIsActive(), - 'name' => $this->escapeHtml($node->getName()), - 'level' => (int) $node->getLevel(), - 'product_count' => (int) $node->getProductCount(), - ]; - - if (is_array($this->_allowedCategoryIds) && !in_array($result['id'], $this->_allowedCategoryIds)) { - $result['disabled'] = true; - } - - if ($node->hasChildren()) { - $result['children'] = []; - foreach ($node->getChildren() as $childNode) { - $result['children'][] = $this->_getNodesArray($childNode); - } - } - $result['cls'] = ($result['is_active'] ? '' : 'no-') . 'active-category'; - $result['expanded'] = (!empty($result['children'])); - - return $result; + return $this->_getNodeJson($node); } /** - * Get URL for categories tree ajax loader + * Returns URL for loading tree * + * @param null $expanded deprecated * @return string */ - public function getLoadTreeUrl() + #[\Override] + public function getLoadTreeUrl($expanded = null) { - return Mage::helper('adminhtml')->getUrl('*/*/categoriesJson'); + return $this->getUrl('*/*/categoriesJson', ['_current' => ['product']]); } } diff --git a/app/code/core/Mage/Adminhtml/controllers/Api/RoleController.php b/app/code/core/Mage/Adminhtml/controllers/Api/RoleController.php index ee0f759e8..52f9cf474 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Api/RoleController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Api/RoleController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -88,8 +88,6 @@ public function editRoleAction() } $this->_addBreadcrumb($breadCrumb, $breadCrumbTitle); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); - $this->_addLeft( $this->getLayout()->createBlock('adminhtml/api_editroles'), ); diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/Category/WidgetController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/Category/WidgetController.php index 83b56bd1f..bba2469d1 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/Category/WidgetController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/Category/WidgetController.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -39,15 +40,23 @@ public function chooserAction() */ public function categoriesJsonAction() { - if ($categoryId = (int) $this->getRequest()->getPost('id')) { + try { + $categoryId = (int) $this->getRequest()->getPost('id'); $category = Mage::getModel('catalog/category')->load($categoryId); - if ($category->getId()) { - Mage::register('category', $category); - Mage::register('current_category', $category); + + if (!$category->getId()) { + Mage::throwException(Mage::helper('catalog')->__('This category no longer exists.')); } + + Mage::register('category', $category); + Mage::register('current_category', $category); + + $this->getResponse()->setHeader('Content-type', 'application/json', true); $this->getResponse()->setBody( $this->_getCategoryTreeBlock()->getTreeJson($category), ); + } catch (Exception $e) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } @@ -55,7 +64,9 @@ protected function _getCategoryTreeBlock() { return $this->getLayout()->createBlock('adminhtml/catalog_category_widget_chooser', '', [ 'id' => $this->getRequest()->getParam('uniq_id'), + 'is_anchor_only' => $this->getRequest()->getParam('is_anchor_only', false), 'use_massaction' => $this->getRequest()->getParam('use_massaction', false), + 'selected_categories' => explode(',', $this->getRequest()->getParam('selected', '')), ]); } } diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php index af473a274..64d5a51b2 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -30,49 +30,50 @@ class Mage_Adminhtml_Catalog_CategoryController extends Mage_Adminhtml_Controlle * Root category can be returned, if inappropriate store/category is specified * * @param bool $getRootInstead - * @return Mage_Catalog_Model_Category|false + * @return ($getRootInstead is true ? Mage_Catalog_Model_Category : Mage_Catalog_Model_Category|false) */ protected function _initCategory($getRootInstead = false) { - $this->_title($this->__('Catalog')) - ->_title($this->__('Categories')) - ->_title($this->__('Manage Categories')); - - $categoryId = (int) $this->getRequest()->getParam('id', false); $storeId = (int) $this->getRequest()->getParam('store'); - $category = Mage::getModel('catalog/category'); - $category->setStoreId($storeId); + $categoryId = (int) $this->getRequest()->getParam('id', false); + + $category = Mage::getModel('catalog/category') + ->setStoreId($storeId); if ($categoryId) { $category->load($categoryId); - if ($storeId) { - $rootId = Mage::app()->getStore($storeId)->getRootCategoryId(); - if (!in_array($rootId, $category->getPathIds())) { - // load root category instead wrong one - if ($getRootInstead) { - $category->load($rootId); - } else { - $this->_redirect('*/*/', ['_current' => true, 'id' => null]); - return false; - } - } - } } - if ($activeTabId = (string) $this->getRequest()->getParam('active_tab_id')) { - Mage::getSingleton('admin/session')->setActiveTabId($activeTabId); + // If a store id was provided, ensure this category belongs to it + if ($categoryId && $storeId) { + if (!$category->getResource()->isInStore($category, $storeId)) { + if (!$getRootInstead) { + return false; + } + $rootId = Mage::app()->getStore($storeId)->getRootCategoryId(); + $category = Mage::getModel('catalog/category') + ->setStoreId($storeId) + ->load($rootId); + } } Mage::register('category', $category); Mage::register('current_category', $category); - Mage::getSingleton('cms/wysiwyg_config')->setStoreId($this->getRequest()->getParam('store')); + Mage::getSingleton('cms/wysiwyg_config')->setStoreId($storeId); return $category; } + /** * Catalog categories index action */ public function indexAction() { + $storeId = (int) $this->getRequest()->getParam('store'); + $store = $storeId + ? Mage::app()->getStore($storeId) + : Mage::app()->getWebsite(true)->getDefaultGroup()->getDefaultStore(); + + $this->getRequest()->setParam('id', $store->getRootCategoryId()); $this->_forward('edit'); } @@ -81,7 +82,6 @@ public function indexAction() */ public function addAction() { - Mage::getSingleton('admin/session')->unsActiveTabId(); $this->_forward('edit'); } @@ -90,86 +90,65 @@ public function addAction() */ public function editAction() { - $params['_current'] = true; - $redirect = false; - $storeId = (int) $this->getRequest()->getParam('store'); - $parentId = (int) $this->getRequest()->getParam('parent'); - $prevStoreId = Mage::getSingleton('admin/session') - ->getLastViewedStore(true); + $categoryId = (int) $this->getRequest()->getParam('id'); - if (!empty($prevStoreId) && !$this->getRequest()->getQuery('isAjax')) { - $params['store'] = $prevStoreId; - $redirect = true; - } + $category = $this->_initCategory(true); - $categoryId = (int) $this->getRequest()->getParam('id'); - $prevCategoryId = Mage::getSingleton('admin/session') - ->getLastEditedCategory(true); - - if ($prevCategoryId - && !$this->getRequest()->getQuery('isAjax') - && !$this->getRequest()->getParam('clear') - ) { - $this->getRequest()->setParam('id', $prevCategoryId); + try { + if (!$category->getId()) { + $parent = Mage::getModel('catalog/category') + ->load((int) $this->getRequest()->getParam('parent')); + if ($storeId && !$parent->getResource()->isInStore($parent, $storeId)) { + Mage::throwException(Mage::helper('catalog')->__('Parent category was not found.')); + } + $category->setPath($parent->getPath()); + } + } catch (Mage_Core_Exception $e) { + $error = $e->getMessage(); + } catch (Exception $e) { + $error = $e->getMessage(); + Mage::logException($e); } - if ($redirect) { - $this->_redirect('*/*/edit', $params); + if (isset($error)) { + if ($this->getRequest()->isAjax()) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => $error]); + } else { + Mage::getSingleton('adminhtml/session')->addError($error); + $this->getResponse()->setRedirect($this->getUrl('*/*/edit', ['_current' => true])); + } return; } - if ($storeId && !$categoryId && !$parentId) { - $store = Mage::app()->getStore($storeId); - $prevCategoryId = (int) $store->getRootCategoryId(); - $this->getRequest()->setParam('id', $prevCategoryId); - } + // Restore saved data in case of exception during save + $data = Mage::getSingleton('adminhtml/session')->getCategoryData(true); + $category->addData($data['general'] ?? []); - if (!($category = $this->_initCategory(true))) { - return; - } + $this->loadLayout(); - $this->_title($categoryId ? $category->getName() : $this->__('New Category')); + $this->_title($this->__('Catalog')) + ->_title($this->__('Categories')) + ->_title($this->__('Manage Categories')) + ->_title($category->getId() ? $category->getName() : $this->__('New Category')); - /** - * Check if we have data in session (if duering category save was exceprion) - */ - $data = Mage::getSingleton('adminhtml/session')->getCategoryData(true); - if (isset($data['general'])) { - $category->addData($data['general']); - } + $this->_setActiveMenu('catalog/categories'); - /** - * Build response for ajax request - */ - if ($this->getRequest()->getQuery('isAjax')) { - // prepare breadcrumbs of selected category, if any - $breadcrumbsPath = $category->getPath(); - if (empty($breadcrumbsPath)) { - // but if no category, and it is deleted - prepare breadcrumbs from path, saved in session - $breadcrumbsPath = Mage::getSingleton('admin/session')->getDeletedPath(true); - if (!empty($breadcrumbsPath)) { - $breadcrumbsPath = explode('/', $breadcrumbsPath); - // no need to get parent breadcrumbs if deleting category level 1 - if (count($breadcrumbsPath) <= 1) { - $breadcrumbsPath = ''; - } else { - array_pop($breadcrumbsPath); - $breadcrumbsPath = implode('/', $breadcrumbsPath); - } - } - } + $this->_addBreadcrumb( + Mage::helper('catalog')->__('Manage Catalog Categories'), + Mage::helper('catalog')->__('Manage Categories'), + ); + + if ($wysiwygBlock = $this->getLayout()->getBlock('catalog.wysiwyg.js')) { + $wysiwygBlock->setStoreId($storeId); + } - Mage::getSingleton('admin/session') - ->setLastViewedStore($this->getRequest()->getParam('store')); - Mage::getSingleton('admin/session') - ->setLastEditedCategory($category->getId()); - $this->loadLayout(); + if ($this->getRequest()->isAjax()) { + $this->_renderTitles(); $eventResponse = new Varien_Object([ - 'content' => $this->getLayout()->getBlock('category.edit')->getFormHtml() - . $this->getLayout()->getBlock('category.tree') - ->getBreadcrumbsJavascript($breadcrumbsPath, 'editingCategoryBreadcrumbs'), + 'title' => implode(' / ', array_reverse($this->_titles)), + 'content' => $this->getLayout()->getBlock('category.edit')->getFormHtml(), 'messages' => $this->getLayout()->getMessagesBlock()->getGroupedHtml(), ]); @@ -178,34 +157,15 @@ public function editAction() 'controller' => $this, ]); - $this->getResponse()->setBody( - Mage::helper('core')->jsonEncode($eventResponse->getData()), - ); - + $this->getResponse()->setBodyJson($eventResponse->getData()); return; } - $this->loadLayout(); - $this->_setActiveMenu('catalog/categories'); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true) - ->setContainerCssClass('catalog-categories'); - - $this->_addBreadcrumb( - Mage::helper('catalog')->__('Manage Catalog Categories'), - Mage::helper('catalog')->__('Manage Categories'), - ); - - $block = $this->getLayout()->getBlock('catalog.wysiwyg.js'); - if ($block) { - $block->setStoreId($storeId); - } - $this->renderLayout(); } /** * WYSIWYG editor action for ajax request - * */ public function wysiwygAction() { @@ -227,22 +187,21 @@ public function wysiwygAction() */ public function categoriesJsonAction() { - if ($this->getRequest()->getParam('expand_all')) { - Mage::getSingleton('admin/session')->setIsTreeWasExpanded(true); - } else { - Mage::getSingleton('admin/session')->setIsTreeWasExpanded(false); - } - if ($categoryId = (int) $this->getRequest()->getPost('id')) { - $this->getRequest()->setParam('id', $categoryId); + $recursionLevel = $this->getRequest()->getParam('expand_all') ? 0 : null; + $categoryId = (int) $this->getRequest()->getPost('id'); - if (!$category = $this->_initCategory()) { - return; - } - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/catalog_category_tree') - ->getTreeJson($category), - ); + $category = $this->_initCategory(); + if (!$category || !$category->getId()) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => Mage::helper('catalog')->__('Category was not found.')]); + return; } + + $this->getResponse()->setHeader('Content-type', 'application/json', true); + $this->getResponse()->setBody( + $this->getLayout()->createBlock('adminhtml/catalog_category_tree') + ->setRecursionLevel($recursionLevel) + ->getTreeJson($category), + ); } /** @@ -250,61 +209,53 @@ public function categoriesJsonAction() */ public function saveAction() { - if (!$category = $this->_initCategory()) { - return; - } - - $storeId = $this->getRequest()->getParam('store'); - $refreshTree = 'false'; - if ($data = $this->getRequest()->getPost()) { - if (isset($data['general']['path'])) { - unset($data['general']['path']); + try { + $storeId = (int) $this->getRequest()->getParam('store'); + if (!$data = $this->getRequest()->getPost()) { + Mage::throwException(Mage::helper('catalog')->__('Unable to complete this request.')); + } + if (!$category = $this->_initCategory()) { + Mage::throwException(Mage::helper('catalog')->__('Category was not found.')); } + + // Add all POST data, except path which may have become stale if category was moved + unset($data['general']['path']); $category->addData($data['general']); + + // If new category, set path to parent and other defaults if (!$category->getId()) { - $parentId = $this->getRequest()->getParam('parent'); - if (!$parentId) { - if ($storeId) { - $parentId = Mage::app()->getStore($storeId)->getRootCategoryId(); - } else { - $parentId = Mage_Catalog_Model_Category::TREE_ROOT_ID; - } + $parent = Mage::getModel('catalog/category') + ->load((int) $this->getRequest()->getParam('parent')); + if ($storeId && !$parent->getResource()->isInStore($parent, $storeId)) { + Mage::throwException(Mage::helper('catalog')->__('Parent category was not found.')); } - $parentCategory = Mage::getModel('catalog/category')->load($parentId); - $category->setPath($parentCategory->getPath()); + $category + ->setPath($parent->getPath()) + ->setStoreId(Mage_Core_Model_App::ADMIN_STORE_ID) + ->setAttributeSetId($category->getDefaultAttributeSetId()); } - /** - * Check "Use Default Value" checkboxes values - */ + // Check "Use Default Value" checkboxes values if ($useDefaults = $this->getRequest()->getPost('use_default')) { foreach ($useDefaults as $attributeCode) { $category->setData($attributeCode, false); } } - /** - * Process "Use Config Settings" checkboxes - */ + // Process "Use Config Settings" checkboxes if ($useConfig = $this->getRequest()->getPost('use_config')) { foreach ($useConfig as $attributeCode) { $category->setData($attributeCode, null); } } - /** - * Create Permanent Redirect for old URL key - */ + // Create Permanent Redirect for old URL key if ($category->getId() && isset($data['general']['url_key_create_redirect'])) { - // && $category->getOrigData('url_key') != $category->getData('url_key') $category->setData('save_rewrites_history', (bool) $data['general']['url_key_create_redirect']); } - $category->setAttributeSetId($category->getDefaultAttributeSetId()); - if (isset($data['category_products']) && - !$category->getProductsReadonly() - ) { + if (isset($data['category_products']) && !$category->getProductsReadonly()) { $products = Mage::helper('core/string')->parseQueryStr($data['category_products']); $category->setPostedProducts($products); } @@ -314,72 +265,98 @@ public function saveAction() 'request' => $this->getRequest(), ]); - /** - * Proceed with $_POST['use_config'] - * set into category model for processing through validation - */ + // Set $_POST['use_config'] into category model for validation processing $category->setData('use_post_data_config', $this->getRequest()->getPost('use_config')); - try { - $validate = $category->validate(); - if ($validate !== true) { - foreach ($validate as $code => $error) { - if ($error === true) { - Mage::throwException(Mage::helper('catalog')->__('Attribute "%s" is required.', $category->getResource()->getAttribute($code)->getFrontend()->getLabel())); - } else { - Mage::throwException($error); - } + $validate = $category->validate(); + if ($validate !== true) { + foreach ($validate as $code => $error) { + if ($error === true) { + $attributeLabel = $category->getResource()->getAttribute($code)->getFrontend()->getLabel(); + Mage::throwException(Mage::helper('catalog')->__('Attribute "%s" is required.', $attributeLabel)); + } else { + Mage::throwException($error); } } + } + + // Unset $_POST['use_config'] before save + $category->unsetData('use_post_data_config'); + + $category->save(); + + // Add success message, will be displayed when frontend loads parent's edit form + Mage::getSingleton('adminhtml/session')->addSuccess(Mage::helper('catalog')->__('The category has been saved.')); + + if ($this->getRequest()->isAjax()) { + $this->getResponse()->setBodyJson([ + 'success' => true, + 'category_id' => (int) $category->getId(), + ]); + return; + } + + $this->getResponse()->setRedirect( + $this->getUrl('*/*/edit', ['_current' => true, 'parent' => null, 'id' => $category->getId()]), + ); + + } catch (Mage_Core_Exception $e) { + $error = $e->getMessage(); + } catch (Exception $e) { + $error = Mage::helper('catalog')->__('Internal Error'); + Mage::logException($e); + } - /** - * Unset $_POST['use_config'] before save - */ - $category->unsetData('use_post_data_config'); - - $category->save(); - Mage::getSingleton('adminhtml/session')->addSuccess(Mage::helper('catalog')->__('The category has been saved.')); - $refreshTree = 'true'; - } catch (Exception $e) { - $this->_getSession()->addError($e->getMessage()) - ->setCategoryData($data); - $refreshTree = 'false'; + if (isset($error)) { + $error = Mage::helper('catalog')->__('Category save error: %s', $error); + if ($this->getRequest()->isAjax()) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => $error]); + return; } + + Mage::getSingleton('adminhtml/session')->setCategoryData($data); + Mage::getSingleton('adminhtml/session')->addError($error); + $this->getResponse()->setRedirect($this->getUrl('*/*/edit', ['_current' => true])); } - $url = $this->getUrl('*/*/edit', ['_current' => true, 'id' => $category->getId()]); - $this->getResponse()->setBody( - '', - ); } /** - * Move category action + * Move category action (AJAX) */ public function moveAction() { - $category = $this->_initCategory(); - if (!$category) { - $this->getResponse()->setBody(Mage::helper('catalog')->__('Category move error')); - return; - } - /** - * New parent category identifier - */ - $parentNodeId = $this->getRequest()->getPost('pid', false); - /** - * Category id after which we have put our category - */ - $prevNodeId = $this->getRequest()->getPost('aid', false); - $category->setData('save_rewrites_history', Mage::helper('catalog')->shouldSaveUrlRewritesHistory()); try { + $category = $this->_initCategory(); + if (!$category || !$category->getId()) { + Mage::throwException( + Mage::helper('catalog')->__('Category was not found.'), + ); + } + + // New parent category identifier + $parentNodeId = $this->getRequest()->getPost('pid', false); + + // Category id after which we have put our category + $prevNodeId = $this->getRequest()->getPost('aid', false); + + $category->setData('save_rewrites_history', Mage::helper('catalog')->shouldSaveUrlRewritesHistory()); + $category->move($parentNodeId, $prevNodeId); - $this->getResponse()->setBody('SUCCESS'); + + $this->getResponse()->setBodyJson([ + 'success' => true, + ]); } catch (Mage_Core_Exception $e) { - $this->getResponse()->setBody($e->getMessage()); + $error = $e->getMessage(); } catch (Exception $e) { - $this->getResponse()->setBody(Mage::helper('catalog')->__('Category move error %s', $e)); + $error = Mage::helper('catalog')->__('Internal Error'); Mage::logException($e); } + + if (isset($error)) { + $error = Mage::helper('catalog')->__('Category move error: %s', $error); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $error]); + } } /** @@ -387,26 +364,52 @@ public function moveAction() */ public function deleteAction() { - if ($id = (int) $this->getRequest()->getParam('id')) { - try { - $category = Mage::getModel('catalog/category')->load($id); - Mage::dispatchEvent('catalog_controller_category_delete', ['category' => $category]); + try { + $category = $this->_initCategory(); + if (!$category || !$category->getId()) { + Mage::throwException( + Mage::helper('catalog')->__('Category was not found.'), + ); + } - Mage::getSingleton('admin/session')->setDeletedPath($category->getPath()); + Mage::dispatchEvent('catalog_controller_category_delete', ['category' => $category]); + $category->delete(); - $category->delete(); - Mage::getSingleton('adminhtml/session')->addSuccess(Mage::helper('catalog')->__('The category has been deleted.')); - } catch (Mage_Core_Exception $e) { - Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); - $this->getResponse()->setRedirect($this->getUrl('*/*/edit', ['_current' => true])); + // Add success message, will be displayed when frontend loads parent's edit form + Mage::getSingleton('adminhtml/session')->addSuccess( + Mage::helper('catalog')->__('The category has been deleted.'), + ); + + if ($this->getRequest()->isAjax()) { + $this->getResponse()->setBodyJson([ + 'success' => true, + 'category_id' => (int) $category->getId(), + 'parent_id' => (int) $category->getParentId(), + ]); return; - } catch (Exception $e) { - Mage::getSingleton('adminhtml/session')->addError(Mage::helper('catalog')->__('An error occurred while trying to delete the category.')); - $this->getResponse()->setRedirect($this->getUrl('*/*/edit', ['_current' => true])); + } + + $this->getResponse()->setRedirect( + $this->getUrl('*/*/edit', ['_current' => true, 'form_key' => null, 'id' => $category->getParentId()]), + ); + + } catch (Mage_Core_Exception $e) { + $error = $e->getMessage(); + } catch (Exception $e) { + $error = Mage::helper('catalog')->__('Internal Error'); + Mage::logException($e); + } + + if (isset($error)) { + $error = Mage::helper('catalog')->__('Category delete error: %s', $error); + if ($this->getRequest()->isAjax()) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => $error]); return; } + + Mage::getSingleton('adminhtml/session')->addError($error); + $this->getResponse()->setRedirect($this->getUrl('*/*/edit', ['_current' => true, 'form_key' => null])); } - $this->getResponse()->setRedirect($this->getUrl('*/*/', ['_current' => true, 'id' => null])); } /** @@ -415,12 +418,9 @@ public function deleteAction() */ public function gridAction() { - if (!$category = $this->_initCategory(true)) { - return; - } + $this->_initCategory(true); $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/catalog_category_tab_product', 'category.product.grid') - ->toHtml(), + $this->getLayout()->createBlock('adminhtml/catalog_category_tab_product', 'category.product.grid')->toHtml(), ); } @@ -432,47 +432,46 @@ public function treeAction() { $storeId = (int) $this->getRequest()->getParam('store'); $categoryId = (int) $this->getRequest()->getParam('id'); + $recursionLevel = $this->getRequest()->getParam('expand_all') ? 0 : null; - if ($storeId) { - if (!$categoryId) { - $store = Mage::app()->getStore($storeId); - $rootId = $store->getRootCategoryId(); - $this->getRequest()->setParam('id', $rootId); - } + if ($storeId && !$categoryId) { + $rootId = Mage::app()->getStore($storeId)->getRootCategoryId(); + $this->getRequest()->setParam('id', $rootId); } $category = $this->_initCategory(true); /** @var Mage_Adminhtml_Block_Catalog_Category_Tree $block */ $block = $this->getLayout()->createBlock('adminhtml/catalog_category_tree'); - $root = $block->getRoot(); - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode([ - 'data' => $block->getTree(), - 'parameters' => [ - 'text' => $block->buildNodeName($root), - 'draggable' => false, - 'allowDrop' => ($root->getIsVisible()) ? true : false, - 'id' => (int) $root->getId(), - 'expanded' => (int) $block->getIsWasExpanded(), - 'store_id' => (int) $block->getStore()->getId(), - 'category_id' => (int) $category->getId(), - 'root_visible' => (int) $root->getIsVisible(), - ]])); + $block->setRecursionLevel($recursionLevel); + + $this->getResponse()->setBodyJson($block->getRootTreeParameters()); } /** - * Build response for refresh input element 'path' in form + * Build response for refresh input element 'path' in form (AJAX) */ public function refreshPathAction() { - if ($id = (int) $this->getRequest()->getParam('id')) { - $category = Mage::getModel('catalog/category')->load($id); - $this->getResponse()->setBody( - Mage::helper('core')->jsonEncode([ - 'id' => $id, - 'path' => $category->getPath(), - ]), - ); + try { + $category = $this->_initCategory(); + if (!$category || !$category->getId()) { + Mage::throwException( + Mage::helper('catalog')->__('Category was not found.'), + ); + } + $this->getResponse()->setBodyJson([ + 'id' => (int) $category->getId(), + 'path' => $category->getPath(), + ]); + } catch (Mage_Core_Exception $e) { + $error = $e->getMessage(); + } catch (Exception $e) { + $error = Mage::helper('catalog')->__('Internal Error'); + Mage::logException($e); + } + if (isset($error)) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => $error]); } } diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/GroupController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/GroupController.php deleted file mode 100644 index b5fc60a1e..000000000 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/GroupController.php +++ /dev/null @@ -1,43 +0,0 @@ -setAttributeGroupName($this->getRequest()->getParam('attribute_group_name')) - ->setAttributeSetId($this->getRequest()->getParam('attribute_set_id')); - - if ($model->itemExists()) { - Mage::getSingleton('adminhtml/session')->addError(Mage::helper('catalog')->__('A group with the same name already exists.')); - } else { - try { - $model->save(); - } catch (Exception $e) { - Mage::getSingleton('adminhtml/session')->addError(Mage::helper('catalog')->__('An error occurred while saving this group.')); - } - } - } -} diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/ReviewController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/ReviewController.php index 33f63d11f..e2d1ec50c 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/ReviewController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/ReviewController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2017-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -111,8 +111,6 @@ public function newAction() $this->loadLayout(); $this->_setActiveMenu('catalog/reviews_ratings/reviews/all'); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); - $this->_addContent($this->getLayout()->createBlock('adminhtml/review_add')); $this->_addContent($this->getLayout()->createBlock('adminhtml/review_product_grid')); @@ -291,18 +289,17 @@ public function reviewGridAction() public function jsonProductInfoAction() { $response = new Varien_Object(); - $id = $this->getRequest()->getParam('id'); - if ((int) $id > 0) { - $product = Mage::getModel('catalog/product') - ->load($id); + $product = Mage::getModel('catalog/product') + ->load((int) $this->getRequest()->getParam('id')); - $response->setId($id); + if ($product->getId()) { + $response->setId($product->getId()); $response->addData($product->getData()); - $response->setError(0); } else { $response->setError(1); $response->setMessage(Mage::helper('catalog')->__('Unable to get the product ID.')); } + $this->getResponse()->setHeader('Content-type', 'application/json', true); $this->getResponse()->setBody($response->toJson()); } diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/SetController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/SetController.php index 09faf6039..785ceadc4 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/SetController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/SetController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -69,7 +69,6 @@ public function editAction() $this->loadLayout(); $this->_setActiveMenu('catalog/attributes/sets'); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); $this->_addBreadcrumb(Mage::helper('catalog')->__('Catalog'), Mage::helper('catalog')->__('Catalog')); $this->_addBreadcrumb( @@ -102,7 +101,6 @@ public function setGridAction() public function saveAction() { $entityTypeId = $this->_getEntityTypeId(); - $hasError = false; $attributeSetId = $this->getRequest()->getParam('id', false); $isNewSet = $this->getRequest()->getParam('gotoEdit', false) == '1'; @@ -140,34 +138,26 @@ public function saveAction() } $model->save(); $this->_getSession()->addSuccess(Mage::helper('catalog')->__('The attribute set has been saved.')); + + if ($isNewSet) { + $this->_redirect('*/*/edit', ['id' => $model->getId()]); + } else { + $this->_prepareDataJSON(['url' => $this->getUrl('*/*/')]); + } } catch (Mage_Core_Exception $e) { - $this->_getSession()->addError($e->getMessage()); - $hasError = true; + $error = $e->getMessage(); } catch (Exception $e) { - $this->_getSession()->addException( - $e, - Mage::helper('catalog')->__('An error occurred while saving the attribute set.'), - ); - $hasError = true; + $error = Mage::helper('catalog')->__('An error occurred while saving the attribute set.'); + Mage::logException($e); } - if ($isNewSet) { - if ($hasError) { + if (isset($error)) { + if ($isNewSet) { + Mage::getSingleton('adminhtml/session')->addError($error); $this->_redirect('*/*/add'); } else { - $this->_redirect('*/*/edit', ['id' => $model->getId()]); + $this->_prepareDataJSON(['error' => true, 'message' => $error]); } - } else { - $response = []; - if ($hasError) { - $this->_initLayoutMessages('adminhtml/session'); - $response['error'] = 1; - $response['message'] = $this->getLayout()->getMessagesBlock()->getGroupedHtml(); - } else { - $response['error'] = 0; - $response['url'] = $this->getUrl('*/*/'); - } - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($response)); } } @@ -240,4 +230,16 @@ protected function _getEntityTypeId() } return Mage::registry('entityType'); } + + /** + * Prepare JSON formatted data for response to client + * + * @param mixed $response + * @return Zend_Controller_Response_Abstract + */ + protected function _prepareDataJSON($response) + { + $this->getResponse()->setHeader('Content-type', 'application/json', true); + return $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($response)); + } } diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php index 6b6d6e51b..870a35e25 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2017-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -219,8 +219,6 @@ public function newAction() $this->_setActiveMenu('catalog/products'); } - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); - $block = $this->getLayout()->getBlock('catalog.wysiwyg.js'); if ($block) { $block->setStoreId($product->getStoreId()); @@ -272,8 +270,6 @@ public function editAction() ); } - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); - $block = $this->getLayout()->getBlock('catalog.wysiwyg.js'); if ($block) { $block->setStoreId($product->getStoreId()); @@ -563,7 +559,7 @@ public function validateAction() $response->setMessage($this->getLayout()->getMessagesBlock()->getGroupedHtml()); } - $this->getResponse()->setBody($response->toJson()); + $this->getResponse()->setBodyJson($response); } /** @@ -710,9 +706,10 @@ public function categoriesJsonAction() { $product = $this->_initProduct(); + $this->getResponse()->setHeader('Content-type', 'application/json', true); $this->getResponse()->setBody( $this->getLayout()->createBlock('adminhtml/catalog_product_edit_tab_categories') - ->getCategoryChildrenJson($this->getRequest()->getParam('category')), + ->getTreeJson($this->getRequest()->getParam('category')), ); } @@ -1037,10 +1034,9 @@ public function quickCreateAction() $productSku = $product->getSku(); if ($productSku && $productSku != Mage::helper('core')->stripTags($productSku)) { - $result['error'] = [ - 'message' => $this->__('HTML tags are not allowed in SKU attribute.'), - ]; - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + $this->getResponse()->setBodyJson([ + 'error' => ['message' => $this->__('HTML tags are not allowed in SKU attribute.')], + ]); return; } @@ -1109,7 +1105,7 @@ public function quickCreateAction() ]; } - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + $this->getResponse()->setBodyJson($result); } /** diff --git a/app/code/core/Mage/Adminhtml/controllers/Cms/Wysiwyg/ImagesController.php b/app/code/core/Mage/Adminhtml/controllers/Cms/Wysiwyg/ImagesController.php index 6fabbbdc6..72a248b75 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Cms/Wysiwyg/ImagesController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Cms/Wysiwyg/ImagesController.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2017-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -56,12 +57,12 @@ public function treeJsonAction() { try { $this->_initAction(); + $this->getResponse()->setHeader('Content-type', 'application/json', true); $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/cms_wysiwyg_images_tree') - ->getTreeJson(), + $this->getLayout()->createBlock('adminhtml/cms_wysiwyg_images_tree')->getTreeJson(), ); } catch (Exception $e) { - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode([])); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } @@ -72,8 +73,7 @@ public function contentsAction() $this->loadLayout('empty'); $this->renderLayout(); } catch (Exception $e) { - $result = ['error' => true, 'message' => $e->getMessage()]; - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } @@ -84,10 +84,10 @@ public function newFolderAction() $name = $this->getRequest()->getPost('name'); $path = $this->getStorage()->getSession()->getCurrentPath(); $result = $this->getStorage()->createDirectory($name, $path); + $this->getResponse()->setBodyJson($result); } catch (Exception $e) { - $result = ['error' => true, 'message' => $e->getMessage()]; + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); } public function deleteFolderAction() @@ -95,9 +95,9 @@ public function deleteFolderAction() try { $path = $this->getStorage()->getSession()->getCurrentPath(); $this->getStorage()->deleteDirectory($path); + $this->getResponse()->setBodyJson([]); } catch (Exception $e) { - $result = ['error' => true, 'message' => $e->getMessage()]; - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } @@ -124,9 +124,9 @@ public function deleteFilesAction() $this->getStorage()->deleteFile($path . DS . $file); } } + $this->getResponse()->setBodyJson([]); } catch (Exception $e) { - $result = ['error' => true, 'message' => $e->getMessage()]; - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } @@ -136,14 +136,13 @@ public function deleteFilesAction() public function uploadAction() { try { - $result = []; $this->_initAction(); $targetPath = $this->getStorage()->getSession()->getCurrentPath(); $result = $this->getStorage()->uploadFile($targetPath, $this->getRequest()->getParam('type')); + $this->getResponse()->setBodyJson($result); } catch (Exception $e) { - $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; + $this->getResponse()->setBodyJson(['error' => $e->getMessage(), 'errorcode' => $e->getCode()]); } - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); } /** diff --git a/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php b/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php index bf8412396..cea1a07a4 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -115,8 +115,6 @@ public function editRoleAction() $this->_addBreadcrumb($breadCrumb, $breadCrumbTitle); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); - $this->_addContent( $this->getLayout()->createBlock('adminhtml/permissions_buttons') ->setRoleId($role->getId()) diff --git a/app/code/core/Mage/Adminhtml/controllers/Promo/WidgetController.php b/app/code/core/Mage/Adminhtml/controllers/Promo/WidgetController.php index 09d20fe52..832a25022 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Promo/WidgetController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Promo/WidgetController.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -42,27 +43,12 @@ public function chooserAction() break; case 'category_ids': - $ids = $request->getParam('selected', []); - if (is_array($ids)) { - foreach ($ids as $key => &$id) { - $id = (int) $id; - if ($id <= 0) { - unset($ids[$key]); - } - } - - $ids = array_unique($ids); - } else { - $ids = []; - } - $block = $this->getLayout()->createBlock( 'adminhtml/catalog_category_checkboxes_tree', 'promo_widget_chooser_category_ids', ['js_form_object' => $request->getParam('form')], - ) - ->setCategoryIds($ids) - ; + ); + $block->setCategoryIds($request->getParam('selected', [])); break; default: @@ -80,16 +66,21 @@ public function chooserAction() */ public function categoriesJsonAction() { - if ($categoryId = (int) $this->getRequest()->getPost('id')) { - $this->getRequest()->setParam('id', $categoryId); + try { + $categoryId = (int) $this->getRequest()->getPost('id'); + $category = $this->_initCategory(); - if (!$category = $this->_initCategory()) { - return; + if (!$category || !$category->getId()) { + Mage::throwException(Mage::helper('catalog')->__('This category no longer exists.')); } + + $this->getResponse()->setHeader('Content-type', 'application/json', true); $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/catalog_category_tree') + $this->getLayout()->createBlock('adminhtml/catalog_category_checkboxes_tree') ->getTreeJson($category), ); + } catch (Exception $e) { + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } diff --git a/app/code/core/Mage/Adminhtml/controllers/System/DesignController.php b/app/code/core/Mage/Adminhtml/controllers/System/DesignController.php index 3d097ff7b..ec5fb53c1 100644 --- a/app/code/core/Mage/Adminhtml/controllers/System/DesignController.php +++ b/app/code/core/Mage/Adminhtml/controllers/System/DesignController.php @@ -7,7 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -63,10 +63,8 @@ public function editAction() ->loadLayout() ->_setActiveMenu('system/design'); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); - - $id = (int) $this->getRequest()->getParam('id'); - $design = Mage::getModel('core/design'); + $id = (int) $this->getRequest()->getParam('id'); + $design = Mage::getModel('core/design'); if ($id) { $design->load($id); diff --git a/app/code/core/Mage/Adminhtml/controllers/UrlrewriteController.php b/app/code/core/Mage/Adminhtml/controllers/UrlrewriteController.php index 636babddc..7fd48f896 100644 --- a/app/code/core/Mage/Adminhtml/controllers/UrlrewriteController.php +++ b/app/code/core/Mage/Adminhtml/controllers/UrlrewriteController.php @@ -7,6 +7,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -80,7 +81,6 @@ public function editAction() $this->loadLayout(); $this->_setActiveMenu('catalog/urlrewrite'); $this->_addContent($this->getLayout()->createBlock('adminhtml/urlrewrite_edit')); - $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); $this->renderLayout(); } @@ -99,10 +99,10 @@ public function productGridAction() */ public function categoriesJsonAction() { - $id = $this->getRequest()->getParam('id', null); + $this->getResponse()->setHeader('Content-type', 'application/json', true); $this->getResponse()->setBody( Mage::getBlockSingleton('adminhtml/urlrewrite_category_tree') - ->getTreeArray($id, true, 1), + ->getTreeArray($this->getRequest()->getParam('id'), true), ); } diff --git a/app/code/core/Mage/Catalog/Model/Category.php b/app/code/core/Mage/Catalog/Model/Category.php index 8ffcf71b2..817f3c8cc 100644 --- a/app/code/core/Mage/Catalog/Model/Category.php +++ b/app/code/core/Mage/Catalog/Model/Category.php @@ -7,7 +7,7 @@ * @package Mage_Catalog * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2017-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -244,17 +244,17 @@ public function move($parentId, $afterCategoryId) if (!$parent->getId()) { Mage::throwException( - Mage::helper('catalog')->__('Category move operation is not possible: the new parent category was not found.'), + Mage::helper('catalog')->__('Parent category was not found.'), ); } if (!$this->getId()) { Mage::throwException( - Mage::helper('catalog')->__('Category move operation is not possible: the current category was not found.'), + Mage::helper('catalog')->__('Category was not found.'), ); } elseif ($parent->getId() == $this->getId()) { Mage::throwException( - Mage::helper('catalog')->__('Category move operation is not possible: parent category is equal to child category.'), + Mage::helper('catalog')->__('Parent category is equal to child category.'), ); } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Category.php b/app/code/core/Mage/Catalog/Model/Resource/Category.php index 43127ca7d..272ff8d22 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Category.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Category.php @@ -7,7 +7,7 @@ * @package Mage_Catalog * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2018-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -734,18 +734,53 @@ public function getAllChildren($category) } /** - * Check is category in list of store categories + * Check if category is a child of current store root category * * @param Mage_Catalog_Model_Category $category * @return bool */ public function isInRootCategoryList($category) { - $rootCategoryId = Mage::app()->getStore()->getRootCategoryId(); + return $this->isInStoreRootCategory($category); + } + /** + * Check if category is a child of specific store root category + * + * @param Mage_Catalog_Model_Category $category + * @param null|string|bool|int|Mage_Core_Model_Store $store + */ + public function isInStoreRootCategory($category, $store = null): bool + { + $rootCategoryId = Mage::app()->getStore($store)->getRootCategoryId(); return in_array($rootCategoryId, $category->getParentIds()); } + /** + * Check if category is a child of specific store root category, or the root category itself + * + * @param Mage_Catalog_Model_Category $category + * @param null|string|bool|int|Mage_Core_Model_Store $store + */ + public function isInStore($category, $store = null): bool + { + $rootCategoryId = Mage::app()->getStore($store)->getRootCategoryId(); + return in_array($rootCategoryId, $category->getPathIds()); + } + + /** + * Return ids of root categories as array + * + * @return list + */ + public function getRootIds(): array + { + return array_map( + fn($store) => (int) $store->getRootCategoryId(), + Mage::app()->getGroups(), + ); + } + /** * Check category is forbidden to delete. * If category is root and assigned to store group return false diff --git a/app/code/core/Mage/Catalog/Model/Resource/Category/Tree.php b/app/code/core/Mage/Catalog/Model/Resource/Category/Tree.php index 5c2e5ca12..88cc2a1be 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Category/Tree.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Category/Tree.php @@ -7,7 +7,7 @@ * @package Mage_Catalog * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -435,52 +435,57 @@ protected function _afterMove($category, $newParent, $prevNode) } /** - * Load whole category tree, that will include specified categories ids. + * Load category tree including specified categories ids. * * @param array $ids * @param bool $addCollectionData * @param bool $updateAnchorProductCount + * @param ?non-negative-int $recursionLevel * @return $this|false */ - public function loadByIds($ids, $addCollectionData = true, $updateAnchorProductCount = true) + public function loadByIds($ids, $addCollectionData = true, $updateAnchorProductCount = true, $recursionLevel = null) { $levelField = $this->_conn->quoteIdentifier('level'); $pathField = $this->_conn->quoteIdentifier('path'); - // load first two levels, if no ids specified - if (empty($ids)) { - $select = $this->_conn->select() - ->from($this->_table, 'entity_id') - ->where($levelField . ' <= 2'); - $ids = $this->_conn->fetchCol($select); - } + $recursionLevel ??= Mage_Adminhtml_Block_Catalog_Category_Abstract::DEFAULT_RECURSION_LEVEL; + if (!is_array($ids)) { $ids = [$ids]; } - foreach ($ids as $key => $id) { - $ids[$key] = (int) $id; + foreach ($ids as $key => &$id) { + $id = (int) $id; + if ($id <= 0) { + unset($ids[$key]); + } } - // collect paths of specified IDs and prepare to collect all their parents and neighbours - $select = $this->_conn->select() - ->from($this->_table, ['path', 'level']) - ->where('entity_id IN (?)', $ids); - $where = [$levelField . '=0' => true]; + $where = []; + if ($recursionLevel !== 0) { + $where[] = $this->_conn->quoteInto("$levelField <= ?", $recursionLevel + 1); + } - foreach ($this->_conn->fetchAll($select) as $item) { - if (!preg_match("#^[0-9\/]+$#", $item['path'])) { - $item['path'] = ''; - } - $pathIds = explode('/', $item['path']); - $level = (int) $item['level']; - while ($level > 0) { - $pathIds[count($pathIds) - 1] = '%'; - $path = implode('/', $pathIds); - $where["$levelField=$level AND $pathField LIKE '$path'"] = true; - array_pop($pathIds); - $level--; + // collect paths of specified IDs and build query to collect their parents and neighbours + if (!empty($ids)) { + $select = $this->_conn->select() + ->from($this->_table, ['path', 'level']) + ->where('entity_id IN (?)', $ids); + + foreach ($this->_conn->fetchAll($select) as $item) { + if (!preg_match("#^[0-9\/]+$#", $item['path'])) { + $item['path'] = ''; + } + $pathIds = explode('/', $item['path']); + $level = (int) $item['level']; + while ($level > $recursionLevel + 1) { + $pathIds[count($pathIds) - 1] = '%'; + $path = implode('/', $pathIds); + $where[] = $this->_conn->quoteInto("$levelField = ?", $level) + . ' AND ' . $this->_conn->quoteInto("$pathField LIKE ?", $path); + array_pop($pathIds); + $level--; + } } } - $where = array_keys($where); // get all required records if ($addCollectionData) { @@ -489,7 +494,9 @@ public function loadByIds($ids, $addCollectionData = true, $updateAnchorProductC $select = clone $this->_select; $select->order($this->_orderField . ' ' . Varien_Db_Select::SQL_ASC); } - $select->where(implode(' OR ', $where)); + if (count($where)) { + $select->where(implode(' OR ', array_unique($where))); + } // get array of records and add them as nodes to the tree $arrNodes = $this->_conn->fetchAll($select); diff --git a/app/code/core/Mage/Catalog/etc/jstranslator.xml b/app/code/core/Mage/Catalog/etc/jstranslator.xml new file mode 100644 index 000000000..858f254f0 --- /dev/null +++ b/app/code/core/Mage/Catalog/etc/jstranslator.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/code/core/Mage/Core/etc/jstranslator.xml b/app/code/core/Mage/Core/etc/jstranslator.xml index 9326fe2d5..b2f4e95b7 100644 --- a/app/code/core/Mage/Core/etc/jstranslator.xml +++ b/app/code/core/Mage/Core/etc/jstranslator.xml @@ -7,11 +7,17 @@ * @package Mage_Core * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> + + + Server returned status %s + + + + diff --git a/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php b/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php index 6e7a96959..ce83a019f 100644 --- a/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php +++ b/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php @@ -7,7 +7,7 @@ * @package Mage_Widget * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -141,17 +141,15 @@ private function setBody($body) */ public function validateAction() { - $response = new Varien_Object(); - $response->setError(false); - $widgetInstance = $this->_initWidgetInstance(); - $result = $widgetInstance->validate(); + $result = $this->_initWidgetInstance()->validate(); if ($result !== true && is_string($result)) { $this->_getSession()->addError($result); $this->_initLayoutMessages('adminhtml/session'); - $response->setError(true); - $response->setMessage($this->getLayout()->getMessagesBlock()->getGroupedHtml()); + $errorHtml = $this->getLayout()->getMessagesBlock()->getGroupedHtml(); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $errorHtml]); + } else { + $this->getResponse()->setBodyJson(['error' => false]); } - $this->setBody($response->toJson()); } /** diff --git a/app/code/core/Mage/Widget/controllers/Adminhtml/WidgetController.php b/app/code/core/Mage/Widget/controllers/Adminhtml/WidgetController.php index c77200a9e..67c971bbb 100644 --- a/app/code/core/Mage/Widget/controllers/Adminhtml/WidgetController.php +++ b/app/code/core/Mage/Widget/controllers/Adminhtml/WidgetController.php @@ -7,6 +7,7 @@ * @package Mage_Widget * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -60,8 +61,7 @@ public function loadOptionsAction() $this->renderLayout(); } } catch (Mage_Core_Exception $e) { - $result = ['error' => true, 'message' => $e->getMessage()]; - $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + $this->getResponse()->setBodyJson(['error' => true, 'message' => $e->getMessage()]); } } diff --git a/app/design/adminhtml/default/default/layout/admin.xml b/app/design/adminhtml/default/default/layout/admin.xml index 50681d702..6363c1118 100644 --- a/app/design/adminhtml/default/default/layout/admin.xml +++ b/app/design/adminhtml/default/default/layout/admin.xml @@ -7,6 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2023 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> @@ -20,6 +21,20 @@ + + + + maho-tree.js + + + + + + + maho-tree.js + + + @@ -59,6 +74,9 @@ + + maho-tree.js + diff --git a/app/design/adminhtml/default/default/layout/api2.xml b/app/design/adminhtml/default/default/layout/api2.xml index 7b81a5900..80a807a60 100644 --- a/app/design/adminhtml/default/default/layout/api2.xml +++ b/app/design/adminhtml/default/default/layout/api2.xml @@ -7,6 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> @@ -18,6 +19,9 @@ + + maho-tree.js + @@ -35,14 +39,12 @@ - - - 1 - - + + maho-tree.js + @@ -65,11 +67,6 @@ - - - 1 - - @@ -108,6 +105,9 @@ + + maho-tree.js + @@ -122,11 +122,6 @@ - - - 1 - - diff --git a/app/design/adminhtml/default/default/layout/catalog.xml b/app/design/adminhtml/default/default/layout/catalog.xml index 80b45ffac..4f69e8176 100644 --- a/app/design/adminhtml/default/default/layout/catalog.xml +++ b/app/design/adminhtml/default/default/layout/catalog.xml @@ -7,7 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2016-2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> @@ -32,7 +32,8 @@ - jshtml5sortable.min.js + sortable.min.js + maho-tree.js @@ -49,7 +50,8 @@ - jshtml5sortable.min.js + sortable.min.js + maho-tree.js @@ -70,6 +72,18 @@ + + + mage/adminhtml/catalog/review.js + + + + + + mage/adminhtml/catalog/review.js + + + @@ -273,13 +287,28 @@ Layout handle for configurable products + + + sortable.min.js + maho-tree.js + mage/adminhtml/eav/set.js + + + + + sortable.min.js + maho-tree.js + mage/adminhtml/catalog/category.js + - + + 1 + diff --git a/app/design/adminhtml/default/default/layout/main.xml b/app/design/adminhtml/default/default/layout/main.xml index 87ebc2f90..70d4c6b7a 100644 --- a/app/design/adminhtml/default/default/layout/main.xml +++ b/app/design/adminhtml/default/default/layout/main.xml @@ -7,7 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2020-2023 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -67,12 +67,6 @@ Default layout, loads most of the pages js_cssflatpickr/flatpickr.min.css jsflatpickr/flatpickr.min.js - jsmaho-effects.jscan_load_ext_js - jsextjs/ext-tree.min.jscan_load_ext_js - jsextjs/fix-defer.jscan_load_ext_js - jsextjs/ext-tree-checkbox.jscan_load_ext_js - js_cssextjs/resources/css/ytheme-magento.csscan_load_ext_js - jsmage/adminhtml/rules.jscan_load_rules_js jsmage/adminhtml/wysiwyg/tinymce/setup.jscan_load_tiny_mce @@ -135,14 +129,13 @@ Layout for editor element --> - 1 flow.min.js mage/adminhtml/uploader/instance.js - + diff --git a/app/design/adminhtml/default/default/layout/oauth.xml b/app/design/adminhtml/default/default/layout/oauth.xml index ff3e74827..8c7fa5245 100644 --- a/app/design/adminhtml/default/default/layout/oauth.xml +++ b/app/design/adminhtml/default/default/layout/oauth.xml @@ -7,7 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2023 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> @@ -77,14 +77,9 @@ cssprint.css js_cssflatpickr/flatpickr.min.css - js_cssextjs/resources/css/ytheme-magento.css skin_cssmenu.css jsflatpickr/flatpickr.min.js - jsmaho-effects.js - jsextjs/ext-tree.min.js - jsextjs/fix-defer.js - jsextjs/ext-tree-checkbox.js diff --git a/app/design/adminhtml/default/default/layout/promo.xml b/app/design/adminhtml/default/default/layout/promo.xml index 0a4cad381..e33885201 100644 --- a/app/design/adminhtml/default/default/layout/promo.xml +++ b/app/design/adminhtml/default/default/layout/promo.xml @@ -7,6 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> @@ -20,8 +21,8 @@ - 1 1 + maho-tree.js @@ -49,8 +50,8 @@ - 1 1 + maho-tree.js diff --git a/app/design/adminhtml/default/default/layout/widget.xml b/app/design/adminhtml/default/default/layout/widget.xml index f988a5463..02ba83958 100644 --- a/app/design/adminhtml/default/default/layout/widget.xml +++ b/app/design/adminhtml/default/default/layout/widget.xml @@ -7,7 +7,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ --> @@ -29,9 +29,9 @@ - 1 jsmaho-dialog.js + jsmaho-tree.js diff --git a/app/design/adminhtml/default/default/template/api/rolesedit.phtml b/app/design/adminhtml/default/default/template/api/rolesedit.phtml index 19f1b3fe9..5fae5a71c 100644 --- a/app/design/adminhtml/default/default/template/api/rolesedit.phtml +++ b/app/design/adminhtml/default/default/template/api/rolesedit.phtml @@ -6,124 +6,58 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ + +/** @var Mage_Adminhtml_Block_Api_Tab_Rolesedit $this */ ?>

__('Roles Resources') ?>

- -
- + +
- + + - -
-
+
+
-
- - diff --git a/app/design/adminhtml/default/default/template/api2/attribute/resource.phtml b/app/design/adminhtml/default/default/template/api2/attribute/resource.phtml index a6e5fa167..bbcbfb12b 100644 --- a/app/design/adminhtml/default/default/template/api2/attribute/resource.phtml +++ b/app/design/adminhtml/default/default/template/api2/attribute/resource.phtml @@ -6,9 +6,11 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ + +/** @var Mage_Api2_Block_Adminhtml_Attribute_Tab_Resource $this */ ?> getChildHtml() ?> @@ -17,121 +19,50 @@

__('User Type Resources') ?>

-
- +
- + + - -
-
+
+
hasEntityOnlyAttributes()): ?> -
- * This attribute data will be returned for a single resource only. +
* This attribute data will be returned for a single resource only.
-
- - diff --git a/app/design/adminhtml/default/default/template/catalog/category/checkboxes/tree.phtml b/app/design/adminhtml/default/default/template/catalog/category/checkboxes/tree.phtml index 010f89e33..03a8f739a 100644 --- a/app/design/adminhtml/default/default/template/catalog/category/checkboxes/tree.phtml +++ b/app/design/adminhtml/default/default/template/catalog/category/checkboxes/tree.phtml @@ -6,180 +6,30 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ -?> - - -
- - diff --git a/app/design/adminhtml/default/default/template/catalog/category/edit.phtml b/app/design/adminhtml/default/default/template/catalog/category/edit.phtml index 73a86b41f..1980f1382 100644 --- a/app/design/adminhtml/default/default/template/catalog/category/edit.phtml +++ b/app/design/adminhtml/default/default/template/catalog/category/edit.phtml @@ -6,178 +6,54 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ -?> -getChild('form'); +$tabsBlock = $this->getLayout()->getBlock('tabs'); +$treeBlock = $this->getLayout()->getBlock('category.tree'); ?>
getChildHtml('form') ?>
- diff --git a/app/design/adminhtml/default/default/template/catalog/category/edit/form.phtml b/app/design/adminhtml/default/default/template/catalog/category/edit/form.phtml index d96ce7352..35cfa8028 100644 --- a/app/design/adminhtml/default/default/template/catalog/category/edit/form.phtml +++ b/app/design/adminhtml/default/default/template/catalog/category/edit/form.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -14,15 +14,13 @@ ?>
-

escapeHtml($this->getHeader()) . ($this->getCategoryId() ? ' (' . Mage::helper('catalog')->__('ID: %s', $this->getCategoryId()) . ')' : '') ?>

-

- getResetButtonHtml() ?> - getCategoryId()): ?> - getDeleteButtonHtml() ?> - - getAdditionalButtonsHtml() ?> - getSaveButtonHtml() ?> -

+

escapeHtml($this->getHeader()) ?>

+

+ getResetButtonHtml() ?> + getCategoryId() ? $this->getDeleteButtonHtml() : '' ?> + getAdditionalButtonsHtml() ?> + getSaveButtonHtml() ?> +

hasStoreRootCategory()): ?> getTabsHtml() ?> @@ -31,216 +29,14 @@ __('Set root category for this store in the configuration', $this->getStoreConfigurationUrl()) ?> - -
-
- - - - -
+ + + +
- diff --git a/app/design/adminhtml/default/default/template/catalog/category/tree.phtml b/app/design/adminhtml/default/default/template/catalog/category/tree.phtml index 35a2132e4..9b3faa4ab 100644 --- a/app/design/adminhtml/default/default/template/catalog/category/tree.phtml +++ b/app/design/adminhtml/default/default/template/catalog/category/tree.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -16,458 +16,19 @@

__('Categories') ?>

getRoot()): ?> - getAddRootButtonHtml() ?>
+ getAddRootButtonHtml() ?> getAddSubButtonHtml() ?>
getStoreSwitcherHtml() ?> - getRoot()): ?>
-
+
- - diff --git a/app/design/adminhtml/default/default/template/catalog/category/widget/tree.phtml b/app/design/adminhtml/default/default/template/catalog/category/widget/tree.phtml index 8269ababf..0d6749f47 100644 --- a/app/design/adminhtml/default/default/template/catalog/category/widget/tree.phtml +++ b/app/design/adminhtml/default/default/template/catalog/category/widget/tree.phtml @@ -6,183 +6,46 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2022 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ -?> - -getId() ?> -
- - + diff --git a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml b/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml index d9087aa17..95046dce0 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -25,449 +25,62 @@ - - - - - - -
- getSetFormHtml() ?> - -
- - - - - - -

__('Groups') ?>

-
- +
+
+ getSetFormHtml() ?> +
+ +
+
+

__('Groups') ?>

getIsReadOnly()): ?> -

getAddGroupButton() ?> getDeleteGroupButton() ?>

-

__('Double click on a group to rename it') ?>

- - - getSetsFilterHtml() ?> - getGroupTreeHtml() ?> -
-
- - - - - - -

__('Unassigned Attributes') ?>

+
+ getAddGroupButton() ?>getRenameButton() ?>getDeleteGroupButton() ?>
-
- -
+ return true; + }, + canDeleteGroup: (group) => { + if (group.childNodes.some((node) => !node.attributes.is_user_defined)) { + return Translator.translate('Cannot delete group. Please move system attributes to another group and try again.'); + } + if (group.childNodes.some((node) => node.attributes.is_configurable)) { + return Translator.translate('Cannot delete group. Please move configurable attributes to another group and try again.'); + } + return true; + } + }); + + setForm.buildGroupTree(getGroupTreeJson() ?>); + setForm.buildAttributeTree(getAttributeTreeJson() ?>); +}); + diff --git a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main/tree/attribute.phtml b/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main/tree/attribute.phtml deleted file mode 100644 index 8f99ce285..000000000 --- a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main/tree/attribute.phtml +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main/tree/group.phtml b/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main/tree/group.phtml deleted file mode 100644 index c806c8dbd..000000000 --- a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main/tree/group.phtml +++ /dev/null @@ -1,12 +0,0 @@ - -
diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit/categories.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit/categories.phtml index 0e25f86b3..7a236b2d5 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/edit/categories.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/edit/categories.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -16,121 +16,40 @@

__('Product Categories') ?>

-
+
-
+
-getRootNode() && $this->getRootNode()->hasChildren()): ?> - diff --git a/app/design/adminhtml/default/default/template/cms/browser/js.phtml b/app/design/adminhtml/default/default/template/cms/browser/js.phtml index cdfc27807..5f48da473 100644 --- a/app/design/adminhtml/default/default/template/cms/browser/js.phtml +++ b/app/design/adminhtml/default/default/template/cms/browser/js.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ ?> @@ -14,13 +14,10 @@ /** * Directories tree template * - * @see Mage_Adminhtml_Block_Cms_Wysiwyg_Images_Content * @var Mage_Adminhtml_Block_Cms_Wysiwyg_Images_Content $this */ ?> - diff --git a/app/design/adminhtml/default/default/template/cms/browser/tree.phtml b/app/design/adminhtml/default/default/template/cms/browser/tree.phtml index 20ff44d9a..9e1efa3a0 100644 --- a/app/design/adminhtml/default/default/template/cms/browser/tree.phtml +++ b/app/design/adminhtml/default/default/template/cms/browser/tree.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ ?> @@ -14,8 +14,7 @@ /** * Directories tree template * - * @see Mage_Adminhtml_Block_Cms_Wysiwyg_Images_Tree - * @var Mage_Adminhtml_Block_Cms_Wysiwyg_Images_Tree $this + * @var Mage_Adminhtml_Block_Cms_Wysiwyg_Images_Tree $this */ ?>
@@ -26,44 +25,42 @@
-
+
- diff --git a/app/design/adminhtml/default/default/template/page/head.phtml b/app/design/adminhtml/default/default/template/page/head.phtml index 587cc71d6..5dd09a7f3 100644 --- a/app/design/adminhtml/default/default/template/page/head.phtml +++ b/app/design/adminhtml/default/default/template/page/head.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -27,13 +27,6 @@ getCssJsHtml() ?> -getCanLoadExtJs()): ?> - - - getCanLoadTinyMce()): // TinyMCE is broken when loaded through index.php?> diff --git a/app/design/adminhtml/default/default/template/permissions/rolesedit.phtml b/app/design/adminhtml/default/default/template/permissions/rolesedit.phtml index 2b3bc59d4..9207946d2 100644 --- a/app/design/adminhtml/default/default/template/permissions/rolesedit.phtml +++ b/app/design/adminhtml/default/default/template/permissions/rolesedit.phtml @@ -6,11 +6,11 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ -/** @var Mage_Adminhtml_Block_Permissions_Tab_Rolesedit $this */ +/** @var Mage_Adminhtml_Block_Permissions_Tab_Rolesedit|Mage_Api2_Block_Adminhtml_Roles_Tab_Resources $this */ ?> getChildHtml() ?> @@ -19,115 +19,47 @@

__('Roles Resources') ?>

-
- +
- + + - -
-
+
+
-
- - diff --git a/app/design/adminhtml/default/default/template/review/add.phtml b/app/design/adminhtml/default/default/template/review/add.phtml deleted file mode 100644 index 32c9a7c9d..000000000 --- a/app/design/adminhtml/default/default/template/review/add.phtml +++ /dev/null @@ -1,33 +0,0 @@ - -
- - - - - -

getHeaderText() ?>

- getBackButtonHtml() ?> - getResetButtonHtml() ?> - -
-
- - - diff --git a/app/design/adminhtml/default/default/template/urlrewrite/categories.phtml b/app/design/adminhtml/default/default/template/urlrewrite/categories.phtml index f51aa3f78..50f1d860b 100644 --- a/app/design/adminhtml/default/default/template/urlrewrite/categories.phtml +++ b/app/design/adminhtml/default/default/template/urlrewrite/categories.phtml @@ -6,14 +6,13 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ /** * Categories tree for urlrewrites * - * @see Mage_Adminhtml_Block_Urlrewrite_Category_Tree * @var Mage_Adminhtml_Block_Urlrewrite_Category_Tree $this */ ?> @@ -23,118 +22,28 @@
-
+
getRoot()): ?> - diff --git a/app/design/adminhtml/default/default/template/widget/instance/edit/layout.phtml b/app/design/adminhtml/default/default/template/widget/instance/edit/layout.phtml index e72388513..0640da3ed 100644 --- a/app/design/adminhtml/default/default/template/widget/instance/edit/layout.phtml +++ b/app/design/adminhtml/default/default/template/widget/instance/edit/layout.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -360,23 +360,9 @@ var WidgetInstance = { } }, checkCategory : function(event) { - node = event.memo.node; - container = event.target.up('div.chooser_container'); - value = container.down('input[type="text"].entities').value.strip(); - if (node.attributes.checked) { - if (value) ids = value.split(','); - else ids = []; - if (-1 == ids.indexOf(node.id)) { - ids.push(node.id); - container.down('input[type="text"].entities').value = ids.join(','); - } - } else { - ids = value.split(','); - while (-1 != ids.indexOf(node.id)) { - ids.splice(ids.indexOf(node.id), 1); - container.down('input[type="text"].entities').value = ids.join(','); - } - } + const containerEl = event.target.closest('div.chooser_container'); + const inputEl = containerEl.querySelector('input[type=text]'); + inputEl.value = event.detail.selected.map((obj) => obj.id).join(','); }, togglePageGroupChooser : function(element) { element = $(element); @@ -437,14 +423,15 @@ var WidgetInstance = { } }; -Ext.onReady(function(){ +document.addEventListener('DOMContentLoaded', () => { getPageGroups() as $pageGroup): ?> WidgetInstance.addPageGroup(); Event.observe(document, 'product:changed', function(event){ WidgetInstance.checkProduct(event); }); - Event.observe(document, 'node:changed', function(event){ + + document.addEventListener('category:changed', (event) => { WidgetInstance.checkCategory(event); }); }); diff --git a/app/design/adminhtml/default/default/template/widget/tabshoriz.phtml b/app/design/adminhtml/default/default/template/widget/tabshoriz.phtml index 3e5ec2112..4000c340e 100644 --- a/app/design/adminhtml/default/default/template/widget/tabshoriz.phtml +++ b/app/design/adminhtml/default/default/template/widget/tabshoriz.phtml @@ -6,7 +6,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2021-2024 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -22,7 +22,7 @@
  • - + getTabLabel($_tab) ?> diff --git a/app/locale/en_US/Mage_Adminhtml.csv b/app/locale/en_US/Mage_Adminhtml.csv index 6d1de7ef2..221fb287c 100644 --- a/app/locale/en_US/Mage_Adminhtml.csv +++ b/app/locale/en_US/Mage_Adminhtml.csv @@ -737,7 +737,6 @@ "Phone:","Phone:" "Please Select","Please Select" "Please confirm site switching. All data that hasn't been saved will be lost.","Please confirm site switching. All data that hasn't been saved will be lost." -"Please select a parent category before adding a new one.","Please select a parent category before adding a new one." "Please enter 6 or more characters.","Please enter 6 or more characters." "Please enter a number greater than 0 in this field.","Please enter a number greater than 0 in this field." "Please enter a valid $ amount. For example $100.00.","Please enter a valid $ amount. For example $100.00." @@ -870,6 +869,7 @@ "Reload captcha","Reload captcha" "Remote FTP","Remote FTP" "Remove","Remove" +"Rename","Rename" "Report Issues","Report Issues" "Reports","Reports" "Request Path","Request Path" diff --git a/app/locale/en_US/Mage_Catalog.csv b/app/locale/en_US/Mage_Catalog.csv index 36504e711..eeb0d3dd7 100644 --- a/app/locale/en_US/Mage_Catalog.csv +++ b/app/locale/en_US/Mage_Catalog.csv @@ -52,7 +52,7 @@ "All Websites","All Websites" "All products","All products" "All products - recently added products, New products - products marked as new","All products - recently added products, New products - products marked as new" -"All products of this set will be deleted! Are you sure you want to delete this attribute set?","All products of this set will be deleted! Are you sure you want to delete this attribute set?" +"All products of this set will be deleted! Type ""confirm"" to proceed.","All products of this set will be deleted! Type ""confirm"" to proceed." "Allow All Products per Page","Allow All Products per Page" "Allow Dynamic Media URLs in Products and Categories","Allow Dynamic Media URLs in Products and Categories" "Allow HTML Tags on Frontend","Allow HTML Tags on Frontend" @@ -68,9 +68,7 @@ "An error occurred while saving the collection, aborting. Error message: %s","An error occurred while saving the collection, aborting. Error message: %s" "An error occurred while saving the product. ","An error occurred while saving the product. " "An error occurred while saving the search query.","An error occurred while saving the search query." -"An error occurred while saving this group.","An error occurred while saving this group." "An error occurred while saving this review.","An error occurred while saving this review." -"An error occurred while trying to delete the category.","An error occurred while trying to delete the category." "An error occurred while updating the product(s) attributes.","An error occurred while updating the product(s) attributes." "An error occurred while updating the product(s) status.","An error occurred while updating the product(s) status." "An invalid group ID is specified, skipping the record.","An invalid group ID is specified, skipping the record." @@ -103,7 +101,6 @@ "Attribute add","Attribute add" "Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter. Do not use ""event"" for an attribute code.","Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter. Do not use ""event"" for an attribute code." "Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter. Do not use event"" for an attribute code.""","Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter. Do not use event"" for an attribute code.""" -"Attribute group with the \"/name/\" name already exists","Attribute group with the \"/name/\" name already exists" "Attribute remove","Attribute remove" "Attribute with the same code already exists","Attribute with the same code already exists" "Attributes","Attributes" @@ -136,9 +133,12 @@ "Can be used only with catalog input type Dropdown","Can be used only with catalog input type Dropdown" "Can be used only with catalog input type Dropdown, Multiple Select and Price","Can be used only with catalog input type Dropdown, Multiple Select and Price" "Can't create image.","Can't create image." +"Can't delete root category.",Can't delete root category." "Cancel","Cancel" +"Cannot delete group. Please move configurable attributes to another group and try again.","Cannot delete group. Please move configurable attributes to another group and try again." "Cannot create image.","Cannot create image." "Cannot create writeable directory '%s'.","Cannot create writeable directory '%s'." +"Cannot unassign configurable attribute","Cannot unassign configurable attribute" "Cart Item Attribute","Cart Item Attribute" "Catalog","Catalog" "Catalog Category (Anchor)","Catalog Category (Anchor)" @@ -176,12 +176,11 @@ "Category Top Navigation","Category Top Navigation" "Category URL Suffix","Category URL Suffix" "Category attributes API","Category attributes API" -"Category move error","Category move error" -"Category move error %s","Category move error %s" -"Category move operation is not possible: parent category is equal to child category.","Category move operation is not possible: parent category is equal to child category." -"Category move operation is not possible: the current category was not found.","Category move operation is not possible: the current category was not found." -"Category move operation is not possible: the new parent category was not found.","Category move operation is not possible: the new parent category was not found." +"Category delete error: %s","Category delete error: %d" +"Category move error: %s","Category move error: %s" "Category must be an instance of Mage_Catalog_Model_Category.","Category must be an instance of Mage_Catalog_Model_Category." +"Category save error: %s","Category save error: %s" +"Category was not found.","Category was not found." "Center","Center" "Change","Change" "Change or Retrieve attribute store view","Change or Retrieve attribute store view" @@ -290,6 +289,7 @@ "Enable for reindexing a big number of SKUs.","Enable for reindexing a big number of SKUs." "Enabled","Enabled" "Error during retrieval of option value: %s","Error during retrieval of option value: %s" +"Error loading children: %s","Error loading children: %s" "Exclude","Exclude" "Expand All","Expand All" "Failed","Failed" @@ -351,6 +351,7 @@ "Info Column Options Wrapper","Info Column Options Wrapper" "Input Type","Input Type" "Integer","Integer" +"Internal Error","Internal Error" "Interval Division Limit","Interval Division Limit" "Invalid Tier Prices","Invalid Tier Prices" "Invalid attribute %s","Invalid attribute %s" @@ -471,15 +472,17 @@ "PM","PM" "Page Title Separator","Page Title Separator" "Parent Category","Parent Category" +"Parent category is equal to child category.","Parent category is equal to child category." +"Parent category was not found.","Parent category was not found." "Pending","Pending" "Pending Reviews RSS","Pending Reviews RSS" "Percentage","Percentage" "Please add rows to option.","Please add rows to option." "Please be careful as once you click on the row it will load package data form the selected file and all unsaved form data will be lost.","Please be careful as once you click on the row it will load package data form the selected file and all unsaved form data will be lost." "Please click on the Close Window button if it is not closed automatically.","Please click on the Close Window button if it is not closed automatically." -"Please enter a new group name","Please enter a new group name" "Please refresh ""Catalog URL Rewrites"" and ""Product Attributes"" in System -> Index Management","Please refresh ""Catalog URL Rewrites"" and ""Product Attributes"" in System -> Index Management" "Please refresh ""Product Attributes"" in System -> Index Management","Please refresh ""Product Attributes"" in System -> Index Management" +"Please select a parent category before adding a new one.","Please select a parent category before adding a new one." "Please select a static block ...","Please select a static block ..." "Please select items.","Please select items." "Please select one or more attributes.","Please select one or more attributes." @@ -739,8 +742,7 @@ "This attribute is used in configurable products. You cannot remove it from the attribute set.","This attribute is used in configurable products. You cannot remove it from the attribute set." "This attribute no longer exists","This attribute no longer exists" "This attribute set no longer exists.","This attribute set no longer exists." -"This group contains attributes, used in configurable products. Please move these attributes to another group and try again.","This group contains attributes, used in configurable products. Please move these attributes to another group and try again." -"This group contains system attributes. Please move system attributes to another group and try again.","This group contains system attributes. Please move system attributes to another group and try again." +"This category no longer exists.","This category no longer exists." "This is a required option","This is a required option" "This product no longer exists.","This product no longer exists." "This search no longer exists.","This search no longer exists." @@ -810,6 +812,7 @@ "Value for ""%s"" is invalid.","Value for ""%s"" is invalid." "Value for ""%s"" is invalid: %s","Value for ""%s"" is invalid: %s" "Varchar","Varchar" +"View All Products","View All Products" "View Details","View Details" "View as","View as" "Virtual Product","Virtual Product" @@ -840,7 +843,6 @@ "Wrong product type to extract configurable options.","Wrong product type to extract configurable options." "Year Range","Year Range" "Yes","Yes" -"You cannot remove system attribute from this set.","You cannot remove system attribute from this set." "You have no items to compare.","You have no items to compare." "You may also be interested in the following product(s)","You may also be interested in the following product(s)" "You saved the search term.","You saved the search term." diff --git a/app/locale/en_US/Mage_Eav.csv b/app/locale/en_US/Mage_Eav.csv index 13201b521..42432000c 100644 --- a/app/locale/en_US/Mage_Eav.csv +++ b/app/locale/en_US/Mage_Eav.csv @@ -36,15 +36,22 @@ "An error occurred while loading a record, aborting. Error: %s","An error occurred while loading a record, aborting. Error: %s" "An error occurred while loading the collection, aborting. Error: %s","An error occurred while loading the collection, aborting. Error: %s" "An error occurred while saving a record, aborting. Error: ","An error occurred while saving a record, aborting. Error: " +"Are you sure you want to delete this set? Type ""confirm"" to proceed.","Are you sure you want to delete this set? Type ""confirm"" to proceed." "Attempt to add an invalid object","Attempt to add an invalid object" "Attribute '%s' used in configurable products","Attribute '%s' used in configurable products" "Attribute Code","Attribute Code" +"Attribute group with the ""%s"" name already exists","Attribute group with the ""%s"" name already exists" "Attribute Label","Attribute Label" "Attribute Properties","Attribute Properties" "Attribute object is undefined","Attribute object is undefined" "Attribute set with the ""%s"" name already exists.","Attribute set with the ""%s"" name already exists." "Attribute with the same code","Attribute with the same code" "Can't create table: %s","Can't create table: %s" +"Cannot delete group.","Cannot delete group." +"Cannot delete group. Please move system attributes to another group and try again.","Cannot delete group. Please move system attributes to another group and try again." +"Cannot unassign attribute","Cannot unassign attribute" +"Cannot unassign group","Cannot unassign group" +"Cannot unassign system attribute","Cannot unassign system attribute" "Catalog Input Type for Store Owner","Catalog Input Type for Store Owner" "Current module EAV entity is undefined","Current module EAV entity is undefined" "Current module pathname is undefined","Current module pathname is undefined" @@ -102,8 +109,10 @@ "No options found in config node %s","No options found in config node %s" "None","None" "Not shared with other products","Not shared with other products" +"Please enter a new group name","Please enter a new group name" "Problem loading the collection, aborting. Error: %s","Problem loading the collection, aborting. Error: %s" "Problem saving the collection, aborting. Error: %s","Problem saving the collection, aborting. Error: %s" +"Remove attribute from set","Remove attribute from set" "Required","Required" "Saved %d record(s).","Saved %d record(s)." "Source model ""%s"" not found for attribute ""%s""","Source model ""%s"" not found for attribute ""%s""" diff --git a/lib/Varien/Data/Tree.php b/lib/Varien/Data/Tree.php index b073e787d..ad9456462 100644 --- a/lib/Varien/Data/Tree.php +++ b/lib/Varien/Data/Tree.php @@ -7,6 +7,7 @@ * @package Varien_Data * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2020-2024 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ @@ -134,9 +135,23 @@ public function getNodes() return $this->_nodes; } + /** + * Retrieve ids of all nodes in the tree + * + * @return list + */ + public function getAllIds(): array + { + $ids = []; + foreach ($this->getNodes() as $node) { + $ids[] = $node->getId(); + } + return $ids; + } + /** * @param int $nodeId - * @return Varien_Data_Tree_Node + * @return Varien_Data_Tree_Node|null */ public function getNodeById($nodeId) { diff --git a/public/js/extjs/ext-tree-checkbox.js b/public/js/extjs/ext-tree-checkbox.js deleted file mode 100644 index 503d4e654..000000000 --- a/public/js/extjs/ext-tree-checkbox.js +++ /dev/null @@ -1,312 +0,0 @@ -/** - * Retrieve an array of ids of checked nodes - * @return {Array} array of ids of checked nodes - */ -Ext.tree.TreePanel.prototype.getChecked = function(node){ - var checked = [], i; - if( typeof node == 'undefined' ) { - //node = this.rootVisible ? this.getRootNode() : this.getRootNode().firstChild; - node = this.getRootNode(); - } - - if( node.attributes.checked ) { - checked.push(node.id); - } - if( node.childNodes.length ) { - for( i = 0; i < node.childNodes.length; i++ ) { - checked = checked.concat( this.getChecked(node.childNodes[i]) ); - } - } - - return checked; -}; - -/** - * @class Ext.tree.CustomUITreeLoader - * @extends Ext.tree.TreeLoader - * Overrides createNode to force uiProvider to be an arbitrary TreeNodeUI to save bandwidth - */ -Ext.tree.CustomUITreeLoader = function() { - Ext.tree.CustomUITreeLoader.superclass.constructor.apply(this, arguments); -}; - -Ext.extend(Ext.tree.CustomUITreeLoader, Ext.tree.TreeLoader, { - createNode : function(attr){ - Ext.apply(attr, this.baseAttr || {}); - - if(this.applyLoader !== false){ - attr.loader = this; - } - - if(typeof attr.uiProvider == 'string'){ - attr.uiProvider = this.uiProviders[attr.uiProvider] || eval(attr.uiProvider); - } - - return(attr.leaf ? - new Ext.tree.TreeNode(attr) : - new Ext.tree.AsyncTreeNode(attr)); - } -}); - - -/** - * @class Ext.tree.CheckboxNodeUI - * @extends Ext.tree.TreeNodeUI - * Adds a checkbox to all nodes - */ -Ext.tree.CheckboxNodeUI = function() { - Ext.tree.CheckboxNodeUI.superclass.constructor.apply(this, arguments); -}; - -Ext.extend(Ext.tree.CheckboxNodeUI, Ext.tree.TreeNodeUI, { - /** - * This is virtually identical to Ext.tree.TreeNodeUI.render, modifications are indicated inline - */ - render : function(bulkRender){ - var n = this.node; - var targetNode = n.parentNode ? - n.parentNode.ui.getContainer() : n.ownerTree.innerCt.dom; /* in previous svn builds this was n.ownerTree.container.dom */ - if(!this.rendered){ - this.rendered = true; - var a = n.attributes; - - // add some indent caching, this helps performance when rendering a large tree - this.indentMarkup = ""; - if(n.parentNode){ - this.indentMarkup = n.parentNode.ui.getChildIndent(); - } - - // modification: added checkbox - var buf = ['
  • ', - '',this.indentMarkup,"", - '', - '', - '" : '>'), - '', - '',n.text,"
    ", - '', - "
  • "]; - - if(bulkRender !== true && n.nextSibling && n.nextSibling.ui.getEl()){ - this.wrap = Ext.DomHelper.insertHtml("beforeBegin", - n.nextSibling.ui.getEl(), buf.join("")); - }else{ - this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf.join("")); - } - this.elNode = this.wrap.childNodes[0]; - this.ctNode = this.wrap.childNodes[1]; - var cs = this.elNode.childNodes; - this.indentNode = cs[0]; - this.ecNode = cs[1]; - this.iconNode = cs[2]; - this.checkbox = cs[3]; // modification: inserted checkbox - this.anchor = cs[4]; - this.textNode = cs[4].firstChild; - if(a.qtip){ - if(this.textNode.setAttributeNS){ - this.textNode.setAttributeNS("ext", "qtip", a.qtip); - if(a.qtipTitle){ - this.textNode.setAttributeNS("ext", "qtitle", a.qtipTitle); - } - }else{ - this.textNode.setAttribute("ext:qtip", a.qtip); - if(a.qtipTitle){ - this.textNode.setAttribute("ext:qtitle", a.qtipTitle); - } - } - } else if(a.qtipCfg) { - a.qtipCfg.target = Ext.id(this.textNode); - Ext.QuickTips.register(a.qtipCfg); - } - - this.initEvents(); - - // modification: Add additional handlers here to avoid modifying Ext.tree.TreeNodeUI - Ext.fly(this.checkbox).on('click', this.check.createDelegate(this, [null])); - n.on('dblclick', function(e) { - if( this.isLeaf() ) { - this.getUI().toggleCheck(); - } - }); - - if(!this.node.expanded){ - this.updateExpandIcon(); - } - }else{ - if(bulkRender === true) { - targetNode.appendChild(this.wrap); - } - } - }, - - checked : function() { - return this.checkbox.checked; - }, - - /** - * Sets a checkbox appropriately. By default only walks down through child nodes - * if called with no arguments (onchange event from the checkbox), otherwise - * it's assumed the call is being made programatically and the correct arguments are provided. - * @param {Boolean} state true to check the checkbox, false to clear it. (defaults to the opposite of the checkbox.checked) - * @param {Boolean} descend true to walk through the nodes children and set their checkbox values. (defaults to false) - */ - check : function(state, descend, bulk) { - if (this.node.disabled) { - return; - } - var n = this.node; - var tree = n.getOwnerTree(); - var parentNode = n.parentNode;n - if( !n.expanded && !n.childrenRendered ) { - n.expand(false, false, this.check.createDelegate(this, arguments)); - } - - if( typeof bulk == 'undefined' ) { - bulk = false; - } - if( typeof state == 'undefined' || state === null ) { - state = this.checkbox.checked; - descend = !state; - if( state ) { - n.expand(false, false); - } - } else { - this.checkbox.checked = state; - } - n.attributes.checked = state; - - // do we have parents? - if( parentNode !== null && state ) { - // if we're checking the box, check it all the way up - if( parentNode.getUI().check ) { - //parentNode.getUI().check(state, false, true); - } - } - if( descend && !n.isLeaf() ) { - var cs = n.childNodes; - for(var i = 0; i < cs.length; i++) { - //cs[i].getUI().check(state, true, true); - } - } - if( !bulk ) { - tree.fireEvent('check', n, state); - } - }, - - toggleCheck : function(state) { - this.check(!this.checkbox.checked, true); - } - -}); - - -/** - * @class Ext.tree.CheckNodeMultiSelectionModel - * @extends Ext.tree.MultiSelectionModel - * Multi selection for a TreePanel containing Ext.tree.CheckboxNodeUI. - * Adds enhanced selection routines for selecting multiple items - * and key processing to check/clear checkboxes. - */ -Ext.tree.CheckNodeMultiSelectionModel = function(){ - Ext.tree.CheckNodeMultiSelectionModel.superclass.constructor.call(this); -}; - -Ext.extend(Ext.tree.CheckNodeMultiSelectionModel, Ext.tree.MultiSelectionModel, { - init : function(tree){ - this.tree = tree; - tree.el.on("keydown", this.onKeyDown, this); - tree.on("click", this.onNodeClick, this); - }, - - /** - * Handle a node click - * If ctrl key is down and node is selected will unselect the node. - * If the shift key is down it will create a contiguous selection - * (see {@link Ext.tree.CheckNodeMultiSelectionModel#extendSelection} for the limitations) - */ - onNodeClick : function(node, e){ - if (node.disabled) { - return; - } - if( e.shiftKey && this.extendSelection(node) ) { - return true; - } - if( e.ctrlKey && this.isSelected(node) ) { - this.unselect(node); - } else { - this.select(node, e, e.ctrlKey); - } - }, - - /** - * Selects all nodes between the previously selected node and the one that the user has just selected. - * Will not span multiple depths, so only children of the same parent will be selected. - */ - extendSelection : function(node) { - var last = this.lastSelNode; - if( node == last || !last ) { - return false; /* same selection, process normally normally */ - } - - if( node.parentNode == last.parentNode ) { - var cs = node.parentNode.childNodes; - var i = 0, attr='id', selecting=false, lastSelect=false; - this.clearSelections(true); - for( i = 0; i < cs.length; i++ ) { - // We have to traverse the entire tree b/c don't know of a way to find - // a numerical representation of a nodes position in a tree. - if( cs[i].attributes[attr] == last.attributes[attr] || cs[i].attributes[attr] == node.attributes[attr] ) { - // lastSelect ensures that we select the final node in the list - lastSelect = selecting; - selecting = !selecting; - } - if( selecting || lastSelect ) { - this.select(cs[i], null, true); - // if we're selecting the last node break to avoid traversing the entire tree - if( lastSelect ) { - break; - } - } - } - return true; - } else { - return false; - } - }, - - /** - * Traps the press of the SPACE bar and sets the check state of selected nodes to the opposite state of - * the selected or last selected node. Assume you have the following five Ext.tree.CheckboxNodeUIs: - * [X] One, [X] Two, [X] Three, [ ] Four, [ ] Five - * If you select them in this order: One, Two, Three, Four, Five and press the space bar they all - * will be checked (the opposite of the checkbox state of Five). - * If you select them in this order: Five, Four, Three, Two, One and press the space bar they all - * will be unchecked which is the opposite of the checkbox state of One. - */ - onKeyDown : Ext.tree.DefaultSelectionModel.prototype.onKeyDown.createInterceptor(function(e) { - var s = this.selNode || this.lastSelNode; - // undesirable, but required - var sm = this; - if(!s){ - return; - } - var k = e.getKey(); - switch(k){ - case e.SPACE: - e.stopEvent(); - var sel = this.getSelectedNodes(); - var state = !s.getUI().checked(); - if( sel.length == 1 ) { - s.getUI().check(state, !s.isLeaf()); - } else { - for( var i = 0; i < sel.length; i++ ) { - sel[i].getUI().check(state, !sel[i].isLeaf() ); - } - } - break; - } - - return true; - }) -}); diff --git a/public/js/extjs/ext-tree.min.js b/public/js/extjs/ext-tree.min.js deleted file mode 100644 index cd0603ea6..000000000 --- a/public/js/extjs/ext-tree.min.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Ext JS Library 1.1.1 - * Copyright (c) 2006-2007, Ext JS, LLC - * All rights reserved. - * licensing@extjs.com - * - * http://extjs.com/license - * - * The CSS and Graphics ("Assets") distributed with Ext are licensed for use ONLY - * with their associated Ext JavaScript component ("Component"). Use of the Assets in - * any way that does not also include the Component is prohibited without explicit - * permission from Ext JS, LLC. Deriving images and CSS from the Assets in an effort - * to bypass this license is also prohibited. - * - * -- - * - * The JavaScript code distributed with Ext (the "Software") is licensed under the - * Lesser GNU (LGPL) open source license version 3.0. - * - * http://www.gnu.org/licenses/lgpl.html - * - * If you are using this library for commercial purposes, we encourage you to purchase - * a commercial license. Please visit http://extjs.com/license for more details. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - */ - -Ext={};window["undefined"]=window["undefined"];Ext.apply=function(C,D,B){if(B){Ext.apply(C,B)}if(C&&D&&typeof D=="object"){for(var A in D){C[A]=D[A]}}return C};(function(){var idSeed=0;var ua=navigator.userAgent.toLowerCase();var isStrict=document.compatMode=="CSS1Compat",isOpera=ua.indexOf("opera")>-1,isSafari=(/webkit|khtml/).test(ua),isIE=ua.indexOf("msie")>-1,isIE7=ua.indexOf("msie 7")>-1,isGecko=!isSafari&&ua.indexOf("gecko")>-1,isBorderBox=isIE&&!isStrict,isWindows=(ua.indexOf("windows")!=-1||ua.indexOf("win32")!=-1),isMac=(ua.indexOf("macintosh")!=-1||ua.indexOf("mac os x")!=-1),isLinux=(ua.indexOf("linux")!=-1),isSecure=window.location.href.toLowerCase().indexOf("https")===0;if(isIE&&!isIE7){try{document.execCommand("BackgroundImageCache",false,true)}catch(e){}}Ext.apply(Ext,{isStrict:isStrict,isSecure:isSecure,isReady:false,enableGarbageCollector:true,enableListenerCollection:false,SSL_SECURE_URL:"javascript:false",BLANK_IMAGE_URL:"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=",emptyFn:function(){},applyIf:function(o,c){if(o&&c){for(var p in c){if(typeof o[p]=="undefined"){o[p]=c[p]}}}return o},addBehaviors:function(o){if(!Ext.isReady){Ext.onReady(function(){Ext.addBehaviors(o)});return }var cache={};for(var b in o){var parts=b.split("@");if(parts[1]){var s=parts[0];if(!cache[s]){cache[s]=Ext.select(s)}cache[s].on(parts[1],o[b])}}cache=null},id:function(el,prefix){prefix=prefix||"ext-gen";el=Ext.getDom(el);var id=prefix+(++idSeed);return el?(el.id?el.id:(el.id=id)):id},extend:function(){var io=function(o){for(var m in o){this[m]=o[m]}};return function(sb,sp,overrides){if(typeof sp=="object"){overrides=sp;sp=sb;sb=function(){sp.apply(this,arguments)}}var F=function(){},sbp,spp=sp.prototype;F.prototype=spp;sbp=sb.prototype=new F();sbp.constructor=sb;sb.superclass=spp;if(spp.constructor==Object.prototype.constructor){spp.constructor=sp}sb.override=function(o){Ext.override(sb,o)};sbp.override=io;Ext.override(sb,overrides);return sb}}(),override:function(origclass,overrides){if(overrides){var p=origclass.prototype;for(var method in overrides){p[method]=overrides[method]}}},namespace:function(){var a=arguments,o=null,i,j,d,rt;for(i=0;i10000){clearInterval(G)}var J=document.getElementById(I);if(J){clearInterval(G);E.call(D||window,J)}};G=setInterval(F,50)}};Ext.lib.Ajax=function(){var E=function(F){return F.success?function(G){F.success.call(F.scope||window,{responseText:G.responseText,responseXML:G.responseXML,argument:F.argument})}:Ext.emptyFn};var D=function(F){return F.failure?function(G){F.failure.call(F.scope||window,{responseText:G.responseText,responseXML:G.responseXML,argument:F.argument})}:Ext.emptyFn};return{request:function(K,H,F,I,G){var J={method:K,parameters:I||"",timeout:F.timeout,onSuccess:E(F),onFailure:D(F)};if(G){if(G.headers){J.requestHeaders=G.headers}if(G.xmlData){K="POST";J.contentType="text/xml";J.postBody=G.xmlData;delete J.parameters}}new Ajax.Request(H,J)},formRequest:function(J,I,G,K,F,H){new Ajax.Request(I,{method:Ext.getDom(J).method||"POST",parameters:Form.serialize(J)+(K?"&"+K:""),timeout:G.timeout,onSuccess:E(G),onFailure:D(G)})},isCallInProgress:function(F){return false},abort:function(F){return false},serializeForm:function(F){return Form.serialize(F.dom||F)}}}();Ext.lib.Anim=function(){var D={easeOut:function(F){return 1-Math.pow(1-F,2)},easeIn:function(F){return 1-Math.pow(1-F,2)}};var E=function(F,G){return{stop:function(H){this.effect.cancel()},isAnimated:function(){return this.effect.state=="running"},proxyCallback:function(){Ext.callback(F,G)}}};return{scroll:function(I,G,K,L,F,H){var J=E(F,H);I=Ext.getDom(I);if(typeof G.scroll.to[0]=="number"){I.scrollLeft=G.scroll.to[0]}if(typeof G.scroll.to[1]=="number"){I.scrollTop=G.scroll.to[1]}J.proxyCallback();return J},motion:function(I,G,J,K,F,H){return this.run(I,G,J,K,F,H)},color:function(I,G,J,K,F,H){return this.run(I,G,J,K,F,H)},run:function(G,O,K,N,H,Q,P){var F={};for(var J in O){switch(J){case"points":var M,S,L=Ext.fly(G,"_animrun");L.position();if(M=O.points.by){var R=L.getXY();S=L.translatePoints([R[0]+M[0],R[1]+M[1]])}else{S=L.translatePoints(O.points.to)}F.left=S.left+"px";F.top=S.top+"px";break;case"width":F.width=O.width.to+"px";break;case"height":F.height=O.height.to+"px";break;case"opacity":F.opacity=String(O.opacity.to);break;default:F[J]=String(O[J].to);break}}var I=E(H,Q);I.effect=new Effect.Morph(Ext.id(G),{duration:K,afterFinish:I.proxyCallback,transition:D[N]||Effect.Transitions.linear,style:F});return I}}}();function C(D){if(!B){B=new Ext.Element.Flyweight()}B.dom=D;return B}Ext.lib.Region=function(F,G,D,E){this.top=F;this[1]=F;this.right=G;this.bottom=D;this.left=E;this[0]=E};Ext.lib.Region.prototype={contains:function(D){return(D.left>=this.left&&D.right<=this.right&&D.top>=this.top&&D.bottom<=this.bottom)},getArea:function(){return((this.bottom-this.top)*(this.right-this.left))},intersect:function(H){var F=Math.max(this.top,H.top);var G=Math.min(this.right,H.right);var D=Math.min(this.bottom,H.bottom);var E=Math.max(this.left,H.left);if(D>=F&&G>=E){return new Ext.lib.Region(F,G,D,E)}else{return null}},union:function(H){var F=Math.min(this.top,H.top);var G=Math.max(this.right,H.right);var D=Math.max(this.bottom,H.bottom);var E=Math.min(this.left,H.left);return new Ext.lib.Region(F,G,D,E)},adjust:function(F,E,D,G){this.top+=F;this.left+=E;this.right+=G;this.bottom+=D;return this}};Ext.lib.Region.getRegion=function(G){var I=Ext.lib.Dom.getXY(G);var F=I[1];var H=I[0]+G.offsetWidth;var D=I[1]+G.offsetHeight;var E=I[0];return new Ext.lib.Region(F,H,D,E)};Ext.lib.Point=function(D,E){if(D instanceof Array){E=D[1];D=D[0]}this.x=this.right=this.left=this[0]=D;this.y=this.top=this.bottom=this[1]=E};Ext.lib.Point.prototype=new Ext.lib.Region();if(Ext.isIE){function A(){var D=Function.prototype;delete D.createSequence;delete D.defer;delete D.createDelegate;delete D.createCallback;delete D.createInterceptor;window.detachEvent("onunload",A)}window.attachEvent("onunload",A)}})(); - - - -Ext.DomHelper=function(){var L=null;var F=/^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i;var B=/^table|tbody|tr|td$/i;var A=function(T){if(typeof T=="string"){return T}var P="";if(!T.tag){T.tag="div"}P+="<"+T.tag;for(var O in T){if(O=="tag"||O=="children"||O=="cn"||O=="html"||typeof T[O]=="function"){continue}if(O=="style"){var S=T["style"];if(typeof S=="function"){S=S.call()}if(typeof S=="string"){P+=" style=\""+S+"\""}else{if(typeof S=="object"){P+=" style=\"";for(var R in S){if(typeof S[R]!="function"){P+=R+":"+S[R]+";"}}P+="\""}}}else{if(O=="cls"){P+=" class=\""+T["cls"]+"\""}else{if(O=="htmlFor"){P+=" for=\""+T["htmlFor"]+"\""}else{P+=" "+O+"=\""+T[O]+"\""}}}}if(F.test(T.tag)){P+="/>"}else{P+=">";var U=T.children||T.cn;if(U){if(U instanceof Array){for(var Q=0,N=U.length;Q"}return P};var M=function(T,P){var S=document.createElement(T.tag||"div");var Q=S.setAttribute?true:false;for(var O in T){if(O=="tag"||O=="children"||O=="cn"||O=="html"||O=="style"||typeof T[O]=="function"){continue}if(O=="cls"){S.className=T["cls"]}else{if(Q){S.setAttribute(O,T[O])}else{S[O]=T[O]}}}Ext.DomHelper.applyStyles(S,T.style);var U=T.children||T.cn;if(U){if(U instanceof Array){for(var R=0,N=U.length;R",K=""+E,H=C+"",D=""+K;var G=function(N,O,Q,P){if(!L){L=document.createElement("div")}var R;var S=null;if(N=="td"){if(O=="afterbegin"||O=="beforeend"){return }if(O=="beforebegin"){S=Q;Q=Q.parentNode}else{S=Q.nextSibling;Q=Q.parentNode}R=I(4,H,P,D)}else{if(N=="tr"){if(O=="beforebegin"){S=Q;Q=Q.parentNode;R=I(3,C,P,K)}else{if(O=="afterend"){S=Q.nextSibling;Q=Q.parentNode;R=I(3,C,P,K)}else{if(O=="afterbegin"){S=Q.firstChild}R=I(4,H,P,D)}}}else{if(N=="tbody"){if(O=="beforebegin"){S=Q;Q=Q.parentNode;R=I(2,J,P,E)}else{if(O=="afterend"){S=Q.nextSibling;Q=Q.parentNode;R=I(2,J,P,E)}else{if(O=="afterbegin"){S=Q.firstChild}R=I(3,C,P,K)}}}else{if(O=="beforebegin"||O=="afterend"){return }if(O=="afterbegin"){S=Q.firstChild}R=I(2,J,P,E)}}}Q.insertBefore(R,S);return R};return{useDom:false,markup:function(N){return A(N)},applyStyles:function(P,Q){if(Q){P=Ext.fly(P);if(typeof Q=="string"){var O=/\s?([a-z\-]*)\:\s?([^;]*);?/gi;var R;while((R=O.exec(Q))!=null){P.setStyle(R[1],R[2])}}else{if(typeof Q=="object"){for(var N in Q){P.setStyle(N,Q[N])}}else{if(typeof Q=="function"){Ext.DomHelper.applyStyles(P,Q.call())}}}}},insertHtml:function(P,R,Q){P=P.toLowerCase();if(R.insertAdjacentHTML){if(B.test(R.tagName)){var O;if(O=G(R.tagName.toLowerCase(),P,R,Q)){return O}}switch(P){case"beforebegin":R.insertAdjacentHTML("BeforeBegin",Q);return R.previousSibling;case"afterbegin":R.insertAdjacentHTML("AfterBegin",Q);return R.firstChild;case"beforeend":R.insertAdjacentHTML("BeforeEnd",Q);return R.lastChild;case"afterend":R.insertAdjacentHTML("AfterEnd",Q);return R.nextSibling}throw"Illegal insertion point -> \""+P+"\""}var N=R.ownerDocument.createRange();var S;switch(P){case"beforebegin":N.setStartBefore(R);S=N.createContextualFragment(Q);R.parentNode.insertBefore(S,R);return R.previousSibling;case"afterbegin":if(R.firstChild){N.setStartBefore(R.firstChild);S=N.createContextualFragment(Q);R.insertBefore(S,R.firstChild);return R.firstChild}else{R.innerHTML=Q;return R.firstChild}case"beforeend":if(R.lastChild){N.setStartAfter(R.lastChild);S=N.createContextualFragment(Q);R.appendChild(S);return R.lastChild}else{R.innerHTML=Q;return R.lastChild}case"afterend":N.setStartAfter(R);S=N.createContextualFragment(Q);R.parentNode.insertBefore(S,R.nextSibling);return R.nextSibling}throw"Illegal insertion point -> \""+P+"\""},insertBefore:function(N,P,O){return this.doInsert(N,P,O,"beforeBegin")},insertAfter:function(N,P,O){return this.doInsert(N,P,O,"afterEnd","nextSibling")},insertFirst:function(N,P,O){return this.doInsert(N,P,O,"afterBegin")},doInsert:function(Q,S,R,T,P){Q=Ext.getDom(Q);var O;if(this.useDom){O=M(S,null);Q.parentNode.insertBefore(O,P?Q[P]:Q)}else{var N=A(S);O=this.insertHtml(T,Q,N)}return R?Ext.get(O,true):O},append:function(P,R,Q){P=Ext.getDom(P);var O;if(this.useDom){O=M(R,null);P.appendChild(O)}else{var N=A(R);O=this.insertHtml("beforeEnd",P,N)}return Q?Ext.get(O,true):O},overwrite:function(N,P,O){N=Ext.getDom(N);N.innerHTML=A(P);return O?Ext.get(N.firstChild,true):N.firstChild},createTemplate:function(O){var N=A(O);return new Ext.Template(N)}}}(); - - - -Ext.util.Observable=function(){if(this.listeners){this.on(this.listeners);delete this.listeners}};Ext.util.Observable.prototype={fireEvent:function(){var A=this.events[arguments[0].toLowerCase()];if(typeof A=="object"){return A.fire.apply(A,Array.prototype.slice.call(arguments,1))}else{return true}},filterOptRe:/^(?:scope|delay|buffer|single)$/,addListener:function(A,C,B,F){if(typeof A=="object"){F=A;for(var E in F){if(this.filterOptRe.test(E)){continue}if(typeof F[E]=="function"){this.addListener(E,F[E],F.scope,F)}else{this.addListener(E,F[E].fn,F[E].scope,F[E])}}return }F=(!F||typeof F=="boolean")?{}:F;A=A.toLowerCase();var D=this.events[A]||true;if(typeof D=="boolean"){D=new Ext.util.Event(this,A);this.events[A]=D}D.addListener(C,B,F)},removeListener:function(A,C,B){var D=this.events[A.toLowerCase()];if(typeof D=="object"){D.removeListener(C,B)}},purgeListeners:function(){for(var A in this.events){if(typeof this.events[A]=="object"){this.events[A].clearListeners()}}},relayEvents:function(F,D){var E=function(G){return function(){return this.fireEvent.apply(this,Ext.combine(G,Array.prototype.slice.call(arguments,0)))}};for(var C=0,A=D.length;C0}};Ext.util.Observable.prototype.on=Ext.util.Observable.prototype.addListener;Ext.util.Observable.prototype.un=Ext.util.Observable.prototype.removeListener;Ext.util.Observable.capture=function(C,B,A){C.fireEvent=C.fireEvent.createInterceptor(B,A)};Ext.util.Observable.releaseCapture=function(A){A.fireEvent=Ext.util.Observable.prototype.fireEvent};(function(){var B=function(F,G,E){var D=new Ext.util.DelayedTask();return function(){D.delay(G.buffer,F,E,Array.prototype.slice.call(arguments,0))}};var C=function(F,G,E,D){return function(){G.removeListener(E,D);return F.apply(D,arguments)}};var A=function(E,F,D){return function(){var G=Array.prototype.slice.call(arguments,0);setTimeout(function(){E.apply(D,G)},F.delay||10)}};Ext.util.Event=function(E,D){this.name=D;this.obj=E;this.listeners=[]};Ext.util.Event.prototype={addListener:function(H,G,E){var I=E||{};G=G||this.obj;if(!this.isListening(H,G)){var D={fn:H,scope:G,options:I};var F=H;if(I.delay){F=A(F,I,G)}if(I.single){F=C(F,this,H,G)}if(I.buffer){F=B(F,I,G)}D.fireFn=F;if(!this.firing){this.listeners.push(D)}else{this.listeners=this.listeners.slice(0);this.listeners.push(D)}}},findListener:function(I,H){H=H||this.obj;var F=this.listeners;for(var G=0,D=F.length;G0){this.firing=true;var G=Array.prototype.slice.call(arguments,0);for(var H=0;H");var D=document.getElementById("ie-deferred-loader");D.onreadystatechange=function(){if(this.readyState=="complete"){B()}}}else{if(Ext.isSafari){M=setInterval(function(){var E=document.readyState;if(E=="complete"){B()}},10)}}}L.on(window,"load",B)};var R=function(E,U){var D=new Ext.util.DelayedTask(E);return function(V){V=new Ext.EventObjectImpl(V);D.delay(U.buffer,E,null,[V])}};var P=function(V,U,D,E){return function(W){Ext.EventManager.removeListener(U,D,E);V(W)}};var F=function(D,E){return function(U){U=new Ext.EventObjectImpl(U);setTimeout(function(){D(U)},E.delay||10)}};var J=function(U,E,D,Y,X){var Z=(!D||typeof D=="boolean")?{}:D;Y=Y||Z.fn;X=X||Z.scope;var W=Ext.getDom(U);if(!W){throw"Error listening for \""+E+"\". Element \""+U+"\" doesn't exist."}var V=function(b){b=Ext.EventObject.setEvent(b);var a;if(Z.delegate){a=b.getTarget(Z.delegate,W);if(!a){return }}else{a=b.target}if(Z.stopEvent===true){b.stopEvent()}if(Z.preventDefault===true){b.preventDefault()}if(Z.stopPropagation===true){b.stopPropagation()}if(Z.normalized===false){b=b.browserEvent}Y.call(X||W,b,a,Z)};if(Z.delay){V=F(V,Z)}if(Z.single){V=P(V,W,E,Y)}if(Z.buffer){V=R(V,Z)}Y._handlers=Y._handlers||[];Y._handlers.push([Ext.id(W),E,V]);L.on(W,E,V);if(E=="mousewheel"&&W.addEventListener){W.addEventListener("DOMMouseScroll",V,false);L.on(window,"beforeunload",function(){W.removeEventListener("DOMMouseScroll",V,false)})}if(E=="mousedown"&&W==document){Ext.EventManager.stoppedMouseDownEvent.addListener(V)}return V};var G=function(E,U,Z){var D=Ext.id(E),a=Z._handlers,X=Z;if(a){for(var V=0,Y=a.length;V=33&&D<=40)||D==this.RETURN||D==this.TAB||D==this.ESC},isSpecialKey:function(){var D=this.keyCode;return(this.type=="keypress"&&this.ctrlKey)||D==9||D==13||D==40||D==27||(D==16)||(D==17)||(D>=18&&D<=20)||(D>=33&&D<=35)||(D>=36&&D<=39)||(D>=44&&D<=45)},stopPropagation:function(){if(this.browserEvent){if(this.type=="mousedown"){Ext.EventManager.stoppedMouseDownEvent.fire(this)}B.stopPropagation(this.browserEvent)}},getCharCode:function(){return this.charCode||this.keyCode},getKey:function(){var D=this.keyCode||this.charCode;return Ext.isSafari?(A[D]||D):D},getPageX:function(){return this.xy[0]},getPageY:function(){return this.xy[1]},getTime:function(){if(this.browserEvent){return B.getTime(this.browserEvent)}return null},getXY:function(){return this.xy},getTarget:function(E,F,D){return E?Ext.fly(this.target).findParent(E,F,D):this.target},getRelatedTarget:function(){if(this.browserEvent){return B.getRelatedTarget(this.browserEvent)}return null},getWheelDelta:function(){var D=this.browserEvent;var E=0;if(D.wheelDelta){E=D.wheelDelta/120}else{if(D.detail){E=-D.detail/3}}return E},hasModifier:function(){return !!((this.ctrlKey||this.altKey)||this.shiftKey)},within:function(E,F){var D=this[F?"getRelatedTarget":"getTarget"]();return D&&Ext.fly(E).contains(D)},getPoint:function(){return new Ext.lib.Point(this.xy[0],this.xy[1])}};return new Ext.EventObjectImpl()}(); - - - -(function(){var D=Ext.lib.Dom;var E=Ext.lib.Event;var A=Ext.lib.Anim;var propCache={};var camelRe=/(-[a-z])/gi;var camelFn=function(m,a){return a.charAt(1).toUpperCase()};var view=document.defaultView;Ext.Element=function(element,forceNew){var dom=typeof element=="string"?document.getElementById(element):element;if(!dom){return null}var id=dom.id;if(forceNew!==true&&id&&Ext.Element.cache[id]){return Ext.Element.cache[id]}this.dom=dom;this.id=id||Ext.id(dom)};var El=Ext.Element;El.prototype={originalDisplay:"",visibilityMode:1,defaultUnit:"px",setVisibilityMode:function(visMode){this.visibilityMode=visMode;return this},enableDisplayMode:function(display){this.setVisibilityMode(El.DISPLAY);if(typeof display!="undefined"){this.originalDisplay=display}return this},findParent:function(simpleSelector,maxDepth,returnEl){var p=this.dom,b=document.body,depth=0,dq=Ext.DomQuery,stopEl;maxDepth=maxDepth||50;if(typeof maxDepth!="number"){stopEl=Ext.getDom(maxDepth);maxDepth=10}while(p&&p.nodeType==1&&depthcb){c.scrollTop=b-ch}}if(hscroll!==false){if(lcr){c.scrollLeft=r-c.clientWidth}}}return this},scrollChildIntoView:function(child,hscroll){Ext.fly(child,"_scrollChildIntoView").scrollIntoView(this,hscroll)},autoHeight:function(animate,duration,onComplete,easing){var oldHeight=this.getHeight();this.clip();this.setHeight(1);setTimeout(function(){var height=parseInt(this.dom.scrollHeight,10);if(!animate){this.setHeight(height);this.unclip();if(typeof onComplete=="function"){onComplete()}}else{this.setHeight(oldHeight);this.setHeight(height,animate,duration,function(){this.unclip();if(typeof onComplete=="function"){onComplete()}}.createDelegate(this),easing)}}.createDelegate(this),0);return this},contains:function(el){if(!el){return false}return D.isAncestor(this.dom,el.dom?el.dom:el)},isVisible:function(deep){var vis=!(this.getStyle("visibility")=="hidden"||this.getStyle("display")=="none");if(deep!==true||!vis){return vis}var p=this.dom.parentNode;while(p&&p.tagName.toLowerCase()!="body"){if(!Ext.fly(p,"_isVisible").isVisible()){return false}p=p.parentNode}return true},select:function(selector,unique){return El.select(selector,unique,this.dom)},query:function(selector,unique){return Ext.DomQuery.select(selector,this.dom)},child:function(selector,returnDom){var n=Ext.DomQuery.selectNode(selector,this.dom);return returnDom?n:Ext.get(n)},down:function(selector,returnDom){var n=Ext.DomQuery.selectNode(" > "+selector,this.dom);return returnDom?n:Ext.get(n)},initDD:function(group,config,overrides){var dd=new Ext.dd.DD(Ext.id(this.dom),group,config);return Ext.apply(dd,overrides)},initDDProxy:function(group,config,overrides){var dd=new Ext.dd.DDProxy(Ext.id(this.dom),group,config);return Ext.apply(dd,overrides)},initDDTarget:function(group,config,overrides){var dd=new Ext.dd.DDTarget(Ext.id(this.dom),group,config);return Ext.apply(dd,overrides)},setVisible:function(visible,animate){if(!animate||!A){if(this.visibilityMode==El.DISPLAY){this.setDisplayed(visible)}else{this.fixDisplay();this.dom.style.visibility=visible?"visible":"hidden"}}else{var dom=this.dom;var visMode=this.visibilityMode;if(visible){this.setOpacity(0.01);this.setVisible(true)}this.anim({opacity:{to:(visible?1:0)}},this.preanim(arguments,1),null,0.35,"easeIn",function(){if(!visible){if(visMode==El.DISPLAY){dom.style.display="none"}else{dom.style.visibility="hidden"}Ext.get(dom).setOpacity(1)}})}return this},isDisplayed:function(){return this.getStyle("display")!="none"},toggle:function(animate){this.setVisible(!this.isVisible(),this.preanim(arguments,0));return this},setDisplayed:function(value){if(typeof value=="boolean"){value=value?this.originalDisplay:"none"}this.setStyle("display",value);return this},focus:function(){try{this.dom.focus()}catch(e){}return this},blur:function(){try{this.dom.blur()}catch(e){}return this},addClass:function(className){if(className instanceof Array){for(var i=0,len=className.length;idw+scrollX){x=swapX?r.left-w:dw+scrollX-w}if(xdh+scrollY){y=swapY?r.top-h:dh+scrollY-h}if(yvr){x=vr-w;moved=true}if((y+h)>vb){y=vb-h;moved=true}if(x";E.onAvailable(id,function(){var hd=document.getElementsByTagName("head")[0];var re=/(?:]*)?>)((\n|\r|.)*?)(?:<\/script>)/ig;var srcRe=/\ssrc=([\'\"])(.*?)\1/i;var typeRe=/\stype=([\'\"])(.*?)\1/i;var match;while(match=re.exec(html)){var attrs=match[1];var srcMatch=attrs?attrs.match(srcRe):false;if(srcMatch&&srcMatch[2]){var s=document.createElement("script");s.src=srcMatch[2];var typeMatch=attrs.match(typeRe);if(typeMatch&&typeMatch[2]){s.type=typeMatch[2]}hd.appendChild(s)}else{if(match[2]&&match[2].length>0){if(window.execScript){window.execScript(match[2])}else{window.eval(match[2])}}}}var el=document.getElementById(id);if(el){el.parentNode.removeChild(el)}if(typeof callback=="function"){callback()}});dom.innerHTML=html.replace(/(?:)((\n|\r|.)*?)(?:<\/script>)/ig,"");return this},load:function(){var um=this.getUpdateManager();um.update.apply(um,arguments);return this},getUpdateManager:function(){if(!this.updateManager){this.updateManager=new Ext.UpdateManager(this)}return this.updateManager},unselectable:function(){this.dom.unselectable="on";this.swallowEvent("selectstart",true);this.applyStyles("-moz-user-select:none;-khtml-user-select:none;");this.addClass("x-unselectable");return this},getCenterXY:function(){return this.getAlignToXY(document,"c-c")},center:function(centerIn){this.alignTo(centerIn||document,"c-c");return this},isBorderBox:function(){return noBoxAdjust[this.dom.tagName.toLowerCase()]||Ext.isBorderBox},getBox:function(contentBox,local){var xy;if(!local){xy=this.getXY()}else{var left=parseInt(this.getStyle("left"),10)||0;var top=parseInt(this.getStyle("top"),10)||0;xy=[left,top]}var el=this.dom,w=el.offsetWidth,h=el.offsetHeight,bx;if(!contentBox){bx={x:xy[0],y:xy[1],0:xy[0],1:xy[1],width:w,height:h}}else{var l=this.getBorderWidth("l")+this.getPadding("l");var r=this.getBorderWidth("r")+this.getPadding("r");var t=this.getBorderWidth("t")+this.getPadding("t");var b=this.getBorderWidth("b")+this.getPadding("b");bx={x:xy[0]+l,y:xy[1]+t,0:xy[0]+l,1:xy[1]+t,width:w-(l+r),height:h-(t+b)}}bx.right=bx.x+bx.width;bx.bottom=bx.y+bx.height;return bx},getFrameWidth:function(sides,onlyContentBox){return onlyContentBox&&Ext.isBorderBox?0:(this.getPadding(sides)+this.getBorderWidth(sides))},setBox:function(box,adjust,animate){var w=box.width,h=box.height;if((adjust&&!this.autoBoxAdjust)&&!this.isBorderBox()){w-=(this.getBorderWidth("lr")+this.getPadding("lr"));h-=(this.getBorderWidth("tb")+this.getPadding("tb"))}this.setBounds(box.x,box.y,w,h,this.preanim(arguments,2));return this},repaint:function(){var dom=this.dom;this.addClass("x-repaint");setTimeout(function(){Ext.get(dom).removeClass("x-repaint")},1);return this},getMargins:function(side){if(!side){return{top:parseInt(this.getStyle("margin-top"),10)||0,left:parseInt(this.getStyle("margin-left"),10)||0,bottom:parseInt(this.getStyle("margin-bottom"),10)||0,right:parseInt(this.getStyle("margin-right"),10)||0}}else{return this.addStyles(side,El.margins)}},addStyles:function(sides,styles){var val=0,v,w;for(var i=0,len=sides.length;idom.clientHeight||dom.scrollWidth>dom.clientWidth},scrollTo:function(side,value,animate){var prop=side.toLowerCase()=="left"?"scrollLeft":"scrollTop";if(!animate||!A){this.dom[prop]=value}else{var to=prop=="scrollLeft"?[value,this.dom.scrollTop]:[this.dom.scrollLeft,value];this.anim({scroll:{"to":to}},this.preanim(arguments,2),"scroll")}return this},scroll:function(direction,distance,animate){if(!this.isScrollable()){return }var el=this.dom;var l=el.scrollLeft,t=el.scrollTop;var w=el.scrollWidth,h=el.scrollHeight;var cw=el.clientWidth,ch=el.clientHeight;direction=direction.toLowerCase();var scrolled=false;var a=this.preanim(arguments,2);switch(direction){case"l":case"left":if(w-l>cw){var v=Math.min(l+distance,w-cw);this.scrollTo("left",v,a);scrolled=true}break;case"r":case"right":if(l>0){var v=Math.max(l-distance,0);this.scrollTo("left",v,a);scrolled=true}break;case"t":case"top":case"up":if(t>0){var v=Math.max(t-distance,0);this.scrollTo("top",v,a);scrolled=true}break;case"b":case"bottom":case"down":if(h-t>ch){var v=Math.min(t+distance,h-ch);this.scrollTo("top",v,a);scrolled=true}break}return scrolled},translatePoints:function(x,y){if(typeof x=="object"||x instanceof Array){y=x[1];x=x[0]}var p=this.getStyle("position");var o=this.getXY();var l=parseInt(this.getStyle("left"),10);var t=parseInt(this.getStyle("top"),10);if(isNaN(l)){l=(p=="relative")?0:this.dom.offsetLeft}if(isNaN(t)){t=(p=="relative")?0:this.dom.offsetTop}return{left:(x-o[0]+l),top:(y-o[1]+t)}},getScroll:function(){var d=this.dom,doc=document;if(d==doc||d==doc.body){var l=window.pageXOffset||doc.documentElement.scrollLeft||doc.body.scrollLeft||0;var t=window.pageYOffset||doc.documentElement.scrollTop||doc.body.scrollTop||0;return{left:l,top:t}}else{return{left:d.scrollLeft,top:d.scrollTop}}},getColor:function(attr,defaultValue,prefix){var v=this.getStyle(attr);if(!v||v=="transparent"||v=="inherit"){return defaultValue}var color=typeof prefix=="undefined"?"#":prefix;if(v.substr(0,4)=="rgb("){var rvs=v.slice(4,v.length-1).split(",");for(var i=0;i<3;i++){var h=parseInt(rvs[i]).toString(16);if(h<16){h="0"+h}color+=h}}else{if(v.substr(0,1)=="#"){if(v.length==4){for(var i=1;i<4;i++){var c=v.charAt(i);color+=c+c}}else{if(v.length==7){color+=v.substr(1)}}}}return(color.length>5?color.toLowerCase():defaultValue)},boxWrap:function(cls){cls=cls||"x-box";var el=Ext.get(this.insertHtml("beforeBegin",String.format("
    "+El.boxMarkup+"
    ",cls)));el.child("."+cls+"-mc").dom.appendChild(this.dom);return el},getAttributeNS:Ext.isIE?function(ns,name){var d=this.dom;var type=typeof d[ns+":"+name];if(type!="undefined"&&type!="unknown"){return d[ns+":"+name]}return d[name]}:function(ns,name){var d=this.dom;return d.getAttributeNS(ns,name)||d.getAttribute(ns+":"+name)||d.getAttribute(name)||d[name]}};var ep=El.prototype;ep.on=ep.addListener;ep.mon=ep.addListener;ep.un=ep.removeListener;ep.autoBoxAdjust=true;El.unitPattern=/\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i;El.addUnits=function(v,defaultUnit){if(v===""||v=="auto"){return v}if(v===undefined){return""}if(typeof v=="number"||!El.unitPattern.test(v)){return v+(defaultUnit||"px")}return v};El.boxMarkup="
    ";El.VISIBILITY=1;El.DISPLAY=2;El.borders={l:"border-left-width",r:"border-right-width",t:"border-top-width",b:"border-bottom-width"};El.paddings={l:"padding-left",r:"padding-right",t:"padding-top",b:"padding-bottom"};El.margins={l:"margin-left",r:"margin-right",t:"margin-top",b:"margin-bottom"};El.cache={};var docEl;El.get=function(el){var ex,elm,id;if(!el){return null}if(typeof el=="string"){if(!(elm=document.getElementById(el))){return null}if(ex=El.cache[el]){ex.dom=elm}else{ex=El.cache[el]=new El(elm)}return ex}else{if(el.tagName){if(!(id=el.id)){id=Ext.id(el)}if(ex=El.cache[id]){ex.dom=el}else{ex=El.cache[id]=new El(el)}return ex}else{if(el instanceof El){if(el!=docEl){el.dom=document.getElementById(el.id)||el.dom;El.cache[el.id]=el}return el}else{if(el.isComposite){return el}else{if(el instanceof Array){return El.select(el)}else{if(el==document){if(!docEl){var f=function(){};f.prototype=El.prototype;docEl=new f();docEl.dom=document}return docEl}}}}}}return null};El.uncache=function(el){for(var i=0,a=arguments,len=a.length;i0){F.defer((duration/2)*1000,this)}else{B.afterFx(D)}};F.call(this)});return this},pause:function(C){var A=this.getFxEl();var B={};A.queueFx(B,function(){setTimeout(function(){A.afterFx(B)},C*1000)});return this},fadeIn:function(B){var A=this.getFxEl();B=B||{};A.queueFx(B,function(){this.setOpacity(0);this.fixDisplay();this.dom.style.visibility="visible";var C=B.endOpacity||1;arguments.callee.anim=this.fxanim({opacity:{to:C}},B,null,0.5,"easeOut",function(){if(C==1){this.clearOpacity()}A.afterFx(B)})});return this},fadeOut:function(B){var A=this.getFxEl();B=B||{};A.queueFx(B,function(){arguments.callee.anim=this.fxanim({opacity:{to:B.endOpacity||0}},B,null,0.5,"easeOut",function(){if(this.visibilityMode==Ext.Element.DISPLAY||B.useDisplay){this.dom.style.display="none"}else{this.dom.style.visibility="hidden"}this.clearOpacity();A.afterFx(B)})});return this},scale:function(A,B,C){this.shift(Ext.apply({},C,{width:A,height:B}));return this},shift:function(B){var A=this.getFxEl();B=B||{};A.queueFx(B,function(){var E={},D=B.width,F=B.height,C=B.x,H=B.y,G=B.opacity;if(D!==undefined){E.width={to:this.adjustWidth(D)}}if(F!==undefined){E.height={to:this.adjustHeight(F)}}if(C!==undefined||H!==undefined){E.points={to:[C!==undefined?C:this.getX(),H!==undefined?H:this.getY()]}}if(G!==undefined){E.opacity={to:G}}if(B.xy!==undefined){E.points={to:B.xy}}arguments.callee.anim=this.fxanim(E,B,"motion",0.35,"easeOut",function(){A.afterFx(B)})});return this},ghost:function(A,C){var B=this.getFxEl();C=C||{};B.queueFx(C,function(){A=A||"b";var H=this.getFxRestore();var E=this.getWidth(),G=this.getHeight();var F=this.dom.style;var J=function(){if(C.useDisplay){B.setDisplayed(false)}else{B.hide()}B.clearOpacity();B.setPositioning(H.pos);F.width=H.width;F.height=H.height;B.afterFx(C)};var D={opacity:{to:0},points:{}},I=D.points;switch(A.toLowerCase()){case"t":I.by=[0,-G];break;case"l":I.by=[-E,0];break;case"r":I.by=[E,0];break;case"b":I.by=[0,G];break;case"tl":I.by=[-E,-G];break;case"bl":I.by=[-E,G];break;case"br":I.by=[E,G];break;case"tr":I.by=[E,-G];break}arguments.callee.anim=this.fxanim(D,C,"motion",0.5,"easeOut",J)});return this},syncFx:function(){this.fxDefaults=Ext.apply(this.fxDefaults||{},{block:false,concurrent:true,stopFx:false});return this},sequenceFx:function(){this.fxDefaults=Ext.apply(this.fxDefaults||{},{block:false,concurrent:false,stopFx:false});return this},nextFx:function(){var A=this.fxQueue[0];if(A){A.call(this)}},hasActiveFx:function(){return this.fxQueue&&this.fxQueue[0]},stopFx:function(){if(this.hasActiveFx()){var A=this.fxQueue[0];if(A&&A.anim&&A.anim.isAnimated()){this.fxQueue=[A];A.anim.stop(true)}}return this},beforeFx:function(A){if(this.hasActiveFx()&&!A.concurrent){if(A.stopFx){this.stopFx();return true}return false}return true},hasFxBlock:function(){var A=this.fxQueue;return A&&A[0]&&A[0].block},queueFx:function(C,A){if(!this.fxQueue){this.fxQueue=[]}if(!this.hasFxBlock()){Ext.applyIf(C,this.fxDefaults);if(!C.concurrent){var B=this.beforeFx(C);A.block=C.block;this.fxQueue.push(A);if(B){this.nextFx()}}else{A.call(this)}}return this},fxWrap:function(F,D,C){var B;if(!D.wrap||!(B=Ext.get(D.wrap))){var A;if(D.fixPosition){A=this.getXY()}var E=document.createElement("div");E.style.visibility=C;B=Ext.get(this.dom.parentNode.insertBefore(E,this.dom));B.setPositioning(F);if(B.getStyle("position")=="static"){B.position("relative")}this.clearPositioning("auto");B.clip();B.dom.appendChild(this.dom);if(A){B.setXY(A)}}return B},fxUnwrap:function(A,C,B){this.clearPositioning();this.setPositioning(C);if(!B.wrap){A.dom.parentNode.insertBefore(this.dom,A.dom);A.remove()}},getFxRestore:function(){var A=this.dom.style;return{pos:this.getPositioning(),width:A.width,height:A.height}},afterFx:function(A){if(A.afterStyle){this.applyStyles(A.afterStyle)}if(A.afterCls){this.addClass(A.afterCls)}if(A.remove===true){this.remove()}Ext.callback(A.callback,A.scope,[this]);if(!A.concurrent){this.fxQueue.shift();this.nextFx()}},getFxEl:function(){return Ext.get(this.dom)},fxanim:function(D,E,B,F,C,A){B=B||"run";E=E||{};var G=Ext.lib.Anim[B](this.dom,D,(E.duration||F)||0.35,(E.easing||C)||"easeOut",function(){Ext.callback(A,this)},this);E.anim=G;return G}};Ext.Fx.resize=Ext.Fx.scale;Ext.apply(Ext.Element.prototype,Ext.Fx); - - - -Ext.UpdateManager=function(B,A){B=Ext.get(B);if(!A&&B.updateManager){return B.updateManager}this.el=B;this.defaultUrl=null;this.addEvents({"beforeupdate":true,"update":true,"failure":true});var C=Ext.UpdateManager.defaults;this.sslBlankUrl=C.sslBlankUrl;this.disableCaching=C.disableCaching;this.indicatorText=C.indicatorText;this.showLoadIndicator=C.showLoadIndicator;this.timeout=C.timeout;this.loadScripts=C.loadScripts;this.transaction=null;this.autoRefreshProcId=null;this.refreshDelegate=this.refresh.createDelegate(this);this.updateDelegate=this.update.createDelegate(this);this.formUpdateDelegate=this.formUpdate.createDelegate(this);this.successDelegate=this.processSuccess.createDelegate(this);this.failureDelegate=this.processFailure.createDelegate(this);if(!this.renderer){this.renderer=new Ext.UpdateManager.BasicRenderer()}Ext.UpdateManager.superclass.constructor.call(this)};Ext.extend(Ext.UpdateManager,Ext.util.Observable,{getEl:function(){return this.el},update:function(B,E,G,C){if(this.fireEvent("beforeupdate",this.el,B,E)!==false){var F=this.method,A;if(typeof B=="object"){A=B;B=A.url;E=E||A.params;G=G||A.callback;C=C||A.discardUrl;if(G&&A.scope){G=G.createDelegate(A.scope)}if(typeof A.method!="undefined"){F=A.method}if(typeof A.nocache!="undefined"){this.disableCaching=A.nocache}if(typeof A.text!="undefined"){this.indicatorText="
    "+A.text+"
    "}if(typeof A.scripts!="undefined"){this.loadScripts=A.scripts}if(typeof A.timeout!="undefined"){this.timeout=A.timeout}}this.showLoading();if(!C){this.defaultUrl=B}if(typeof B=="function"){B=B.call(this)}F=F||(E?"POST":"GET");if(F=="GET"){B=this.prepareUrl(B)}var D=Ext.apply(A||{},{url:B,params:E,success:this.successDelegate,failure:this.failureDelegate,callback:undefined,timeout:(this.timeout*1000),argument:{"url":B,"form":null,"callback":G,"params":E}});this.transaction=Ext.Ajax.request(D)}},formUpdate:function(C,A,B,D){if(this.fireEvent("beforeupdate",this.el,C,A)!==false){if(typeof A=="function"){A=A.call(this)}C=Ext.getDom(C);this.transaction=Ext.Ajax.request({form:C,url:A,success:this.successDelegate,failure:this.failureDelegate,timeout:(this.timeout*1000),argument:{"url":A,"form":C,"callback":D,"reset":B}});this.showLoading.defer(1,this)}},refresh:function(A){if(this.defaultUrl==null){return }this.update(this.defaultUrl,null,A,true)},startAutoRefresh:function(B,C,D,E,A){if(A){this.update(C||this.defaultUrl,D,E,true)}if(this.autoRefreshProcId){clearInterval(this.autoRefreshProcId)}this.autoRefreshProcId=setInterval(this.update.createDelegate(this,[C||this.defaultUrl,D,E,true]),B*1000)},stopAutoRefresh:function(){if(this.autoRefreshProcId){clearInterval(this.autoRefreshProcId);delete this.autoRefreshProcId}},isAutoRefreshing:function(){return this.autoRefreshProcId?true:false},showLoading:function(){if(this.showLoadIndicator){this.el.update(this.indicatorText)}},prepareUrl:function(B){if(this.disableCaching){var A="_dc="+(new Date().getTime());if(B.indexOf("?")!==-1){B+="&"+A}else{B+="?"+A}}return B},processSuccess:function(A){this.transaction=null;if(A.argument.form&&A.argument.reset){try{A.argument.form.reset()}catch(B){}}if(this.loadScripts){this.renderer.render(this.el,A,this,this.updateComplete.createDelegate(this,[A]))}else{this.renderer.render(this.el,A,this);this.updateComplete(A)}},updateComplete:function(A){this.fireEvent("update",this.el,A);if(typeof A.argument.callback=="function"){A.argument.callback(this.el,true,A)}},processFailure:function(A){this.transaction=null;this.fireEvent("failure",this.el,A);if(typeof A.argument.callback=="function"){A.argument.callback(this.el,false,A)}},setRenderer:function(A){this.renderer=A},getRenderer:function(){return this.renderer},setDefaultUrl:function(A){this.defaultUrl=A},abort:function(){if(this.transaction){Ext.Ajax.abort(this.transaction)}},isUpdating:function(){if(this.transaction){return Ext.Ajax.isLoading(this.transaction)}return false}});Ext.UpdateManager.defaults={timeout:30,loadScripts:false,sslBlankUrl:(Ext.SSL_SECURE_URL||"javascript:false"),disableCaching:false,showLoadIndicator:true,indicatorText:"
    Loading...
    "};Ext.UpdateManager.updateElement=function(D,C,E,B){var A=Ext.get(D,true).getUpdateManager();Ext.apply(A,B);A.update(C,E,B?B.callback:null)};Ext.UpdateManager.update=Ext.UpdateManager.updateElement;Ext.UpdateManager.BasicRenderer=function(){};Ext.UpdateManager.BasicRenderer.prototype={render:function(C,A,B,D){C.update(A.responseText,B.loadScripts,D)}}; - - - -Ext.util.MixedCollection=function(B,A){this.items=[];this.map={};this.keys=[];this.length=0;this.addEvents({"clear":true,"add":true,"replace":true,"remove":true,"sort":true});this.allowFunctions=B===true;if(A){this.getKey=A}Ext.util.MixedCollection.superclass.constructor.call(this)};Ext.extend(Ext.util.MixedCollection,Ext.util.Observable,{allowFunctions:false,add:function(B,C){if(arguments.length==1){C=arguments[0];B=this.getKey(C)}if(typeof B=="undefined"||B===null){this.length++;this.items.push(C);this.keys.push(null)}else{var A=this.map[B];if(A){return this.replace(B,C)}this.length++;this.items.push(C);this.map[B]=C;this.keys.push(B)}this.fireEvent("add",this.length-1,C,B);return C},getKey:function(A){return A.id},replace:function(C,D){if(arguments.length==1){D=arguments[0];C=this.getKey(D)}var A=this.item(C);if(typeof C=="undefined"||C===null||typeof A=="undefined"){return this.add(C,D)}var B=this.indexOfKey(C);this.items[B]=D;this.map[C]=D;this.fireEvent("replace",C,A,D);return D},addAll:function(E){if(arguments.length>1||E instanceof Array){var B=arguments.length>1?arguments:E;for(var D=0,A=B.length;D=this.length){return this.add(B,C)}this.length++;this.items.splice(A,0,C);if(typeof B!="undefined"&&B!=null){this.map[B]=C}this.keys.splice(A,0,B);this.fireEvent("add",A,C,B);return C},remove:function(A){return this.removeAt(this.indexOf(A))},removeAt:function(A){if(A=0){this.length--;var C=this.items[A];this.items.splice(A,1);var B=this.keys[A];if(typeof B!="undefined"){delete this.map[B]}this.keys.splice(A,1);this.fireEvent("remove",C,B)}},removeKey:function(A){return this.removeAt(this.indexOfKey(A))},getCount:function(){return this.length},indexOf:function(C){if(!this.items.indexOf){for(var B=0,A=this.items.length;B=A;C--){D[D.length]=B[C]}}return D},filter:function(B,A){if(!A.exec){A=String(A);if(A.length==0){return this.clone()}A=new RegExp("^"+Ext.escapeRe(A),"i")}return this.filterBy(function(C){return C&&A.test(C[B])})},filterBy:function(F,E){var G=new Ext.util.MixedCollection();G.getKey=this.getKey;var B=this.keys,D=this.items;for(var C=0,A=D.length;C=this.minX;D=D-C){if(!E[D]){this.xTicks[this.xTicks.length]=D;E[D]=true}}for(D=this.initPageX;D<=this.maxX;D=D+C){if(!E[D]){this.xTicks[this.xTicks.length]=D;E[D]=true}}this.xTicks.sort(this.DDM.numericSort)},setYTicks:function(F,C){this.yTicks=[];this.yTickSize=C;var E={};for(var D=this.initPageY;D>=this.minY;D=D-C){if(!E[D]){this.yTicks[this.yTicks.length]=D;E[D]=true}}for(D=this.initPageY;D<=this.maxY;D=D+C){if(!E[D]){this.yTicks[this.yTicks.length]=D;E[D]=true}}this.yTicks.sort(this.DDM.numericSort)},setXConstraint:function(E,D,C){this.leftConstraint=E;this.rightConstraint=D;this.minX=this.initPageX-E;this.maxX=this.initPageX+D;if(C){this.setXTicks(this.initPageX,C)}this.constrainX=true},clearConstraints:function(){this.constrainX=false;this.constrainY=false;this.clearTicks()},clearTicks:function(){this.xTicks=null;this.yTicks=null;this.xTickSize=0;this.yTickSize=0},setYConstraint:function(C,E,D){this.topConstraint=C;this.bottomConstraint=E;this.minY=this.initPageY-C;this.maxY=this.initPageY+E;if(D){this.setYTicks(this.initPageY,D)}this.constrainY=true},resetConstraints:function(){if(this.initPageX||this.initPageX===0){var D=(this.maintainOffset)?this.lastPageX-this.initPageX:0;var C=(this.maintainOffset)?this.lastPageY-this.initPageY:0;this.setInitPosition(D,C)}else{this.setInitPosition()}if(this.constrainX){this.setXConstraint(this.leftConstraint,this.rightConstraint,this.xTickSize)}if(this.constrainY){this.setYConstraint(this.topConstraint,this.bottomConstraint,this.yTickSize)}},getTick:function(I,F){if(!F){return I}else{if(F[0]>=I){return F[0]}else{for(var D=0,C=F.length;D=I){var H=I-F[D];var G=F[E]-I;return(G>H)?F[D]:F[E]}}return F[F.length-1]}}},toString:function(){return("DragDrop "+this.id)}}})();if(!Ext.dd.DragDropMgr){Ext.dd.DragDropMgr=function(){var A=Ext.EventManager;return{ids:{},handleIds:{},dragCurrent:null,dragOvers:{},deltaX:0,deltaY:0,preventDefault:true,stopPropagation:true,initalized:false,locked:false,init:function(){this.initialized=true},POINT:0,INTERSECT:1,mode:0,_execOnAll:function(D,C){for(var E in this.ids){for(var B in this.ids[E]){var F=this.ids[E][B];if(!this.isTypeOfDD(F)){continue}F[D].apply(F,C)}}},_onLoad:function(){this.init();A.on(document,"mouseup",this.handleMouseUp,this,true);A.on(document,"mousemove",this.handleMouseMove,this,true);A.on(window,"beforeunload",this._onUnload,this,true);A.on(window,"resize",this._onResize,this,true)},_onResize:function(B){this._execOnAll("resetConstraints",[])},lock:function(){this.locked=true},unlock:function(){this.locked=false},isLocked:function(){return this.locked},locationCache:{},useCache:true,clickPixelThresh:3,clickTimeThresh:350,dragThreshMet:false,clickTimeout:null,startX:0,startY:0,regDragDrop:function(C,B){if(!this.initialized){this.init()}if(!this.ids[B]){this.ids[B]={}}this.ids[B][C.id]=C},removeDDFromGroup:function(D,B){if(!this.ids[B]){this.ids[B]={}}var C=this.ids[B];if(C&&C[D.id]){delete C[D.id]}},_remove:function(C){for(var B in C.groups){if(B&&this.ids[B][C.id]){delete this.ids[B][C.id]}}delete this.handleIds[C.id]},regHandle:function(C,B){if(!this.handleIds[C]){this.handleIds[C]={}}this.handleIds[C][B]=B},isDragDrop:function(B){return(this.getDDById(B))?true:false},getRelated:function(F,C){var E=[];for(var D in F.groups){for(j in this.ids[D]){var B=this.ids[D][j];if(!this.isTypeOfDD(B)){continue}if(!C||B.isTarget){E[E.length]=B}}}return E},isLegalTarget:function(F,E){var C=this.getRelated(F,true);for(var D=0,B=C.length;Dthis.clickPixelThresh||B>this.clickPixelThresh){this.startDrag(this.startX,this.startY)}}if(this.dragThreshMet){this.dragCurrent.b4Drag(D);this.dragCurrent.onDrag(D);if(!this.dragCurrent.moveOnly){this.fireEvents(D,false)}}this.stopEvent(D);return true},fireEvents:function(K,L){var N=this.dragCurrent;if(!N||N.isLocked()){return }var O=K.getPoint();var B=[];var E=[];var I=[];var G=[];var D=[];for(var F in this.dragOvers){var C=this.dragOvers[F];if(!this.isTypeOfDD(C)){continue}if(!this.isOverTarget(O,C,this.mode)){E.push(C)}B[F]=true;delete this.dragOvers[F]}for(var M in N.groups){if("string"!=typeof M){continue}for(F in this.ids[M]){var H=this.ids[M][F];if(!this.isTypeOfDD(H)){continue}if(H.isTarget&&!H.isLocked()&&H!=N){if(this.isOverTarget(O,H,this.mode)){if(L){G.push(H)}else{if(!B[H.id]){D.push(H)}else{I.push(H)}this.dragOvers[H.id]=H}}}}}if(this.mode){if(E.length){N.b4DragOut(K,E);N.onDragOut(K,E)}if(D.length){N.onDragEnter(K,D)}if(I.length){N.b4DragOver(K,I);N.onDragOver(K,I)}if(G.length){N.b4DragDrop(K,G);N.onDragDrop(K,G)}}else{var J=0;for(F=0,J=E.length;F2000){}else{setTimeout(B._addListeners,10);if(document&&document.body){B._timeoutCount+=1}}}},handleWasClicked:function(B,D){if(this.isHandle(D,B.id)){return true}else{var C=B.parentNode;while(C){if(this.isHandle(D,C.id)){return true}else{C=C.parentNode}}}return false}}}();Ext.dd.DDM=Ext.dd.DragDropMgr;Ext.dd.DDM._addListeners()}Ext.dd.DD=function(C,A,B){if(C){this.init(C,A,B)}};Ext.extend(Ext.dd.DD,Ext.dd.DragDrop,{scroll:true,autoOffset:function(C,B){var A=C-this.startPageX;var D=B-this.startPageY;this.setDelta(A,D)},setDelta:function(B,A){this.deltaX=B;this.deltaY=A},setDragElPos:function(C,B){var A=this.getDragEl();this.alignElWithMouse(A,C,B)},alignElWithMouse:function(C,G,F){var E=this.getTargetCoord(G,F);var B=C.dom?C:Ext.fly(C);if(!this.deltaSetXY){var H=[E.x,E.y];B.setXY(H);var D=B.getLeft(true);var A=B.getTop(true);this.deltaSetXY=[D-E.x,A-E.y]}else{B.setLeftTop(E.x+this.deltaSetXY[0],E.y+this.deltaSetXY[1])}this.cachePosition(E.x,E.y);this.autoScroll(E.x,E.y,C.offsetHeight,C.offsetWidth);return E},cachePosition:function(B,A){if(B){this.lastPageX=B;this.lastPageY=A}else{var C=Ext.lib.Dom.getXY(this.getEl());this.lastPageX=C[0];this.lastPageY=C[1]}},autoScroll:function(J,I,E,K){if(this.scroll){var L=Ext.lib.Dom.getViewWidth();var B=Ext.lib.Dom.getViewHeight();var N=this.DDM.getScrollTop();var D=this.DDM.getScrollLeft();var H=E+I;var M=K+J;var G=(L+N-I-this.deltaY);var F=(B+D-J-this.deltaX);var C=40;var A=(document.all)?80:30;if(H>L&&G0&&I-NB&&F0&&J-Dthis.maxX){A=this.maxX}}if(this.constrainY){if(Dthis.maxY){D=this.maxY}}A=this.getTick(A,this.xTicks);D=this.getTick(D,this.yTicks);return{x:A,y:D}},applyConfig:function(){Ext.dd.DD.superclass.applyConfig.call(this);this.scroll=(this.config.scroll!==false)},b4MouseDown:function(A){this.autoOffset(A.getPageX(),A.getPageY())},b4Drag:function(A){this.setDragElPos(A.getPageX(),A.getPageY())},toString:function(){return("DD "+this.id)}});Ext.dd.DDProxy=function(C,A,B){if(C){this.init(C,A,B);this.initFrame()}};Ext.dd.DDProxy.dragElId="ygddfdiv";Ext.extend(Ext.dd.DDProxy,Ext.dd.DD,{resizeFrame:true,centerFrame:false,createFrame:function(){var B=this;var A=document.body;if(!A||!A.firstChild){setTimeout(function(){B.createFrame()},50);return }var D=this.getDragEl();if(!D){D=document.createElement("div");D.id=this.dragElId;var C=D.style;C.position="absolute";C.visibility="hidden";C.cursor="move";C.border="2px solid #aaa";C.zIndex=999;A.insertBefore(D,A.firstChild)}},initFrame:function(){this.createFrame()},applyConfig:function(){Ext.dd.DDProxy.superclass.applyConfig.call(this);this.resizeFrame=(this.config.resizeFrame!==false);this.centerFrame=(this.config.centerFrame);this.setDragElId(this.config.dragElId||Ext.dd.DDProxy.dragElId)},showFrame:function(E,D){var C=this.getEl();var A=this.getDragEl();var B=A.style;this._resizeProxy();if(this.centerFrame){this.setDelta(Math.round(parseInt(B.width,10)/2),Math.round(parseInt(B.height,10)/2))}this.setDragElPos(E,D);Ext.fly(A).show()},_resizeProxy:function(){if(this.resizeFrame){var A=this.getEl();Ext.fly(this.getDragEl()).setSize(A.offsetWidth,A.offsetHeight)}},b4MouseDown:function(B){var A=B.getPageX();var C=B.getPageY();this.autoOffset(A,C);this.setDragElPos(A,C)},b4StartDrag:function(A,B){this.showFrame(A,B)},b4EndDrag:function(A){Ext.fly(this.getDragEl()).hide()},endDrag:function(C){var B=this.getEl();var A=this.getDragEl();A.style.visibility="";this.beforeMove();B.style.visibility="hidden";Ext.dd.DDM.moveToEl(B,A);A.style.visibility="hidden";B.style.visibility="";this.afterDrag()},beforeMove:function(){},afterDrag:function(){},toString:function(){return("DDProxy "+this.id)}});Ext.dd.DDTarget=function(C,A,B){if(C){this.initTarget(C,A,B)}};Ext.extend(Ext.dd.DDTarget,Ext.dd.DragDrop,{toString:function(){return("DDTarget "+this.id)}}); - - - -Ext.dd.ScrollManager=function(){var C=Ext.dd.DragDropMgr;var E={};var B=null;var H={};var G=function(K){B=null;A()};var I=function(){if(C.dragCurrent){C.refreshCache(C.dragCurrent.groups)}};var D=function(){if(C.dragCurrent){var K=Ext.dd.ScrollManager;if(!K.animate){if(H.el.scroll(H.dir,K.increment)){I()}}else{H.el.scroll(H.dir,K.increment,true,K.animDuration,I)}}};var A=function(){if(H.id){clearInterval(H.id)}H.id=0;H.el=null;H.dir=""};var F=function(L,K){A();H.el=L;H.dir=K;H.id=setInterval(D,Ext.dd.ScrollManager.frequency)};var J=function(Q,L){if(L||!C.dragCurrent){return }var K=Ext.dd.ScrollManager;if(!B||B!=C.dragCurrent){B=C.dragCurrent;K.refreshCache()}var P=Ext.lib.Event.getXY(Q);var O=new Ext.lib.Point(P[0],P[1]);for(var R in E){var M=E[R],N=M._region;if(N&&N.contains(O)&&M.isScrollable()){if(N.bottom-O.y<=K.thresh){if(H.el!=M){F(M,"down")}return }else{if(N.right-O.x<=K.thresh){if(H.el!=M){F(M,"left")}return }else{if(O.y-N.top<=K.thresh){if(H.el!=M){F(M,"up")}return }else{if(O.x-N.left<=K.thresh){if(H.el!=M){F(M,"right")}return }}}}}}A()};C.fireEvents=C.fireEvents.createSequence(J,C);C.stopDrag=C.stopDrag.createSequence(G,C);return{register:function(M){if(M instanceof Array){for(var L=0,K=M.length;L0},appendChild:function(E){var F=false;if(E instanceof Array){F=E}else{if(arguments.length>1){F=arguments}}if(F){for(var D=0,A=F.length;D0){var F=D?function(){E.apply(D,arguments)}:E;C.sort(F);for(var B=0;BG+L.left){H=G-I-this.shadowOffset;E=true}if((F+D)>C+L.top){F=C-D-this.shadowOffset;E=true}if(H=J){F=J-D-5}}K=[H,F];this.storeXY(K);A.setXY.call(this,K);this.sync()}}},isVisible:function(){return this.visible},showAction:function(){this.visible=true;if(this.useDisplay===true){this.setDisplayed("")}else{if(this.lastXY){A.setXY.call(this,this.lastXY)}else{if(this.lastLT){A.setLeftTop.call(this,this.lastLT[0],this.lastLT[1])}}}},hideAction:function(){this.visible=false;if(this.useDisplay===true){this.setDisplayed(false)}else{this.setLeftTop(-10000,-10000)}},setVisible:function(E,D,G,H,F){if(E){this.showAction()}if(D&&E){var C=function(){this.sync(true);if(H){H()}}.createDelegate(this);A.setVisible.call(this,true,true,G,C,F)}else{if(!E){this.hideUnders(true)}var C=H;if(D){C=function(){this.hideAction();if(H){H()}}.createDelegate(this)}A.setVisible.call(this,E,D,G,C,F);if(E){this.sync(true)}else{if(!D){this.hideAction()}}}},storeXY:function(C){delete this.lastLT;this.lastXY=C},storeLeftTop:function(D,C){delete this.lastXY;this.lastLT=[D,C]},beforeFx:function(){this.beforeAction();return Ext.Layer.superclass.beforeFx.apply(this,arguments)},afterFx:function(){Ext.Layer.superclass.afterFx.apply(this,arguments);this.sync(this.isVisible())},beforeAction:function(){if(!this.updating&&this.shadow){this.shadow.hide()}},setLeft:function(C){this.storeLeftTop(C,this.getTop(true));A.setLeft.apply(this,arguments);this.sync()},setTop:function(C){this.storeLeftTop(this.getLeft(true),C);A.setTop.apply(this,arguments);this.sync()},setLeftTop:function(D,C){this.storeLeftTop(D,C);A.setLeftTop.apply(this,arguments);this.sync()},setXY:function(F,D,G,H,E){this.fixDisplay();this.beforeAction();this.storeXY(F);var C=this.createCB(H);A.setXY.call(this,F,D,G,C,E);if(!D){C()}},createCB:function(D){var C=this;return function(){C.constrainXY();C.sync(true);if(D){D()}}},setX:function(C,D,F,G,E){this.setXY([C,this.getY()],D,F,G,E)},setY:function(G,C,E,F,D){this.setXY([this.getX(),G],C,E,F,D)},setSize:function(E,F,D,H,I,G){this.beforeAction();var C=this.createCB(I);A.setSize.call(this,E,F,D,H,C,G);if(!D){C()}},setWidth:function(E,D,G,H,F){this.beforeAction();var C=this.createCB(H);A.setWidth.call(this,E,D,G,C,F);if(!D){C()}},setHeight:function(E,D,G,H,F){this.beforeAction();var C=this.createCB(H);A.setHeight.call(this,E,D,G,C,F);if(!D){C()}},setBounds:function(J,H,K,D,I,F,G,E){this.beforeAction();var C=this.createCB(G);if(!I){this.storeXY([J,H]);A.setXY.call(this,[J,H]);A.setSize.call(this,K,D,I,F,C,E);C()}else{A.setBounds.call(this,J,H,K,D,I,F,C,E)}return this},setZIndex:function(C){this.zindex=C;this.setStyle("z-index",C+2);if(this.shadow){this.shadow.setZIndex(C+1)}if(this.shim){this.shim.setStyle("z-index",C)}}})})(); - - - -Ext.Shadow=function(C){Ext.apply(this,C);if(typeof this.mode!="string"){this.mode=this.defaultMode}var D=this.offset,B={h:0};var A=Math.floor(this.offset/2);switch(this.mode.toLowerCase()){case"drop":B.w=0;B.l=B.t=D;B.t-=1;if(Ext.isIE){B.l-=this.offset+A;B.t-=this.offset+A;B.w-=A;B.h-=A;B.t+=1}break;case"sides":B.w=(D*2);B.l=-D;B.t=D-1;if(Ext.isIE){B.l-=(this.offset-A);B.t-=this.offset+A;B.l+=1;B.w-=(this.offset-A)*2;B.w-=A+1;B.h-=1}break;case"frame":B.w=B.h=(D*2);B.l=B.t=-D;B.t+=1;B.h-=2;if(Ext.isIE){B.l-=(this.offset-A);B.t-=(this.offset-A);B.l+=1;B.w-=(this.offset+A+1);B.h-=(this.offset+A);B.h+=1}break}this.adjusts=B};Ext.Shadow.prototype={offset:4,defaultMode:"drop",show:function(A){A=Ext.get(A);if(!this.el){this.el=Ext.Shadow.Pool.pull();if(this.el.dom.nextSibling!=A.dom){this.el.insertBefore(A)}}this.el.setStyle("z-index",this.zIndex||parseInt(A.getStyle("z-index"),10)-1);if(Ext.isIE){this.el.dom.style.filter="progid:DXImageTransform.Microsoft.alpha(opacity=50) progid:DXImageTransform.Microsoft.Blur(pixelradius="+(this.offset)+")"}this.realign(A.getLeft(true),A.getTop(true),A.getWidth(),A.getHeight());this.el.dom.style.display="block"},isVisible:function(){return this.el?true:false},realign:function(A,M,L,D){if(!this.el){return }var I=this.adjusts,G=this.el.dom,N=G.style;var E=0;N.left=(A+I.l)+"px";N.top=(M+I.t)+"px";var K=(L+I.w),C=(D+I.h),F=K+"px",J=C+"px";if(N.width!=F||N.height!=J){N.width=F;N.height=J;if(!Ext.isIE){var H=G.childNodes;var B=Math.max(0,(K-12))+"px";H[0].childNodes[1].style.width=B;H[1].childNodes[1].style.width=B;H[2].childNodes[1].style.width=B;H[1].style.height=Math.max(0,(C-12))+"px"}}},hide:function(){if(this.el){this.el.dom.style.display="none";Ext.Shadow.Pool.push(this.el);delete this.el}},setZIndex:function(A){this.zIndex=A;if(this.el){this.el.setStyle("z-index",A)}}};Ext.Shadow.Pool=function(){var B=[];var A=Ext.isIE?"
    ":"
    ";return{pull:function(){var C=B.shift();if(!C){C=Ext.get(Ext.DomHelper.insertHtml("beforeBegin",document.body.firstChild,A));C.autoBoxAdjust=false}return C},push:function(C){B.push(C)}}}(); - - - -Ext.Editor=function(B,A){Ext.Editor.superclass.constructor.call(this,A);this.field=B;this.addEvents({"beforestartedit":true,"startedit":true,"beforecomplete":true,"complete":true,"specialkey":true})};Ext.extend(Ext.Editor,Ext.Component,{value:"",alignment:"c-c?",shadow:"frame",constrain:false,completeOnEnter:false,cancelOnEsc:false,updateEl:false,onRender:function(B,A){this.el=new Ext.Layer({shadow:this.shadow,cls:"x-editor",parentEl:B,shim:this.shim,shadowOffset:4,id:this.id,constrain:this.constrain});this.el.setStyle("overflow",Ext.isGecko?"auto":"hidden");if(this.field.msgTarget!="title"){this.field.msgTarget="qtip"}this.field.render(this.el);if(Ext.isGecko){this.field.el.dom.setAttribute("autocomplete","off")}this.field.on("specialkey",this.onSpecialKey,this);if(this.swallowKeys){this.field.el.swallowEvent(["keydown","keypress"])}this.field.show();this.field.on("blur",this.onBlur,this);if(this.field.grow){this.field.on("autosize",this.el.sync,this.el,{delay:1})}},onSpecialKey:function(B,A){if(this.completeOnEnter&&A.getKey()==A.ENTER){A.stopEvent();this.completeEdit()}else{if(this.cancelOnEsc&&A.getKey()==A.ESC){this.cancelEdit()}else{this.fireEvent("specialkey",B,A)}}},startEdit:function(B,C){if(this.editing){this.completeEdit()}this.boundEl=Ext.get(B);var A=C!==undefined?C:this.boundEl.dom.innerHTML;if(!this.rendered){this.render(this.parentEl||document.body)}if(this.fireEvent("beforestartedit",this,this.boundEl,A)===false){return }this.startValue=A;this.field.setValue(A);if(this.autoSize){var D=this.boundEl.getSize();switch(this.autoSize){case"width":this.setSize(D.width,"");break;case"height":this.setSize("",D.height);break;default:this.setSize(D.width,D.height)}}this.el.alignTo(this.boundEl,this.alignment);this.editing=true;if(Ext.QuickTips){Ext.QuickTips.disable()}this.show()},setSize:function(A,B){this.field.setSize(A,B);if(this.el){this.el.sync()}},realign:function(){this.el.alignTo(this.boundEl,this.alignment)},completeEdit:function(A){if(!this.editing){return }var B=this.getValue();if(this.revertInvalid!==false&&!this.field.isValid()){B=this.startValue;this.cancelEdit(true)}if(String(B)===String(this.startValue)&&this.ignoreNoChange){this.editing=false;this.hide();return }if(this.fireEvent("beforecomplete",this,B,this.startValue)!==false){this.editing=false;if(this.updateEl&&this.boundEl){this.boundEl.update(B)}if(A!==true){this.hide()}this.fireEvent("complete",this,B,this.startValue)}},onShow:function(){this.el.show();if(this.hideEl!==false){this.boundEl.hide()}this.field.show();if(Ext.isIE&&!this.fixIEFocus){this.fixIEFocus=true;this.deferredFocus.defer(50,this)}else{this.field.focus()}this.fireEvent("startedit",this.boundEl,this.startValue)},deferredFocus:function(){if(this.editing){this.field.focus()}},cancelEdit:function(A){if(this.editing){this.setValue(this.startValue);if(A!==true){this.hide()}}},onBlur:function(){if(this.allowBlur!==true&&this.editing){this.completeEdit()}},onHide:function(){if(this.editing){this.completeEdit();return }this.field.blur();if(this.field.collapse){this.field.collapse()}this.el.hide();if(this.hideEl!==false){this.boundEl.show()}if(Ext.QuickTips){Ext.QuickTips.enable()}},setValue:function(A){this.field.setValue(A)},getValue:function(){return this.field.getValue()}}); - - - -Ext.tree.TreePanel=function(B,A){Ext.apply(this,A);Ext.tree.TreePanel.superclass.constructor.call(this);this.el=Ext.get(B);this.el.addClass("x-tree");this.id=this.el.id;this.addEvents({"beforeload":true,"load":true,"textchange":true,"beforeexpand":true,"beforecollapse":true,"expand":true,"disabledchange":true,"collapse":true,"beforeclick":true,"checkchange":true,"click":true,"dblclick":true,"contextmenu":true,"beforechildrenrendered":true,"startdrag":true,"enddrag":true,"dragdrop":true,"beforenodedrop":true,"nodedrop":true,"nodedragover":true});if(this.singleExpand){this.on("beforeexpand",this.restrictExpand,this)}};Ext.extend(Ext.tree.TreePanel,Ext.data.Tree,{rootVisible:true,animate:Ext.enableFx,lines:true,enableDD:false,hlDrop:Ext.enableFx,restrictExpand:function(A){var B=A.parentNode;if(B){if(B.expandedChild&&B.expandedChild.parentNode==B){B.expandedChild.collapse()}B.expandedChild=A}},setRootNode:function(A){Ext.tree.TreePanel.superclass.setRootNode.call(this,A);if(!this.rootVisible){A.ui=new Ext.tree.RootTreeNodeUI(A)}return A},getEl:function(){return this.el},getLoader:function(){return this.loader},expandAll:function(){this.root.expand(true)},collapseAll:function(){this.root.collapse(true)},getSelectionModel:function(){if(!this.selModel){this.selModel=new Ext.tree.DefaultSelectionModel()}return this.selModel},getChecked:function(A,B){B=B||this.root;var C=[];var D=function(){if(this.attributes.checked){C.push(!A?this:(A=="id"?this.id:this.attributes[A]))}};B.cascade(D);return C},expandPath:function(F,A,G){A=A||"id";var D=F.split(this.pathSeparator);var C=this.root;if(C.attributes[A]!=D[1]){if(G){G(false,null)}return }var B=1;var E=function(){if(++B==D.length){if(G){G(true,C)}return }var H=C.findChild(A,D[B]);if(!H){if(G){G(false,C)}return }C=H;H.expand(false,false,E)};C.expand(false,false,E)},selectPath:function(E,A,F){A=A||"id";var C=E.split(this.pathSeparator);var B=C.pop();if(C.length>0){var D=function(H,G){if(H&&G){var I=G.findChild(A,B);if(I){I.select();if(F){F(true,I)}}else{if(F){F(false,I)}}}else{if(F){F(false,I)}}};this.expandPath(C.join(this.pathSeparator),A,D)}else{this.root.select();if(F){F(true,this.root)}}},getTreeEl:function(){return this.el},render:function(){this.innerCt=this.el.createChild({tag:"ul",cls:"x-tree-root-ct "+(this.lines?"x-tree-lines":"x-tree-no-lines")});if(this.containerScroll){Ext.dd.ScrollManager.register(this.el)}if((this.enableDD||this.enableDrop)&&!this.dropZone){this.dropZone=new Ext.tree.TreeDropZone(this,this.dropConfig||{ddGroup:this.ddGroup||"TreeDD",appendOnly:this.ddAppendOnly===true})}if((this.enableDD||this.enableDrag)&&!this.dragZone){this.dragZone=new Ext.tree.TreeDragZone(this,this.dragConfig||{ddGroup:this.ddGroup||"TreeDD",scroll:this.ddScroll})}this.getSelectionModel().init(this);this.root.render();if(!this.rootVisible){this.root.renderChildren()}return this}}); - - - -Ext.tree.DefaultSelectionModel=function(){this.selNode=null;this.addEvents({"selectionchange":true,"beforeselect":true})};Ext.extend(Ext.tree.DefaultSelectionModel,Ext.util.Observable,{init:function(A){this.tree=A;A.getTreeEl().on("keydown",this.onKeyDown,this);A.on("click",this.onNodeClick,this)},onNodeClick:function(A,B){this.select(A)},select:function(B){var A=this.selNode;if(A!=B&&this.fireEvent("beforeselect",this,B,A)!==false){if(A){A.ui.onSelectedChange(false)}this.selNode=B;B.ui.onSelectedChange(true);this.fireEvent("selectionchange",this,B,A)}return B},unselect:function(A){if(this.selNode==A){this.clearSelections()}},clearSelections:function(){var A=this.selNode;if(A){A.ui.onSelectedChange(false);this.selNode=null;this.fireEvent("selectionchange",this,null)}return A},getSelectedNode:function(){return this.selNode},isSelected:function(A){return this.selNode==A},selectPrevious:function(){var A=this.selNode||this.lastSelNode;if(!A){return null}var C=A.previousSibling;if(C){if(!C.isExpanded()||C.childNodes.length<1){return this.select(C)}else{var B=C.lastChild;while(B&&B.isExpanded()&&B.childNodes.length>0){B=B.lastChild}return this.select(B)}}else{if(A.parentNode&&(this.tree.rootVisible||!A.parentNode.isRoot)){return this.select(A.parentNode)}}return null},selectNext:function(){var B=this.selNode||this.lastSelNode;if(!B){return null}if(B.firstChild&&B.isExpanded()){return this.select(B.firstChild)}else{if(B.nextSibling){return this.select(B.nextSibling)}else{if(B.parentNode){var A=null;B.parentNode.bubble(function(){if(this.nextSibling){A=this.getOwnerTree().selModel.select(this.nextSibling);return false}});return A}}}return null},onKeyDown:function(C){var B=this.selNode||this.lastSelNode;var D=this;if(!B){return }var A=C.getKey();switch(A){case C.DOWN:C.stopEvent();this.selectNext();break;case C.UP:C.stopEvent();this.selectPrevious();break;case C.RIGHT:C.preventDefault();if(B.hasChildNodes()){if(!B.isExpanded()){B.expand()}else{if(B.firstChild){this.select(B.firstChild,C)}}}break;case C.LEFT:C.preventDefault();if(B.hasChildNodes()&&B.isExpanded()){B.collapse()}else{if(B.parentNode&&(this.tree.rootVisible||B.parentNode!=this.tree.getRootNode())){this.select(B.parentNode,C)}}break}}});Ext.tree.MultiSelectionModel=function(){this.selNodes=[];this.selMap={};this.addEvents({"selectionchange":true})};Ext.extend(Ext.tree.MultiSelectionModel,Ext.util.Observable,{init:function(A){this.tree=A;A.getTreeEl().on("keydown",this.onKeyDown,this);A.on("click",this.onNodeClick,this)},onNodeClick:function(A,B){this.select(A,B,B.ctrlKey)},select:function(A,C,B){if(B!==true){this.clearSelections(true)}if(this.isSelected(A)){this.lastSelNode=A;return A}this.selNodes.push(A);this.selMap[A.id]=A;this.lastSelNode=A;A.ui.onSelectedChange(true);this.fireEvent("selectionchange",this,this.selNodes);return A},unselect:function(D){if(this.selMap[D.id]){D.ui.onSelectedChange(false);var E=this.selNodes;var B=-1;if(E.indexOf){B=E.indexOf(D)}else{for(var C=0,A=E.length;C0){for(var C=0,A=D.length;C
    ","",this.indentMarkup,"","","",D?("":" />")):"","",C.text,"
    ","
      ",""];if(I!==true&&C.nextSibling&&C.nextSibling.ui.getEl()){this.wrap=Ext.DomHelper.insertHtml("beforeBegin",C.nextSibling.ui.getEl(),B.join(""))}else{this.wrap=Ext.DomHelper.insertHtml("beforeEnd",G,B.join(""))}this.elNode=this.wrap.childNodes[0];this.ctNode=this.wrap.childNodes[1];var F=this.elNode.childNodes;this.indentNode=F[0];this.ecNode=F[1];this.iconNode=F[2];var E=3;if(D){this.checkbox=F[3];E++}this.anchor=F[E];this.textNode=F[E].firstChild},getAnchor:function(){return this.anchor},getTextEl:function(){return this.textNode},getIconEl:function(){return this.iconNode},isChecked:function(){return this.checkbox?this.checkbox.checked:false},updateExpandIcon:function(){if(this.rendered){var F=this.node,D,C;var A=F.isLast()?"x-tree-elbow-end":"x-tree-elbow";var E=F.hasChildNodes();if(E){if(F.expanded){A+="-minus";D="x-tree-node-collapsed";C="x-tree-node-expanded"}else{A+="-plus";D="x-tree-node-expanded";C="x-tree-node-collapsed"}if(this.wasLeaf){this.removeClass("x-tree-node-leaf");this.wasLeaf=false}if(this.c1!=D||this.c2!=C){Ext.fly(this.elNode).replaceClass(D,C);this.c1=D;this.c2=C}}else{if(!this.wasLeaf){Ext.fly(this.elNode).replaceClass("x-tree-node-expanded","x-tree-node-leaf");delete this.c1;delete this.c2;this.wasLeaf=true}}var B="x-tree-ec-icon "+A;if(this.ecc!=B){this.ecNode.className=B;this.ecc=B}}},getChildIndent:function(){if(!this.childIndent){var A=[];var B=this.node;while(B){if(!B.isRoot||(B.isRoot&&B.ownerTree.rootVisible)){if(!B.isLast()){A.unshift("")}else{A.unshift("")}}B=B.parentNode}this.childIndent=A.join("")}return this.childIndent},renderIndent:function(){if(this.rendered){var A="";var B=this.node.parentNode;if(B){A=B.ui.getChildIndent()}if(this.indentMarkup!=A){this.indentNode.innerHTML=A;this.indentMarkup=A}this.updateExpandIcon()}}};Ext.tree.RootTreeNodeUI=function(){Ext.tree.RootTreeNodeUI.superclass.constructor.apply(this,arguments)};Ext.extend(Ext.tree.RootTreeNodeUI,Ext.tree.TreeNodeUI,{render:function(){if(!this.rendered){var A=this.node.ownerTree.innerCt.dom;this.node.expanded=true;A.innerHTML="
      ";this.wrap=this.ctNode=A.firstChild}},collapse:function(){},expand:function(){}}); - - - -Ext.tree.TreeLoader=function(A){this.baseParams={};this.requestMethod="POST";Ext.apply(this,A);this.addEvents({"beforeload":true,"load":true,"loadexception":true});Ext.tree.TreeLoader.superclass.constructor.call(this)};Ext.extend(Ext.tree.TreeLoader,Ext.util.Observable,{uiProviders:{},clearOnLoad:true,load:function(D,E){if(this.clearOnLoad){while(D.firstChild){D.removeChild(D.firstChild)}}if(D.attributes.children){var C=D.attributes.children;for(var B=0,A=C.length;BK){return E?-1:+1}else{return 0}}}};Ext.tree.TreeSorter.prototype={doSort:function(A){A.sort(this.sortFn)},compareNodes:function(B,A){return(B.text.toUpperCase()>A.text.toUpperCase()?1:-1)},updateSort:function(A,B){if(B.childrenRendered){this.doSort.defer(1,this,[B])}}}; - - - -if(Ext.dd.DropZone){Ext.tree.TreeDropZone=function(A,B){this.allowParentInsert=false;this.allowContainerDrop=false;this.appendOnly=false;Ext.tree.TreeDropZone.superclass.constructor.call(this,A.innerCt,B);this.tree=A;this.lastInsertClass="x-tree-no-status";this.dragOverData={}};Ext.extend(Ext.tree.TreeDropZone,Ext.dd.DropZone,{ddGroup:"TreeDD",expandDelay:1000,expandNode:function(A){if(A.hasChildNodes()&&!A.isExpanded()){A.expand(false,null,this.triggerCacheRefresh.createDelegate(this))}},queueExpand:function(A){this.expandProcId=this.expandNode.defer(this.expandDelay,this,[A])},cancelExpand:function(){if(this.expandProcId){clearTimeout(this.expandProcId);this.expandProcId=false}},isValidDropPoint:function(A,I,G,D,C){if(!A||!C){return false}var E=A.node;var F=C.node;if(!(E&&E.isTarget&&I)){return false}if(I=="append"&&E.allowChildren===false){return false}if((I=="above"||I=="below")&&(E.parentNode&&E.parentNode.allowChildren===false)){return false}if(F&&(E==F||F.contains(E))){return false}var B=this.dragOverData;B.tree=this.tree;B.target=E;B.data=C;B.point=I;B.source=G;B.rawEvent=D;B.dropNode=F;B.cancel=false;var H=this.tree.fireEvent("nodedragover",B);return B.cancel===false&&H!==false},getDropPoint:function(E,D,I){var J=D.node;if(J.isRoot){return J.allowChildren!==false?"append":false}var B=D.ddel;var K=Ext.lib.Dom.getY(B),G=K+B.offsetHeight;var F=Ext.lib.Event.getPageY(E);var H=J.allowChildren===false||J.isLeaf();if(this.appendOnly||J.parentNode.allowChildren===false){return H?false:"append"}var C=false;if(!this.allowParentInsert){C=J.hasChildNodes()&&J.isExpanded()}var A=(G-K)/(H?2:3);if(F>=K&&F<(K+A)){return"above"}else{if(!C&&(H||F>=G-A&&F<=G)){return"below"}else{return"append"}}},onNodeEnter:function(D,A,C,B){this.cancelExpand()},onNodeOver:function(B,G,F,E){var I=this.getDropPoint(F,B,G);var C=B.node;if(!this.expandProcId&&I=="append"&&C.hasChildNodes()&&!B.node.isExpanded()){this.queueExpand(C)}else{if(I!="append"){this.cancelExpand()}}var D=this.dropNotAllowed;if(this.isValidDropPoint(B,I,G,F,E)){if(I){var A=B.ddel;var H;if(I=="above"){D=B.node.isFirst()?"x-tree-drop-ok-above":"x-tree-drop-ok-between";H="x-tree-drag-insert-above"}else{if(I=="below"){D=B.node.isLast()?"x-tree-drop-ok-below":"x-tree-drop-ok-between";H="x-tree-drag-insert-below"}else{D="x-tree-drop-ok-append";H="x-tree-drag-append"}}if(this.lastInsertClass!=H){Ext.fly(A).replaceClass(this.lastInsertClass,H);this.lastInsertClass=H}}}return D},onNodeOut:function(D,A,C,B){this.cancelExpand();this.removeDropIndicators(D)},onNodeDrop:function(C,I,E,D){var H=this.getDropPoint(E,C,I);var F=C.node;F.ui.startDrop();if(!this.isValidDropPoint(C,H,I,E,D)){F.ui.endDrop();return false}var G=D.node||(I.getTreeNode?I.getTreeNode(D,F,H,E):null);var B={tree:this.tree,target:F,data:D,point:H,source:I,rawEvent:E,dropNode:G,cancel:!G};var A=this.tree.fireEvent("beforenodedrop",B);if(A===false||B.cancel===true||!B.dropNode){F.ui.endDrop();return false}F=B.target;if(H=="append"&&!F.isExpanded()){F.expand(false,null,function(){this.completeDrop(B)}.createDelegate(this))}else{this.completeDrop(B)}return true},completeDrop:function(G){var D=G.dropNode,E=G.point,C=G.target;if(!(D instanceof Array)){D=[D]}var F;for(var B=0,A=D.length;BD.offsetLeft){E.scrollLeft=D.offsetLeft}var A=Math.min(this.maxWidth,(E.clientWidth>20?E.clientWidth:E.offsetWidth)-Math.max(0,D.offsetLeft-E.scrollLeft)-5);this.setSize(A,"")},triggerEdit:function(A){this.completeEdit();this.editNode=A;this.startEdit(A.ui.textNode,A.text)},bindScroll:function(){this.tree.getTreeEl().on("scroll",this.cancelEdit,this)},beforeNodeClick:function(B,C){var A=(this.lastClick?this.lastClick.getElapsed():0);this.lastClick=new Date();if(A>this.editDelay&&this.tree.getSelectionModel().isSelected(B)){C.stopEvent();this.triggerEdit(B);return false}},updateNode:function(A,B){this.tree.getTreeEl().un("scroll",this.cancelEdit,this);this.editNode.setText(B)},onHide:function(){Ext.tree.TreeEditor.superclass.onHide.call(this);if(this.editNode){this.editNode.ui.focus()}},onSpecialKey:function(C,B){var A=B.getKey();if(A==B.ESC){B.stopEvent();this.cancelEdit()}else{if(A==B.ENTER&&!B.hasModifier()){B.stopEvent();this.completeEdit()}}}}); - - - -Ext.form.Field=function(A){Ext.form.Field.superclass.constructor.call(this,A)};Ext.extend(Ext.form.Field,Ext.BoxComponent,{invalidClass:"x-form-invalid",invalidText:"The value in this field is invalid",focusClass:"x-form-focus",validationEvent:"keyup",validateOnBlur:true,validationDelay:250,defaultAutoCreate:{tag:"input",type:"text",size:"20",autocomplete:"off"},fieldClass:"x-form-field",msgTarget:"qtip",msgFx:"normal",readOnly:false,disabled:false,inputType:undefined,tabIndex:undefined,isFormField:true,hasFocus:false,value:undefined,initComponent:function(){Ext.form.Field.superclass.initComponent.call(this);this.addEvents({focus:true,blur:true,specialkey:true,change:true,invalid:true,valid:true})},getName:function(){return this.rendered&&this.el.dom.name?this.el.dom.name:(this.hiddenName||"")},onRender:function(C,A){Ext.form.Field.superclass.onRender.call(this,C,A);if(!this.el){var B=this.getAutoCreate();if(!B.name){B.name=this.name||this.id}if(this.inputType){B.type=this.inputType}this.el=C.createChild(B,A)}var D=this.el.dom.type;if(D){if(D=="password"){D="text"}this.el.addClass("x-form-"+D)}if(this.readOnly){this.el.dom.readOnly=true}if(this.tabIndex!==undefined){this.el.dom.setAttribute("tabIndex",this.tabIndex)}this.el.addClass([this.fieldClass,this.cls]);this.initValue()},applyTo:function(A){this.allowDomMove=false;this.el=Ext.get(A);this.render(this.el.dom.parentNode);return this},initValue:function(){if(this.value!==undefined){this.setValue(this.value)}else{if(this.el.dom.value.length>0){this.setValue(this.el.dom.value)}}},isDirty:function(){if(this.disabled){return false}return String(this.getValue())!==String(this.originalValue)},afterRender:function(){Ext.form.Field.superclass.afterRender.call(this);this.initEvents()},fireKey:function(A){if(A.isNavKeyPress()){this.fireEvent("specialkey",this,A)}},reset:function(){this.setValue(this.originalValue);this.clearInvalid()},initEvents:function(){this.el.on(Ext.isIE?"keydown":"keypress",this.fireKey,this);this.el.on("focus",this.onFocus,this);this.el.on("blur",this.onBlur,this);this.originalValue=this.getValue()},onFocus:function(){if(!Ext.isOpera&&this.focusClass){this.el.addClass(this.focusClass)}if(!this.hasFocus){this.hasFocus=true;this.startValue=this.getValue();this.fireEvent("focus",this)}},beforeBlur:Ext.emptyFn,onBlur:function(){this.beforeBlur();if(!Ext.isOpera&&this.focusClass){this.el.removeClass(this.focusClass)}this.hasFocus=false;if(this.validationEvent!==false&&this.validateOnBlur&&this.validationEvent!="blur"){this.validate()}var A=this.getValue();if(String(A)!==String(this.startValue)){this.fireEvent("change",this,A,this.startValue)}this.fireEvent("blur",this)},isValid:function(A){if(this.disabled){return true}var C=this.preventMark;this.preventMark=A===true;var B=this.validateValue(this.processValue(this.getRawValue()));this.preventMark=C;return B},validate:function(){if(this.disabled||this.validateValue(this.processValue(this.getRawValue()))){this.clearInvalid();return true}return false},processValue:function(A){return A},validateValue:function(A){return true},markInvalid:function(C){if(!this.rendered||this.preventMark){return }this.el.addClass(this.invalidClass);C=C||this.invalidText;switch(this.msgTarget){case"qtip":this.el.dom.qtip=C;this.el.dom.qclass="x-form-invalid-tip";if(Ext.QuickTips){Ext.QuickTips.enable()}break;case"title":this.el.dom.title=C;break;case"under":if(!this.errorEl){var B=this.el.findParent(".x-form-element",5,true);this.errorEl=B.createChild({cls:"x-form-invalid-msg"});this.errorEl.setWidth(B.getWidth(true)-20)}this.errorEl.update(C);Ext.form.Field.msgFx[this.msgFx].show(this.errorEl,this);break;case"side":if(!this.errorIcon){var B=this.el.findParent(".x-form-element",5,true);this.errorIcon=B.createChild({cls:"x-form-invalid-icon"})}this.alignErrorIcon();this.errorIcon.dom.qtip=C;this.errorIcon.dom.qclass="x-form-invalid-tip";this.errorIcon.show();this.on("resize",this.alignErrorIcon,this);break;default:var A=Ext.getDom(this.msgTarget);A.innerHTML=C;A.style.display=this.msgDisplay;break}this.fireEvent("invalid",this,C)},alignErrorIcon:function(){this.errorIcon.alignTo(this.el,"tl-tr",[2,0])},clearInvalid:function(){if(!this.rendered||this.preventMark){return }this.el.removeClass(this.invalidClass);switch(this.msgTarget){case"qtip":this.el.dom.qtip="";break;case"title":this.el.dom.title="";break;case"under":if(this.errorEl){Ext.form.Field.msgFx[this.msgFx].hide(this.errorEl,this)}break;case"side":if(this.errorIcon){this.errorIcon.dom.qtip="";this.errorIcon.hide();this.un("resize",this.alignErrorIcon,this)}break;default:var A=Ext.getDom(this.msgTarget);A.innerHTML="";A.style.display="none";break}this.fireEvent("valid",this)},getRawValue:function(){var A=this.el.getValue();if(A===this.emptyText){A=""}return A},getValue:function(){var A=this.el.getValue();if(A===this.emptyText||A===undefined){A=""}return A},setRawValue:function(A){return this.el.dom.value=(A===null||A===undefined?"":A)},setValue:function(A){this.value=A;if(this.rendered){this.el.dom.value=(A===null||A===undefined?"":A);this.validate()}},adjustSize:function(A,C){var B=Ext.form.Field.superclass.adjustSize.call(this,A,C);B.width=this.adjustWidth(this.el.dom.tagName,B.width);return B},adjustWidth:function(A,B){A=A.toLowerCase();if(typeof B=="number"&&Ext.isStrict&&!Ext.isSafari){if(Ext.isIE&&(A=="input"||A=="textarea")){if(A=="input"){return B+2}if(A="textarea"){return B-2}}else{if(Ext.isOpera){if(A=="input"){return B+2}if(A="textarea"){return B-2}}}}return B}});Ext.form.Field.msgFx={normal:{show:function(A,B){A.setDisplayed("block")},hide:function(A,B){A.setDisplayed(false).update("")}},slide:{show:function(A,B){A.slideIn("t",{stopFx:true})},hide:function(A,B){A.slideOut("t",{stopFx:true,useDisplay:true})}},slideRight:{show:function(A,B){A.fixDisplay();A.alignTo(B.el,"tl-tr");A.slideIn("l",{stopFx:true})},hide:function(A,B){A.slideOut("l",{stopFx:true,useDisplay:true})}}}; - - - -Ext.form.TextField=function(A){Ext.form.TextField.superclass.constructor.call(this,A);this.addEvents({autosize:true})};Ext.extend(Ext.form.TextField,Ext.form.Field,{grow:false,growMin:30,growMax:800,vtype:null,maskRe:null,disableKeyFilter:false,allowBlank:true,minLength:0,maxLength:Number.MAX_VALUE,minLengthText:"The minimum length for this field is {0}",maxLengthText:"The maximum length for this field is {0}",selectOnFocus:false,blankText:"This field is required",validator:null,regex:null,regexText:"",emptyText:null,emptyClass:"x-form-empty-field",initEvents:function(){Ext.form.TextField.superclass.initEvents.call(this);if(this.validationEvent=="keyup"){this.validationTask=new Ext.util.DelayedTask(this.validate,this);this.el.on("keyup",this.filterValidation,this)}else{if(this.validationEvent!==false){this.el.on(this.validationEvent,this.validate,this,{buffer:this.validationDelay})}}if(this.selectOnFocus||this.emptyText){this.on("focus",this.preFocus,this);if(this.emptyText){this.on("blur",this.postBlur,this);this.applyEmptyText()}}if(this.maskRe||(this.vtype&&this.disableKeyFilter!==true&&(this.maskRe=Ext.form.VTypes[this.vtype+"Mask"]))){this.el.on("keypress",this.filterKeys,this)}if(this.grow){this.el.on("keyup",this.onKeyUp,this,{buffer:50});this.el.on("click",this.autoSize,this)}},processValue:function(A){if(this.stripCharsRe){var B=A.replace(this.stripCharsRe,"");if(B!==A){this.setRawValue(B);return B}}return A},filterValidation:function(A){if(!A.isNavKeyPress()){this.validationTask.delay(this.validationDelay)}},onKeyUp:function(A){if(!A.isNavKeyPress()){this.autoSize()}},reset:function(){Ext.form.TextField.superclass.reset.call(this);this.applyEmptyText()},applyEmptyText:function(){if(this.rendered&&this.emptyText&&this.getRawValue().length<1){this.setRawValue(this.emptyText);this.el.addClass(this.emptyClass)}},preFocus:function(){if(this.emptyText){if(this.el.dom.value==this.emptyText){this.setRawValue("")}this.el.removeClass(this.emptyClass)}if(this.selectOnFocus){this.el.dom.select()}},postBlur:function(){this.applyEmptyText()},filterKeys:function(B){var A=B.getKey();if(!Ext.isIE&&(B.isNavKeyPress()||A==B.BACKSPACE||(A==B.DELETE&&B.button==-1))){return }var D=B.getCharCode(),C=String.fromCharCode(D);if(Ext.isIE&&(B.isSpecialKey()||!C)){return }if(!this.maskRe.test(C)){B.stopEvent()}},setValue:function(A){if(this.emptyText&&this.el&&A!==undefined&&A!==null&&A!==""){this.el.removeClass(this.emptyClass)}Ext.form.TextField.superclass.setValue.apply(this,arguments);this.applyEmptyText();this.autoSize()},validateValue:function(A){if(A.length<1||A===this.emptyText){if(this.allowBlank){this.clearInvalid();return true}else{this.markInvalid(this.blankText);return false}}if(A.lengththis.maxLength){this.markInvalid(String.format(this.maxLengthText,this.maxLength));return false}if(this.vtype){var C=Ext.form.VTypes;if(!C[this.vtype](A,this)){this.markInvalid(this.vtypeText||C[this.vtype+"Text"]);return false}}if(typeof this.validator=="function"){var B=this.validator(A);if(B!==true){this.markInvalid(B);return false}}if(this.regex&&!this.regex.test(A)){this.markInvalid(this.regexText);return false}return true},selectText:function(E,A){var C=this.getRawValue();if(C.length>0){E=E===undefined?0:E;A=A===undefined?C.length:A;var D=this.el.dom;if(D.setSelectionRange){D.setSelectionRange(E,A)}else{if(D.createTextRange){var B=D.createTextRange();B.moveStart("character",E);B.moveEnd("character",C.length-A);B.select()}}}},autoSize:function(){if(!this.grow||!this.rendered){return }if(!this.metrics){this.metrics=Ext.util.TextMetrics.createInstance(this.el)}var C=this.el;var B=C.dom.value;var D=document.createElement("div");D.appendChild(document.createTextNode(B));B=D.innerHTML;D=null;B+=" ";var A=Math.min(this.growMax,Math.max(this.metrics.getWidth(B)+10,this.growMin));this.el.setWidth(A);this.fireEvent("autosize",this,A)}}); - - diff --git a/public/js/extjs/fix-defer.js b/public/js/extjs/fix-defer.js deleted file mode 100644 index d0f86e14e..000000000 --- a/public/js/extjs/fix-defer.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Maho - * - * @category Mage - * @package js - * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) - * @copyright Copyright (c) 2022-2023 The OpenMage Contributors (https://openmage.org) - * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) - */ - -/* - * Both ExtJS and PrototypeJS write to Function.prototype.defer - * However, PrototypeJS has a default delay of 0.01s if no first argument is provided - * Ref: https://github.com/prototypejs/prototype/blob/1.7.3/src/prototype/lang/function.js#L292-L295 - * While ExtJS executes the function immediately. Presumably this causes an error for - * PrototypeJS Ajax calls. - * - */ - -(function(){ - var eDefer = Function.prototype.defer; - Function.prototype.defer = function() { - var argLen = arguments.length; - if (argLen==0 || (argLen==1 && arguments[0]==1)) { - //common for Prototype Ajax requests - return this.delay.curry(0.01).apply(this, arguments); - } - - return eDefer.apply(this, arguments); - } -})(); diff --git a/public/js/extjs/resources/css/ytheme-magento.css b/public/js/extjs/resources/css/ytheme-magento.css deleted file mode 100644 index 7fcc40bd7..000000000 --- a/public/js/extjs/resources/css/ytheme-magento.css +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Ext JS Library 1.1.1 - * Copyright(c) 2006-2007, Ext JS, LLC. - * licensing@extjs.com - * - * http://www.extjs.com/license - */ - -/* Tree */ -.x-tree-icon, -.x-tree-ec-icon, -.x-tree-elbow-line, -.x-tree-elbow, -.x-tree-elbow-end, -.x-tree-elbow-plus, -.x-tree-elbow-minus, -.x-tree-elbow-end-plus, -.x-tree-elbow-end-minus { - border: 0 none; - height: 18px; - margin: 0; - padding: 0; - vertical-align: middle; - width: 16px; - background-repeat: no-repeat; -} - -.x-tree-node-collapsed .x-tree-node-icon, -.x-tree-node-expanded .x-tree-node-icon, -.x-tree-node-leaf .x-tree-node-icon { - border: 0 none; - height: 18px; - margin: 0; - padding: 0; - vertical-align: middle; - width: 16px; - background-position: center; - background-repeat: no-repeat; -} - -/* some default icons for leaf/folder */ -.x-tree-node-collapsed .x-tree-node-icon { - background-image: url(../images/default/tree/folder.gif); -} - -.x-tree-node-expanded .x-tree-node-icon { - background-image: url(../images/default/tree/folder-open.gif); -} - -.x-tree-node-leaf .x-tree-node-icon { - background-image: url(../images/default/tree/leaf.gif); -} - -/* checkboxes */ -input.l-tcb { - height:13px; - width:13px; - margin-left:2px -} - -input.x-tree-node-cb { - margin-left: 1px; -} - -.x-tree-noicon .x-tree-node-icon { - width: 0; - height: 0; -} - -/* loading icon */ -.x-tree-node-loading .x-tree-node-icon { - background-image: url(../images/default/tree/loading.gif) !important; -} - -.x-tree-node-loading a span { - font-style: italic; - color: #444444; -} - -/* Line styles */ -.x-tree-lines .x-tree-elbow { - background-image: url(../images/default/tree/elbow.gif); -} - -.x-tree-lines .x-tree-elbow-plus { - background-image: url(../images/default/tree/elbow-plus.gif); -} - -.x-tree-lines .x-tree-elbow-minus { - background-image: url(../images/default/tree/elbow-minus.gif); -} - -.x-tree-lines .x-tree-elbow-end { - background-image: url(../images/default/tree/elbow-end.gif); -} - -.x-tree-lines .x-tree-elbow-end-plus { - background-image: url(../images/default/tree/elbow-end-plus.gif); -} - -.x-tree-lines .x-tree-elbow-end-minus { - background-image: url(../images/default/tree/elbow-end-minus.gif); -} - -.x-tree-lines .x-tree-elbow-line { - background-image: url(../images/default/tree/elbow-line.gif); -} - -/* No line styles */ -.x-tree-no-lines .x-tree-elbow { - background: transparent; -} - -.x-tree-no-lines .x-tree-elbow-plus { - background-image: url(../images/default/tree/elbow-plus-nl.gif); -} - -.x-tree-no-lines .x-tree-elbow-minus { - background-image: url(../images/default/tree/elbow-minus-nl.gif); -} - -.x-tree-no-lines .x-tree-elbow-end { - background: transparent; -} - -.x-tree-no-lines .x-tree-elbow-end-plus { - background-image: url(../images/default/tree/elbow-end-plus-nl.gif); -} - -.x-tree-no-lines .x-tree-elbow-end-minus { - background-image: url(../images/default/tree/elbow-end-minus-nl.gif); -} - -.x-tree-no-lines .x-tree-elbow-line { - background: transparent; -} - -.x-tree-elbow-plus, -.x-tree-elbow-minus, -.x-tree-elbow-end-plus, -.x-tree-elbow-end-minus { - cursor: pointer; -} - -.x-tree-node { - color: black; - font: normal 12px arial, tahoma, helvetica, sans-serif; - white-space: nowrap; - list-style-type: none; -} - -.x-tree-node a, -.x-dd-drag-ghost a { - text-decoration: none; - color: black; - outline: 0 none; -} - -.x-tree-node a span, -.x-dd-drag-ghost a span { - text-decoration: none; - color: black; - padding: 1px 3px 1px 2px; -} - -.x-tree-node .x-tree-node-disabled a span { - color: gray !important; -} - -.x-tree-node .x-tree-node-disabled .x-tree-node-icon { - opacity: .5; -} - -.x-tree-node .x-tree-node-inline-icon { - background: transparent; -} - -.x-tree-node a:hover, -.x-dd-drag-ghost a:hover { - text-decoration: none; -} - -.x-tree-node div.x-tree-drag-insert-below { - border-bottom: 1px dotted #3366cc; -} - -.x-tree-node div.x-tree-drag-insert-above { - border-top: 1px dotted #3366cc; -} - -.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below { - border-bottom: 0 none; -} - -.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above { - border-top: 0 none; -} - -.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below a { - border-bottom: 2px solid #3366cc; -} - -.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above a { - border-top: 2px solid #3366cc; -} - -.x-tree-node .x-tree-drag-append a span { - background: #dddddd; - border: 1px dotted gray; -} - -.x-tree-node .x-tree-selected a span { - background: #f5d6c7; - color: #000; -} - -.x-dd-drag-ghost .x-tree-node-indent, -.x-dd-drag-ghost .x-tree-ec-icon { - display: none !important; -} - -.x-tree-drop-ok-append .x-dd-drop-icon { - background-image: url(../images/default/tree/drop-add.gif); -} - -.x-tree-drop-ok-above .x-dd-drop-icon { - background-image: url(../images/default/tree/drop-over.gif); -} - -.x-tree-drop-ok-below .x-dd-drop-icon { - background-image: url(../images/default/tree/drop-under.gif); -} - -.x-tree-drop-ok-between .x-dd-drop-icon { - background-image: url(../images/default/tree/drop-between.gif); -} diff --git a/public/js/extjs/resources/images/default/tree/drop-add.gif b/public/js/extjs/resources/images/default/tree/drop-add.gif deleted file mode 100644 index b22cd1448..000000000 Binary files a/public/js/extjs/resources/images/default/tree/drop-add.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/drop-between.gif b/public/js/extjs/resources/images/default/tree/drop-between.gif deleted file mode 100644 index 5c6c09d98..000000000 Binary files a/public/js/extjs/resources/images/default/tree/drop-between.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/drop-no.gif b/public/js/extjs/resources/images/default/tree/drop-no.gif deleted file mode 100644 index 9d9c6a9ce..000000000 Binary files a/public/js/extjs/resources/images/default/tree/drop-no.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/drop-over.gif b/public/js/extjs/resources/images/default/tree/drop-over.gif deleted file mode 100644 index 30d1ca710..000000000 Binary files a/public/js/extjs/resources/images/default/tree/drop-over.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/drop-under.gif b/public/js/extjs/resources/images/default/tree/drop-under.gif deleted file mode 100644 index 85f66b1e5..000000000 Binary files a/public/js/extjs/resources/images/default/tree/drop-under.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/drop-yes.gif b/public/js/extjs/resources/images/default/tree/drop-yes.gif deleted file mode 100644 index 8aacb307e..000000000 Binary files a/public/js/extjs/resources/images/default/tree/drop-yes.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-end-minus-nl.gif b/public/js/extjs/resources/images/default/tree/elbow-end-minus-nl.gif deleted file mode 100644 index b0ee9ed12..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-end-minus-nl.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-end-minus.gif b/public/js/extjs/resources/images/default/tree/elbow-end-minus.gif deleted file mode 100644 index fffcde9f7..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-end-minus.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-end-plus-nl.gif b/public/js/extjs/resources/images/default/tree/elbow-end-plus-nl.gif deleted file mode 100644 index b0ee9ed12..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-end-plus-nl.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-end-plus.gif b/public/js/extjs/resources/images/default/tree/elbow-end-plus.gif deleted file mode 100644 index bf4311c7b..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-end-plus.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-end.gif b/public/js/extjs/resources/images/default/tree/elbow-end.gif deleted file mode 100644 index 7cf667a52..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-end.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-line.gif b/public/js/extjs/resources/images/default/tree/elbow-line.gif deleted file mode 100644 index 0709423fc..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-line.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-minus-nl.gif b/public/js/extjs/resources/images/default/tree/elbow-minus-nl.gif deleted file mode 100644 index f62b7c5e0..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-minus-nl.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-minus.gif b/public/js/extjs/resources/images/default/tree/elbow-minus.gif deleted file mode 100644 index 7daf59459..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-minus.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-plus-nl.gif b/public/js/extjs/resources/images/default/tree/elbow-plus-nl.gif deleted file mode 100644 index 31e8cd140..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-plus-nl.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow-plus.gif b/public/js/extjs/resources/images/default/tree/elbow-plus.gif deleted file mode 100644 index 8c91262c2..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow-plus.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/elbow.gif b/public/js/extjs/resources/images/default/tree/elbow.gif deleted file mode 100644 index cb769ea31..000000000 Binary files a/public/js/extjs/resources/images/default/tree/elbow.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/folder-open.gif b/public/js/extjs/resources/images/default/tree/folder-open.gif deleted file mode 100644 index 193ac35c8..000000000 Binary files a/public/js/extjs/resources/images/default/tree/folder-open.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/folder.gif b/public/js/extjs/resources/images/default/tree/folder.gif deleted file mode 100644 index 94abaa778..000000000 Binary files a/public/js/extjs/resources/images/default/tree/folder.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/leaf.gif b/public/js/extjs/resources/images/default/tree/leaf.gif deleted file mode 100644 index 193ac35c8..000000000 Binary files a/public/js/extjs/resources/images/default/tree/leaf.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/loading.gif b/public/js/extjs/resources/images/default/tree/loading.gif deleted file mode 100644 index e846e1d6c..000000000 Binary files a/public/js/extjs/resources/images/default/tree/loading.gif and /dev/null differ diff --git a/public/js/extjs/resources/images/default/tree/s.gif b/public/js/extjs/resources/images/default/tree/s.gif deleted file mode 100644 index 5bfd67a2d..000000000 Binary files a/public/js/extjs/resources/images/default/tree/s.gif and /dev/null differ diff --git a/public/js/extjs/resources/license.txt b/public/js/extjs/resources/license.txt deleted file mode 100644 index 5e421141e..000000000 --- a/public/js/extjs/resources/license.txt +++ /dev/null @@ -1,27 +0,0 @@ -Ext JS - JavaScript Library -Copyright (c) 2006-2007, Ext JS, LLC -All rights reserved. -licensing@extjs.com - -http://extjs.com/license - -The CSS and Graphics ("Assets") distributed with Ext are licensed for use ONLY -with their associated Ext JavaScript component ("Component"). Use of the Assets in -any way that does not also include the Component is prohibited without explicit -permission from Ext JS, LLC. Deriving images and CSS from the Assets in an effort -to bypass this license is also prohibited. - --- - -The JavaScript code distributed with Ext (the "Software") is licensed under the -Lesser GNU (LGPL) open source license version 3.0. - -http://www.gnu.org/licenses/lgpl.html - -If you are using this library for commercial purposes, we encourage you to purchase -a commercial license. Please visit http://extjs.com/license for more details. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. \ No newline at end of file diff --git a/public/js/html5sortable.min.js b/public/js/html5sortable.min.js deleted file mode 100644 index b8109cb83..000000000 --- a/public/js/html5sortable.min.js +++ /dev/null @@ -1,2 +0,0 @@ -var sortable=function(){"use strict";function c(e,t,n){if(void 0===n)return e&&e.h5s&&e.h5s.data&&e.h5s.data[t];e.h5s=e.h5s||{},e.h5s.data=e.h5s.data||{},e.h5s.data[t]=n}var v=function(e,t){if(!(e instanceof NodeList||e instanceof HTMLCollection||e instanceof Array))throw new Error("You must provide a nodeList/HTMLCollection/Array of elements to be filtered.");return"string"!=typeof t?Array.from(e):Array.from(e).filter(function(e){return 1===e.nodeType&&e.matches(t)})},y=new Map,t=function(){function e(){this._config=new Map,this._placeholder=void 0,this._data=new Map}return Object.defineProperty(e.prototype,"config",{get:function(){var n={};return this._config.forEach(function(e,t){n[t]=e}),n},set:function(e){if("object"!=typeof e)throw new Error("You must provide a valid configuration object to the config setter.");var t=Object.assign({},e);this._config=new Map(Object.entries(t))},enumerable:!1,configurable:!0}),e.prototype.setConfig=function(e,t){if(!this._config.has(e))throw new Error("Trying to set invalid configuration item: ".concat(e));this._config.set(e,t)},e.prototype.getConfig=function(e){if(!this._config.has(e))throw new Error("Invalid configuration item requested: ".concat(e));return this._config.get(e)},Object.defineProperty(e.prototype,"placeholder",{get:function(){return this._placeholder},set:function(e){if(!(e instanceof HTMLElement)&&null!==e)throw new Error("A placeholder must be an html element or null.");this._placeholder=e},enumerable:!1,configurable:!0}),e.prototype.setData=function(e,t){if("string"!=typeof e)throw new Error("The key must be a string.");this._data.set(e,t)},e.prototype.getData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.get(e)},e.prototype.deleteData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.delete(e)},e}(),E=function(e){if(!(e instanceof HTMLElement))throw new Error("Please provide a sortable to the store function.");return y.has(e)||y.set(e,new t),y.get(e)};function i(e,t,n){if(e instanceof Array)for(var r=0;r':t=document.createElement("div")),"string"==typeof n&&(r=t.classList).add.apply(r,n.split(" ")),t},L=function(e){if(!(e instanceof HTMLElement))throw new Error("You must provide a valid dom element");var n=window.getComputedStyle(e);return"border-box"===n.getPropertyValue("box-sizing")?parseInt(n.getPropertyValue("height"),10):["height","padding-top","padding-bottom"].map(function(e){var t=parseInt(n.getPropertyValue(e),10);return isNaN(t)?0:t}).reduce(function(e,t){return e+t})},D=function(e){if(!(e instanceof HTMLElement))throw new Error("You must provide a valid dom element");var n=window.getComputedStyle(e);return["width","padding-left","padding-right"].map(function(e){var t=parseInt(n.getPropertyValue(e),10);return isNaN(t)?0:t}).reduce(function(e,t){return e+t})},s=function(e,t){if(!(e instanceof Array))throw new Error("You must provide a Array of HTMLElements to be filtered.");return"string"!=typeof t?e:e.filter(function(e){return e.querySelector(t)instanceof HTMLElement||e.shadowRoot&&e.shadowRoot.querySelector(t)instanceof HTMLElement}).map(function(e){return e.querySelector(t)||e.shadowRoot&&e.shadowRoot.querySelector(t)})},p=function(e){return e.composedPath&&e.composedPath()[0]||e.target},m=function(e,t,n){return{element:e,posX:n.pageX-t.left,posY:n.pageY-t.top}},g=function(e,t,n){if(!(e instanceof Event))throw new Error("setDragImage requires a DragEvent as the first argument.");if(!(t instanceof HTMLElement))throw new Error("setDragImage requires the dragged element as the second argument.");if(n||(n=m),n instanceof HTMLElement){var r=w(n),o={element:n,posX:e.pageX-r.left,posY:e.pageY-r.top};e.dataTransfer.effectAllowed="copyMove",e.dataTransfer.setData("text/plain",p(e).id),e.dataTransfer.setDragImage(o.element,e.offsetX,e.offsetY)}else if("function"==typeof n&&e.dataTransfer.setDragImage){if(!((o=n(t,r=w(t),e)).element instanceof HTMLElement)||"number"!=typeof o.posX||"number"!=typeof o.posY)throw new Error("The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].");e.dataTransfer.effectAllowed="copyMove",e.dataTransfer.setData("text/plain",p(e).id),e.dataTransfer.setDragImage(o.element,o.posX,o.posY)}},M=function(e,t){if(!0===e.isSortable){var n=E(e).getConfig("acceptFrom");if(null!==n&&!1!==n&&"string"!=typeof n)throw new Error('HTML5Sortable: Wrong argument, "acceptFrom" must be "null", "false", or a valid selector string.');if(null!==n)return!1!==n&&0=parseInt(r.maxItems)&&I.parentElement!==n||(e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect=!0===E(n).getConfig("copy")?"copy":"move",o(n,t,e.pageX,e.pageY))}};i(t.concat(s),"dragover",r),i(t.concat(s),"dragenter",r)}),e)}return V.destroy=function(e){var t,n,r,o;n=c(t=e,"opts")||{},r=v(t.children,n.items),o=s(r,n.handle),h(t,!1),a(t,"dragover"),a(t,"dragenter"),a(t,"dragstart"),a(t,"dragend"),a(t,"drop"),q(t),a(o,"mousedown"),N(r),F(r),W(r),W([t]),X(S,P),t.isSortable=!1},V.enable=function(e){k(e)},V.disable=function(e){var t,n,r,o;n=c(t=e,"opts"),r=v(t.children,n.items),o=s(r,n.handle),l(t,"aria-dropeffect","none"),c(t,"_disabled","true"),l(o,"draggable","false"),a(o,"mousedown"),h(t,!1)},V.__testing={data:c,removeItemEvents:N,removeItemData:F,removeSortableData:q,removeContainerEvents:X},V}(); -//# sourceMappingURL=html5sortable.min.js.map diff --git a/public/js/html5sortable.min.js.map b/public/js/html5sortable.min.js.map deleted file mode 100644 index 6ffabaed0..000000000 --- a/public/js/html5sortable.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"html5sortable.min.js","sources":["../src/data.ts","../src/filter.ts","../src/store.ts","../src/eventListener.ts","../src/attribute.ts","../src/offset.ts","../src/debounce.ts","../src/getIndex.ts","../src/isInDom.ts","../src/insertHtmlElements.ts","../src/serialize.ts","../src/makePlaceholder.ts","../src/elementHeight.ts","../src/elementWidth.ts","../src/getHandles.ts","../src/getEventTarget.ts","../src/setDragImage.ts","../src/isConnected.ts","../src/defaultConfiguration.ts","../src/hoverClass.ts","../src/html5sortable.ts","../src/throttle.ts"],"sourcesContent":["/**\n * Get or set data on element\n * @param {HTMLElement} element\n * @param {string} key\n * @param {any} value\n * @return {*}\n */\n\nfunction addData (element: HTMLElement, key: string, value?: any): HTMLElement|configuration|string|void {\n if (value === undefined) {\n return element && element.h5s && element.h5s.data && element.h5s.data[key]\n } else {\n element.h5s = element.h5s || {}\n element.h5s.data = element.h5s.data || {}\n element.h5s.data[key] = value\n }\n}\n/**\n * Remove data from element\n * @param {HTMLElement} element\n */\nfunction removeData (element: HTMLElement) {\n if (element.h5s) {\n delete element.h5s.data\n }\n}\n\nexport { addData, removeData }\n","/* eslint-env browser */\n/**\n * Filter only wanted nodes\n * @param {NodeList|HTMLCollection|Array} nodes\n * @param {String} selector\n * @returns {Array}\n */\nexport default (nodes: NodeList|HTMLCollection|Array, selector: string): Array => {\n if (!(nodes instanceof NodeList || nodes instanceof HTMLCollection || nodes instanceof Array)) {\n throw new Error('You must provide a nodeList/HTMLCollection/Array of elements to be filtered.')\n }\n if (typeof selector !== 'string') {\n return Array.from(nodes)\n }\n\n return Array.from(nodes).filter((item) => item.nodeType === 1 && item.matches(selector))\n}\n","/* eslint-env browser */\n/* eslint-disable no-use-before-define */\nimport { Store as StoreInterface } from './types/store'\nexport const stores: Map = new Map()\n/* eslint-enable no-use-before-define */\n/**\n * Stores data & configurations per Sortable\n * @param {Object} config\n */\nexport class Store implements StoreInterface {\n private _config: Map = new Map() // eslint-disable-line no-undef\n private _placeholder?: HTMLElement = undefined // eslint-disable-line no-undef\n private _data: Map = new Map() // eslint-disable-line no-undef\n /**\n * set the configuration of a class instance\n * @method config\n * @param {object} config object of configurations\n */\n set config (config: configuration) {\n if (typeof config !== 'object') {\n throw new Error('You must provide a valid configuration object to the config setter.')\n }\n // combine config with default\n const mergedConfig = Object.assign({}, config)\n // add config to map\n this._config = new Map(Object.entries(mergedConfig))\n }\n /**\n * get the configuration map of a class instance\n * @method config\n * @return {object}\n */\n\n get config (): configuration {\n // transform Map to object\n const config = {}\n this._config.forEach((value, key) => {\n config[key] = value\n })\n // return object\n return config\n }\n\n /**\n * set individual configuration of a class instance\n * @method setConfig\n * @param key valid configuration key\n * @param value any value\n * @return void\n */\n setConfig (key: string, value: any): void {\n if (!this._config.has(key)) {\n throw new Error(`Trying to set invalid configuration item: ${key}`)\n }\n // set config\n this._config.set(key, value)\n }\n\n /**\n * get an individual configuration of a class instance\n * @method getConfig\n * @param key valid configuration key\n * @return any configuration value\n */\n getConfig (key: string): any {\n if (!this._config.has(key)) {\n throw new Error(`Invalid configuration item requested: ${key}`)\n }\n return this._config.get(key)\n }\n\n /**\n * get the placeholder for a class instance\n * @method placeholder\n * @return {HTMLElement|null}\n */\n get placeholder (): HTMLElement {\n return this._placeholder\n }\n\n /**\n * set the placeholder for a class instance\n * @method placeholder\n * @param {HTMLElement} placeholder\n * @return {void}\n */\n set placeholder (placeholder: HTMLElement) {\n if (!(placeholder instanceof HTMLElement) && placeholder !== null) {\n throw new Error('A placeholder must be an html element or null.')\n }\n this._placeholder = placeholder\n }\n\n /**\n * set an data entry\n * @method setData\n * @param {string} key\n * @param {any} value\n * @return {void}\n */\n setData (key: string, value: Function): void {\n if (typeof key !== 'string') {\n throw new Error('The key must be a string.')\n }\n this._data.set(key, value)\n }\n\n /**\n * get an data entry\n * @method getData\n * @param {string} key an existing key\n * @return {any}\n */\n getData (key: string): any {\n if (typeof key !== 'string') {\n throw new Error('The key must be a string.')\n }\n return this._data.get(key)\n }\n\n /**\n * delete an data entry\n * @method deleteData\n * @param {string} key an existing key\n * @return {boolean}\n */\n deleteData (key: string): boolean {\n if (typeof key !== 'string') {\n throw new Error('The key must be a string.')\n }\n return this._data.delete(key)\n }\n}\n/**\n * @param {HTMLElement} sortableElement\n * @returns {Class: Store}\n */\nexport default (sortableElement: HTMLElement): Store => {\n // if sortableElement is wrong type\n if (!(sortableElement instanceof HTMLElement)) {\n throw new Error('Please provide a sortable to the store function.')\n }\n // create new instance if not avilable\n if (!stores.has(sortableElement)) {\n stores.set(sortableElement, new Store())\n }\n // return instance\n return stores.get(sortableElement)\n}\n","import store from './store'\n/**\n * @param {Array|HTMLElement} element\n * @param {Function} callback\n * @param {string} event\n */\nfunction addEventListener (element: Array|HTMLElement, eventName:string, callback: () => void) {\n if (element instanceof Array) {\n for (let i = 0; i < element.length; ++i) {\n addEventListener(element[i], eventName, callback)\n }\n return\n }\n element.addEventListener(eventName, callback)\n store(element).setData(`event${eventName}`, callback)\n}\n/**\n * @param {Array|HTMLElement} element\n * @param {string} eventName\n */\nfunction removeEventListener (element: Array|HTMLElement, eventName: string) {\n if (element instanceof Array) {\n for (let i = 0; i < element.length; ++i) {\n removeEventListener(element[i], eventName)\n }\n return\n }\n element.removeEventListener(eventName, store(element).getData(`event${eventName}`))\n store(element).deleteData(`event${eventName}`)\n}\n\nexport { addEventListener, removeEventListener }\n","/**\n * @param {Array|HTMLElement} element\n * @param {string} attribute\n * @param {string} value\n */\nfunction addAttribute (element: Array|HTMLElement, attribute:string, value:string) {\n if (element instanceof Array) {\n for (let i = 0; i < element.length; ++i) {\n addAttribute(element[i], attribute, value)\n }\n return\n }\n element.setAttribute(attribute, value)\n}\n/**\n * @param {Array|HTMLElement} element\n * @param {string} attribute\n */\nfunction removeAttribute (element: Array|HTMLElement, attribute:string) {\n if (element instanceof Array) {\n for (let i = 0; i < element.length; ++i) {\n removeAttribute(element[i], attribute)\n }\n return\n }\n element.removeAttribute(attribute)\n}\n\nexport { addAttribute, removeAttribute }\n","/**\n * @param {HTMLElement} element\n * @returns {Object}\n */\nexport default (element: HTMLElement): offsetObject => {\n if (!element.parentElement || element.getClientRects().length === 0) {\n throw new Error('target element must be part of the dom')\n }\n\n const rect = element.getClientRects()[0]\n return {\n left: rect.left + window.pageXOffset,\n right: rect.right + window.pageXOffset,\n top: rect.top + window.pageYOffset,\n bottom: rect.bottom + window.pageYOffset\n }\n}\n","/**\n * Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed\n * @param {Function} func to debounce\n * @param {number} time to wait before calling function with latest arguments, 0 - no debounce\n * @returns {function} - debounced function\n */\nexport default (func: Function, wait: number = 0): Function => {\n let timeout\n return (...args) => {\n clearTimeout(timeout)\n timeout = setTimeout(() => {\n func(...args)\n }, wait)\n }\n}\n","/* eslint-env browser */\n/**\n * Get position of the element relatively to its sibling elements\n * @param {HTMLElement} element\n * @returns {number}\n */\nexport default (element: HTMLElement, elementList: HTMLCollection | NodeList | Array): number => {\n if (!(element instanceof HTMLElement) || !(elementList instanceof NodeList || elementList instanceof HTMLCollection || elementList instanceof Array)) {\n throw new Error('You must provide an element and a list of elements.')\n }\n\n return Array.from(elementList).indexOf(element)\n}\n","/* eslint-env browser */\n/**\n * Test whether element is in DOM\n * @param {HTMLElement} element\n * @returns {boolean}\n */\nexport default (element: HTMLElement): boolean => {\n if (!(element instanceof HTMLElement)) {\n throw new Error('Element is not a node element.')\n }\n\n return element.parentNode !== null\n}\n","/* eslint-env browser */\n/**\n * Insert node before or after target\n * @param {HTMLElement} referenceNode - reference element\n * @param {HTMLElement} newElement - element to be inserted\n * @param {String} position - insert before or after reference element\n */\nconst insertNode = (referenceNode: HTMLElement, newElement: HTMLElement, position: String) => {\n if (!(referenceNode instanceof HTMLElement) || !(referenceNode.parentElement instanceof HTMLElement)) {\n throw new Error('target and element must be a node')\n }\n referenceNode.parentElement.insertBefore(\n newElement,\n (position === 'before' ? referenceNode : referenceNode.nextElementSibling)\n )\n}\n/**\n * Insert before target\n * @param {HTMLElement} target\n * @param {HTMLElement} element\n */\nconst insertBefore = (target: HTMLElement, element: HTMLElement) => insertNode(target, element, 'before')\n/**\n * Insert after target\n * @param {HTMLElement} target\n * @param {HTMLElement} element\n */\nconst insertAfter = (target: HTMLElement, element: HTMLElement) => insertNode(target, element, 'after')\n\nexport { insertBefore, insertAfter }\n","/* eslint-env browser */\nimport { addData } from './data' // yuk, data really needs to be refactored\nimport filter from './filter'\nimport getIndex from './getIndex'\n/**\n * Filter only wanted nodes\n * @param {HTMLElement} sortableContainer\n * @param {Function} customSerializer\n * @returns {Array}\n */\nexport default (sortableContainer: HTMLElement, customItemSerializer: Function = (serializedItem: serializedItem, sortableContainer: HTMLElement) => serializedItem, customContainerSerializer: Function = (serializedContainer: object) => serializedContainer): object => {\n // check for valid sortableContainer\n if (!(sortableContainer instanceof HTMLElement) || !sortableContainer.isSortable === true) {\n throw new Error('You need to provide a sortableContainer to be serialized.')\n }\n // check for valid serializers\n if (typeof customItemSerializer !== 'function' || typeof customContainerSerializer !== 'function') {\n throw new Error('You need to provide a valid serializer for items and the container.')\n }\n // get options\n const options = addData(sortableContainer, 'opts')\n\n const item: string|undefined = options.items\n\n // serialize container\n const items = filter(sortableContainer.children, item)\n const serializedItems: serializedItem[] = items.map((item) => {\n return {\n parent: sortableContainer,\n node: item,\n html: item.outerHTML,\n index: getIndex(item, items)\n }\n })\n // serialize container\n const container = {\n node: sortableContainer,\n itemCount: serializedItems.length\n }\n\n return {\n container: customContainerSerializer(container),\n items: serializedItems.map((item: object) => customItemSerializer(item, sortableContainer))\n }\n}\n","/* eslint-env browser */\n/**\n * create a placeholder element\n * @param {HTMLElement} sortableElement a single sortable\n * @param {string|undefined} placeholder a string representing an html element\n * @param {string} placeholderClasses a string representing the classes that should be added to the placeholder\n */\nexport default (sortableElement: HTMLElement, placeholder?: HTMLElement, placeholderClass: string = 'sortable-placeholder') => {\n if (!(sortableElement instanceof HTMLElement)) {\n throw new Error('You must provide a valid element as a sortable.')\n }\n // if placeholder is not an element\n if (!(placeholder instanceof HTMLElement) && placeholder !== undefined) {\n throw new Error('You must provide a valid element as a placeholder or set ot to undefined.')\n }\n // if no placeholder element is given\n if (placeholder === undefined) {\n if (['UL', 'OL'].includes(sortableElement.tagName)) {\n placeholder = document.createElement('li')\n } else if (['TABLE', 'TBODY'].includes(sortableElement.tagName)) {\n placeholder = document.createElement('tr')\n // set colspan to always all rows, otherwise the item can only be dropped in first column\n placeholder.innerHTML = ''\n } else {\n placeholder = document.createElement('div')\n }\n }\n // add classes to placeholder\n if (typeof placeholderClass === 'string') {\n placeholder.classList.add(...placeholderClass.split(' '))\n }\n\n return placeholder\n}\n","/* eslint-env browser */\n/**\n * Get height of an element including padding\n * @param {HTMLElement} element an dom element\n */\nexport default (element: HTMLElement) => {\n if (!(element instanceof HTMLElement)) {\n throw new Error('You must provide a valid dom element')\n }\n // get calculated style of element\n const style = window.getComputedStyle(element)\n // get only height if element has box-sizing: border-box specified\n if (style.getPropertyValue('box-sizing') === 'border-box') {\n return parseInt(style.getPropertyValue('height'), 10)\n }\n // pick applicable properties, convert to int and reduce by adding\n return ['height', 'padding-top', 'padding-bottom']\n .map((key) => {\n const int = parseInt(style.getPropertyValue(key), 10)\n return isNaN(int) ? 0 : int\n })\n .reduce((sum, value) => sum + value)\n}\n","/* eslint-env browser */\n/**\n * Get width of an element including padding\n * @param {HTMLElement} element an dom element\n */\nexport default (element: HTMLElement) => {\n if (!(element instanceof HTMLElement)) {\n throw new Error('You must provide a valid dom element')\n }\n // get calculated style of element\n const style = window.getComputedStyle(element)\n // pick applicable properties, convert to int and reduce by adding\n return ['width', 'padding-left', 'padding-right']\n .map((key) => {\n const int = parseInt(style.getPropertyValue(key), 10)\n return isNaN(int) ? 0 : int\n })\n .reduce((sum, value) => sum + value)\n}\n","/* eslint-env browser */\n/**\n * get handle or return item\n * @param {Array} items\n * @param {string} selector\n */\n\nexport default (items: Array, selector: string): Array => {\n if (!(items instanceof Array)) {\n throw new Error('You must provide a Array of HTMLElements to be filtered.')\n }\n\n if (typeof selector !== 'string') {\n return items\n }\n\n return items\n // remove items without handle from array\n .filter((item: HTMLElement) => {\n return item.querySelector(selector) instanceof HTMLElement ||\n (item.shadowRoot && item.shadowRoot.querySelector(selector) instanceof HTMLElement)\n })\n // replace item with handle in array\n .map((item: HTMLElement) => {\n return item.querySelector(selector) || (item.shadowRoot && item.shadowRoot.querySelector(selector))\n })\n}\n","/**\n * @param {Event} event\n * @returns {HTMLElement}\n */\nexport default (event: Event): HTMLElement => {\n return (event.composedPath && event.composedPath()[0]) || event.target\n}\n","/* eslint-env browser */\nimport offset from './offset'\nimport getEventTarget from './getEventTarget'\n/**\n * defaultDragImage returns the current item as dragged image\n * @param {HTMLElement} draggedElement - the item that the user drags\n * @param {object} elementOffset - an object with the offsets top, left, right & bottom\n * @param {Event} event - the original drag event object\n * @return {object} with element, posX and posY properties\n */\nconst defaultDragImage = (draggedElement: HTMLElement, elementOffset: offsetObject, event: DragEvent): object => {\n return {\n element: draggedElement,\n posX: event.pageX - elementOffset.left,\n posY: event.pageY - elementOffset.top\n }\n}\n/**\n * attaches an element as the drag image to an event\n * @param {Event} event - the original drag event object\n * @param {HTMLElement} draggedElement - the item that the user drags\n * @param {Function} customDragImage - function to create a custom dragImage\n * @return void\n */\nexport default (event: DragEvent, draggedElement: HTMLElement, customDragImage: Function | Element): void => {\n // check if event is provided\n if (!(event instanceof Event)) {\n throw new Error('setDragImage requires a DragEvent as the first argument.')\n }\n // check if draggedElement is provided\n if (!(draggedElement instanceof HTMLElement)) {\n throw new Error('setDragImage requires the dragged element as the second argument.')\n }\n // set default function of none provided\n if (!customDragImage) {\n customDragImage = defaultDragImage\n }\n // set default function if none is provided\n if (customDragImage instanceof HTMLElement) {\n const elementOffset = offset(customDragImage)\n const dragImage = {\n element: customDragImage,\n posX: event.pageX - elementOffset.left,\n posY: event.pageY - elementOffset.top\n }\n // set the drag image on the event\n event.dataTransfer.effectAllowed = 'copyMove'\n event.dataTransfer.setData('text/plain', getEventTarget(event).id)\n event.dataTransfer.setDragImage(dragImage.element, event.offsetX, event.offsetY)\n } else if (typeof customDragImage === 'function' && event.dataTransfer.setDragImage) {\n // check if setDragImage method is available\n // get the elements offset\n const elementOffset = offset(draggedElement)\n // get the dragImage\n const dragImage = customDragImage(draggedElement, elementOffset, event)\n // check if custom function returns correct values\n if (!(dragImage.element instanceof HTMLElement) || typeof dragImage.posX !== 'number' || typeof dragImage.posY !== 'number') {\n throw new Error('The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].')\n }\n // needs to be set for HTML5 drag & drop to work\n event.dataTransfer.effectAllowed = 'copyMove'\n // Firefox requires it to use the event target's id for the data\n event.dataTransfer.setData('text/plain', getEventTarget(event).id)\n // set the drag image on the event\n event.dataTransfer.setDragImage(dragImage.element, dragImage.posX, dragImage.posY)\n }\n}\n","import store from './store'\n/**\n * Check if curList accepts items from destList\n * @param {sortable} destination the container an item is move to\n * @param {sortable} origin the container an item comes from\n */\nexport default (destination: sortable, origin: sortable) => {\n // check if valid sortable\n if (destination.isSortable === true) {\n const acceptFrom = store(destination).getConfig('acceptFrom')\n // check if acceptFrom is valid\n if (acceptFrom !== null && acceptFrom !== false && typeof acceptFrom !== 'string') {\n throw new Error('HTML5Sortable: Wrong argument, \"acceptFrom\" must be \"null\", \"false\", or a valid selector string.')\n }\n\n if (acceptFrom !== null) {\n return acceptFrom !== false && acceptFrom.split(',').filter(function (sel) {\n return sel.length > 0 && origin.matches(sel)\n }).length > 0\n }\n // drop in same list\n if (destination === origin) {\n return true\n }\n // check if lists are connected with connectWith\n if (store(destination).getConfig('connectWith') !== undefined && store(destination).getConfig('connectWith') !== null) {\n return store(destination).getConfig('connectWith') === store(origin).getConfig('connectWith')\n }\n }\n return false\n}\n","/**\n * default configurations\n */\nexport default {\n items: null,\n // deprecated\n connectWith: null,\n // deprecated\n disableIEFix: null,\n acceptFrom: null,\n copy: false,\n placeholder: null,\n placeholderClass: 'sortable-placeholder',\n draggingClass: 'sortable-dragging',\n hoverClass: false,\n dropTargetContainerClass: false,\n debounce: 0,\n throttleTime: 100,\n maxItems: 0,\n itemSerializer: undefined,\n containerSerializer: undefined,\n customDragImage: null,\n orientation: 'vertical'\n}\n","/* eslint-env browser */\nimport store from './store'\nimport filter from './filter'\nimport throttle from './throttle'\nimport { addEventListener, removeEventListener } from './eventListener'\n/**\n * enable or disable hoverClass on mouseenter/leave if container Items\n * @param {sortable} sortableContainer a valid sortableContainer\n * @param {boolean} enable enable or disable event\n */\nexport default (sortableContainer: sortable, enable: boolean) => {\n if (typeof store(sortableContainer).getConfig('hoverClass') === 'string') {\n const hoverClasses = store(sortableContainer).getConfig('hoverClass').split(' ')\n // add class on hover\n if (enable === true) {\n addEventListener(sortableContainer, 'mousemove', throttle((event) => {\n // check of no mouse button was pressed when mousemove started == no drag\n if (event.buttons === 0) {\n filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(item => {\n if (item === event.target || item.contains(event.target)) {\n item.classList.add(...hoverClasses)\n } else {\n item.classList.remove(...hoverClasses)\n }\n })\n }\n }, store(sortableContainer).getConfig('throttleTime')))\n // remove class on leave\n addEventListener(sortableContainer, 'mouseleave', () => {\n filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(item => {\n item.classList.remove(...hoverClasses)\n })\n })\n // remove events\n } else {\n removeEventListener(sortableContainer, 'mousemove')\n removeEventListener(sortableContainer, 'mouseleave')\n }\n }\n}\n","/* eslint-env browser */\n'use strict'\n\nimport { addData as data, removeData } from './data'\nimport filter from './filter'\nimport { addEventListener as on, removeEventListener as off } from './eventListener'\nimport { addAttribute as attr, removeAttribute as removeAttr } from './attribute'\nimport offset from './offset'\nimport debounce from './debounce'\nimport getIndex from './getIndex'\nimport isInDom from './isInDom'\nimport { insertBefore as before, insertAfter as after } from './insertHtmlElements'\nimport serialize from './serialize'\nimport makePlaceholder from './makePlaceholder'\nimport getElementHeight from './elementHeight'\nimport getElementWidth from './elementWidth'\nimport getHandles from './getHandles'\nimport getEventTarget from './getEventTarget'\nimport setDragImage from './setDragImage'\nimport { default as store, stores } from './store' /* eslint-disable-line */\nimport listsConnected from './isConnected'\nimport defaultConfiguration from './defaultConfiguration'\nimport enableHoverClass from './hoverClass'\n\n/*\n * variables global to the plugin\n */\nlet dragging\nlet draggingHeight\nlet draggingWidth\n\n/*\n * Keeps track of the initialy selected list, where 'dragstart' event was triggered\n * It allows us to move the data in between individual Sortable List instances\n */\n\n// Origin List - data from before any item was changed\nlet originContainer\nlet originIndex\nlet originElementIndex\nlet originItemsBeforeUpdate\n\n// Previous Sortable Container - we dispatch as sortenter event when a\n// dragged item enters a sortableContainer for the first time\nlet previousContainer\n\n// Destination List - data from before any item was changed\nlet destinationItemsBeforeUpdate\n\n/**\n * remove event handlers from items\n * @param {Array|NodeList} items\n */\nconst removeItemEvents = function (items) {\n off(items, 'dragstart')\n off(items, 'dragend')\n off(items, 'dragover')\n off(items, 'dragenter')\n off(items, 'drop')\n off(items, 'mouseenter')\n off(items, 'mouseleave')\n}\n\n/**\n *\n * remove Store map values\n * @param {Array|NodeList} items\n */\nconst removeStoreData = function (items) {\n if (items instanceof Array) {\n items.forEach(element => stores.delete(element))\n }\n}\n\n// Remove container events\nconst removeContainerEvents = function (originContainer, previousContainer) {\n if (originContainer) {\n off(originContainer, 'dragleave')\n }\n if (previousContainer && (previousContainer !== originContainer)) {\n off(previousContainer, 'dragleave')\n }\n}\n\n/**\n * getDragging returns the current element to drag or\n * a copy of the element.\n * Is Copy Active for sortable\n * @param {HTMLElement} draggedItem - the item that the user drags\n * @param {HTMLElement} sortable a single sortable\n */\nconst getDragging = function (draggedItem, sortable) {\n let ditem = draggedItem\n if (store(sortable).getConfig('copy') === true) {\n ditem = draggedItem.cloneNode(true)\n attr(ditem, 'aria-copied', 'true')\n draggedItem.parentElement.appendChild(ditem)\n ditem.style.display = 'none'\n ditem.oldDisplay = draggedItem.style.display\n }\n return ditem\n}\n/**\n * Remove data from sortable\n * @param {HTMLElement} sortable a single sortable\n */\nconst removeSortableData = function (sortable) {\n removeData(sortable)\n removeAttr(sortable, 'aria-dropeffect')\n}\n/**\n * Remove data from items\n * @param {Array|HTMLElement} items\n */\nconst removeItemData = function (items) {\n removeAttr(items, 'aria-grabbed')\n removeAttr(items, 'aria-copied')\n removeAttr(items, 'draggable')\n removeAttr(items, 'role')\n}\n/**\n * find sortable from element. travels up parent element until found or null.\n * @param {HTMLElement} element a single sortable\n * @param {Event} event - the current event. We need to pass it to be able to\n * find Sortable whith shadowRoot (document fragment has no parent)\n */\nfunction findSortable (element, event) {\n if (event.composedPath) {\n return event.composedPath().find(el => el.isSortable)\n }\n while (element.isSortable !== true) {\n element = element.parentElement\n }\n return element\n}\n/**\n * Dragging event is on the sortable element. finds the top child that\n * contains the element.\n * @param {HTMLElement} sortableElement a single sortable\n * @param {HTMLElement} element is that being dragged\n */\nfunction findDragElement (sortableElement, element) {\n const options = data(sortableElement, 'opts')\n const items = filter(sortableElement.children, options.items)\n const itemlist = items.filter(function (ele) {\n return ele.contains(element) || (ele.shadowRoot && ele.shadowRoot.contains(element))\n })\n\n return itemlist.length > 0 ? itemlist[0] : element\n}\n/**\n * Destroy the sortable\n * @param {HTMLElement} sortableElement a single sortable\n */\nconst destroySortable = function (sortableElement) {\n const opts = data(sortableElement, 'opts') || {}\n const items = filter(sortableElement.children, opts.items)\n const handles = getHandles(items, opts.handle)\n // disable adding hover class\n enableHoverClass(sortableElement, false)\n // remove event handlers & data from sortable\n off(sortableElement, 'dragover')\n off(sortableElement, 'dragenter')\n off(sortableElement, 'dragstart')\n off(sortableElement, 'dragend')\n off(sortableElement, 'drop')\n // remove event data from sortable\n removeSortableData(sortableElement)\n // remove event handlers & data from items\n off(handles, 'mousedown')\n removeItemEvents(items)\n removeItemData(items)\n removeStoreData(items)\n removeStoreData([sortableElement])\n removeContainerEvents(originContainer, previousContainer)\n // clear sortable flag\n sortableElement.isSortable = false\n}\n/**\n * Enable the sortable\n * @param {HTMLElement} sortableElement a single sortable\n */\nconst enableSortable = function (sortableElement) {\n const opts = data(sortableElement, 'opts')\n const items = filter(sortableElement.children, opts.items)\n const handles = getHandles(items, opts.handle)\n attr(sortableElement, 'aria-dropeffect', 'move')\n data(sortableElement, '_disabled', 'false')\n attr(handles, 'draggable', 'true')\n // enable hover class\n enableHoverClass(sortableElement, true)\n // @todo: remove this fix\n // IE FIX for ghost\n // can be disabled as it has the side effect that other events\n // (e.g. click) will be ignored\n if (opts.disableIEFix === false) {\n const spanEl = (document || window.document).createElement('span')\n if (typeof spanEl.dragDrop === 'function') {\n on(handles, 'mousedown', function () {\n if (items.indexOf(this) !== -1) {\n this.dragDrop()\n } else {\n let parent = this.parentElement\n while (items.indexOf(parent) === -1) {\n parent = parent.parentElement\n }\n parent.dragDrop()\n }\n })\n }\n }\n}\n/**\n * Disable the sortable\n * @param {HTMLElement} sortableElement a single sortable\n */\nconst disableSortable = function (sortableElement) {\n const opts = data(sortableElement, 'opts')\n const items = filter(sortableElement.children, opts.items)\n const handles = getHandles(items, opts.handle)\n attr(sortableElement, 'aria-dropeffect', 'none')\n data(sortableElement, '_disabled', 'true')\n attr(handles, 'draggable', 'false')\n off(handles, 'mousedown')\n enableHoverClass(sortableElement, false)\n}\n/**\n * Reload the sortable\n * @param {HTMLElement} sortableElement a single sortable\n * @description events need to be removed to not be double bound\n */\nconst reloadSortable = function (sortableElement) {\n const opts = data(sortableElement, 'opts')\n const items = filter(sortableElement.children, opts.items)\n const handles = getHandles(items, opts.handle)\n data(sortableElement, '_disabled', 'false')\n // remove event handlers from items\n removeItemEvents(items)\n removeContainerEvents(originContainer, previousContainer)\n off(handles, 'mousedown')\n // remove event handlers from sortable\n off(sortableElement, 'dragover')\n off(sortableElement, 'dragenter')\n off(sortableElement, 'drop')\n}\n\n/**\n * Public sortable object\n * @param {Array|NodeList} sortableElements\n * @param {object|string} options|method\n */\nexport default function sortable (sortableElements, options: configuration|object|string|undefined): sortable {\n // get method string to see if a method is called\n const method = String(options)\n options = options || {}\n // check if the user provided a selector instead of an element\n if (typeof sortableElements === 'string') {\n sortableElements = document.querySelectorAll(sortableElements)\n }\n // if the user provided an element, return it in an array to keep the return value consistant\n if (sortableElements instanceof HTMLElement) {\n sortableElements = [sortableElements]\n }\n\n sortableElements = Array.prototype.slice.call(sortableElements)\n\n if (/serialize/.test(method)) {\n return sortableElements.map((sortableContainer) => {\n const opts = data(sortableContainer, 'opts')\n return serialize(sortableContainer, opts.itemSerializer, opts.containerSerializer)\n })\n }\n\n sortableElements.forEach(function (sortableElement) {\n if (/enable|disable|destroy/.test(method)) {\n return sortable[method](sortableElement)\n }\n // log deprecation\n ['connectWith', 'disableIEFix'].forEach((configKey) => {\n if (Object.prototype.hasOwnProperty.call(options, configKey) && options[configKey] !== null) {\n console.warn(`HTML5Sortable: You are using the deprecated configuration \"${configKey}\". This will be removed in an upcoming version, make sure to migrate to the new options when updating.`)\n }\n })\n // merge options with default options\n options = Object.assign({}, defaultConfiguration, store(sortableElement).config, options)\n // init data store for sortable\n store(sortableElement).config = options\n // set options on sortable\n data(sortableElement, 'opts', options)\n // property to define as sortable\n sortableElement.isSortable = true\n // reset sortable\n reloadSortable(sortableElement)\n // initialize\n const listItems = filter(sortableElement.children, options.items)\n // create element if user defined a placeholder element as a string\n let customPlaceholder\n if (options.placeholder !== null && options.placeholder !== undefined) {\n const tempContainer = document.createElement(sortableElement.tagName)\n if (options.placeholder instanceof HTMLElement) {\n tempContainer.appendChild(options.placeholder)\n } else {\n tempContainer.innerHTML = options.placeholder\n }\n customPlaceholder = tempContainer.children[0]\n }\n // add placeholder\n store(sortableElement).placeholder = makePlaceholder(sortableElement, customPlaceholder, options.placeholderClass)\n\n data(sortableElement, 'items', options.items)\n\n if (options.acceptFrom) {\n data(sortableElement, 'acceptFrom', options.acceptFrom)\n } else if (options.connectWith) {\n data(sortableElement, 'connectWith', options.connectWith)\n }\n\n enableSortable(sortableElement)\n attr(listItems, 'role', 'option')\n attr(listItems, 'aria-grabbed', 'false')\n /*\n Handle drag events on draggable items\n Handle is set at the sortableElement level as it will bubble up\n from the item\n */\n on(sortableElement, 'dragstart', function (e) {\n // ignore dragstart events\n const target = getEventTarget(e)\n if (target.isSortable === true) {\n return\n }\n e.stopImmediatePropagation()\n\n if ((options.handle && !target.matches(options.handle)) || target.getAttribute('draggable') === 'false') {\n return\n }\n\n const sortableContainer = findSortable(target, e)\n const dragItem = findDragElement(sortableContainer, target)\n\n // grab values\n originItemsBeforeUpdate = filter(sortableContainer.children, options.items)\n originIndex = originItemsBeforeUpdate.indexOf(dragItem)\n originElementIndex = getIndex(dragItem, sortableContainer.children)\n originContainer = sortableContainer\n\n // add transparent clone or other ghost to cursor\n let dragImage: Function | Element = null\n if (typeof options.customDragImage === 'string') {\n dragImage = document.querySelector(options.customDragImage)\n dragImage ?? console.error('The NodeList provided does not contain any valid elements.')\n } else if (options.customDragImage === 'function') {\n dragImage = options.customDragImage as Function\n }\n setDragImage(e, dragItem, dragImage)\n // cache selsection & add attr for dragging\n draggingHeight = getElementHeight(dragItem)\n draggingWidth = getElementWidth(dragItem)\n dragItem.classList.add(options.draggingClass)\n dragging = getDragging(dragItem, sortableContainer)\n attr(dragging, 'aria-grabbed', 'true')\n\n // dispatch sortstart event on each element in group\n sortableContainer.dispatchEvent(new CustomEvent('sortstart', {\n detail: {\n origin: {\n elementIndex: originElementIndex,\n index: originIndex,\n container: originContainer\n },\n item: dragging,\n originalTarget: target\n }\n }))\n })\n\n /*\n We are capturing targetSortable before modifications with 'dragenter' event\n */\n on(sortableElement, 'dragenter', (e) => {\n const target = getEventTarget(e)\n const sortableContainer = findSortable(target, e)\n\n if (sortableContainer && sortableContainer !== previousContainer) {\n destinationItemsBeforeUpdate = filter(sortableContainer.children, data(sortableContainer, 'items'))\n .filter(item => item !== store(sortableElement).placeholder)\n\n if (options.dropTargetContainerClass) {\n sortableContainer.classList.add(options.dropTargetContainerClass)\n }\n sortableContainer.dispatchEvent(new CustomEvent('sortenter', {\n detail: {\n origin: {\n elementIndex: originElementIndex,\n index: originIndex,\n container: originContainer\n },\n destination: {\n container: sortableContainer,\n itemsBeforeUpdate: destinationItemsBeforeUpdate\n },\n item: dragging,\n originalTarget: target\n }\n }))\n\n on(sortableContainer, 'dragleave', e => {\n // TODO: rename outTarget to be more self-explanatory\n // e.fromElement for very old browsers, similar to relatedTarget\n const outTarget = e.relatedTarget || e.fromElement\n if (!e.currentTarget.contains(outTarget)) {\n if (options.dropTargetContainerClass) {\n sortableContainer.classList.remove(options.dropTargetContainerClass)\n }\n sortableContainer.dispatchEvent(new CustomEvent('sortleave', {\n detail: {\n origin: {\n elementIndex: originElementIndex,\n index: originIndex,\n container: sortableContainer\n },\n item: dragging,\n originalTarget: target\n }\n }))\n }\n })\n }\n previousContainer = sortableContainer\n })\n\n /*\n * Dragend Event - https://developer.mozilla.org/en-US/docs/Web/Events/dragend\n * Fires each time dragEvent end, or ESC pressed\n * We are using it to clean up any draggable elements and placeholders\n */\n on(sortableElement, 'dragend', function (e) {\n if (!dragging) {\n return\n }\n\n dragging.classList.remove(options.draggingClass)\n attr(dragging, 'aria-grabbed', 'false')\n\n if (dragging.getAttribute('aria-copied') === 'true' && data(dragging, 'dropped') !== 'true') {\n dragging.remove()\n }\n if (dragging.oldDisplay !== undefined) {\n dragging.style.display = dragging.oldDisplay\n delete dragging.oldDisplay\n }\n const visiblePlaceholder = Array.from(stores.values()).map(data => data.placeholder)\n .filter(placeholder => placeholder instanceof HTMLElement)\n .filter(isInDom)[0]\n\n if (visiblePlaceholder) {\n visiblePlaceholder.remove()\n }\n\n // dispatch sortstart event on each element in group\n sortableElement.dispatchEvent(new CustomEvent('sortstop', {\n detail: {\n origin: {\n elementIndex: originElementIndex,\n index: originIndex,\n container: originContainer\n },\n item: dragging\n }\n }))\n\n previousContainer = null\n dragging = null\n draggingHeight = null\n draggingWidth = null\n })\n\n /*\n * Drop Event - https://developer.mozilla.org/en-US/docs/Web/Events/drop\n * Fires when valid drop target area is hit\n */\n on(sortableElement, 'drop', function (e) {\n if (!listsConnected(sortableElement, dragging.parentElement)) {\n return\n }\n e.preventDefault()\n e.stopPropagation()\n\n data(dragging, 'dropped', 'true')\n // get the one placeholder that is currently visible\n const visiblePlaceholder = Array.from(stores.values()).map((data) => {\n return data.placeholder\n })\n // filter only HTMLElements\n .filter(placeholder => placeholder instanceof HTMLElement)\n // only elements in DOM\n .filter(isInDom)[0]\n if (visiblePlaceholder) {\n visiblePlaceholder.replaceWith(dragging)\n // to avoid flickering restoring element display immediately after replacing placeholder\n if (dragging.oldDisplay !== undefined) {\n dragging.style.display = dragging.oldDisplay\n delete dragging.oldDisplay\n }\n } else {\n // set the dropped value to 'false' to delete copied dragging at the time of 'dragend'\n data(dragging, 'dropped', 'false')\n return\n }\n /*\n * Fires Custom Event - 'sortstop'\n */\n sortableElement.dispatchEvent(new CustomEvent('sortstop', {\n detail: {\n origin: {\n elementIndex: originElementIndex,\n index: originIndex,\n container: originContainer\n },\n item: dragging\n }\n }))\n\n const placeholder = store(sortableElement).placeholder\n const originItems = filter(originContainer.children, options.items)\n .filter(item => item !== placeholder)\n const destinationContainer = this.isSortable === true ? this : this.parentElement\n const destinationItems = filter(destinationContainer.children, data(destinationContainer, 'items'))\n .filter(item => item !== placeholder)\n const destinationElementIndex = getIndex(dragging, Array.from(dragging.parentElement.children)\n .filter(item => item !== placeholder))\n const destinationIndex = getIndex(dragging, destinationItems)\n\n if (options.dropTargetContainerClass) {\n destinationContainer.classList.remove(options.dropTargetContainerClass)\n }\n\n /*\n * When a list item changed container lists or index within a list\n * Fires Custom Event - 'sortupdate'\n */\n if (originElementIndex !== destinationElementIndex || originContainer !== destinationContainer) {\n sortableElement.dispatchEvent(new CustomEvent('sortupdate', {\n detail: {\n origin: {\n elementIndex: originElementIndex,\n index: originIndex,\n container: originContainer,\n itemsBeforeUpdate: originItemsBeforeUpdate,\n items: originItems\n },\n destination: {\n index: destinationIndex,\n elementIndex: destinationElementIndex,\n container: destinationContainer,\n itemsBeforeUpdate: destinationItemsBeforeUpdate,\n items: destinationItems\n },\n item: dragging\n }\n }))\n }\n })\n\n const debouncedDragOverEnter = debounce((sortableElement, element, pageX, pageY) => {\n if (!dragging) {\n return\n }\n\n // set placeholder height if forcePlaceholderSize option is set\n if (options.forcePlaceholderSize) {\n store(sortableElement).placeholder.style.height = draggingHeight + 'px'\n store(sortableElement).placeholder.style.width = draggingWidth + 'px'\n }\n // if element the draggedItem is dragged onto is within the array of all elements in list\n // (not only items, but also disabled, etc.)\n if (Array.from(sortableElement.children).indexOf(element) > -1) {\n const thisHeight = getElementHeight(element)\n const thisWidth = getElementWidth(element)\n const placeholderIndex = getIndex(store(sortableElement).placeholder, element.parentElement.children)\n const thisIndex = getIndex(element, element.parentElement.children)\n // Check if `element` is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering\n if (thisHeight > draggingHeight || thisWidth > draggingWidth) {\n // Dead zone?\n const deadZoneVertical = thisHeight - draggingHeight\n const deadZoneHorizontal = thisWidth - draggingWidth\n const offsetTop = offset(element).top\n const offsetLeft = offset(element).left\n if (placeholderIndex < thisIndex &&\n ((options.orientation === 'vertical' && pageY < offsetTop) ||\n (options.orientation === 'horizontal' && pageX < offsetLeft))) {\n return\n }\n if (placeholderIndex > thisIndex &&\n ((options.orientation === 'vertical' && pageY > offsetTop + thisHeight - deadZoneVertical) ||\n (options.orientation === 'horizontal' && pageX > offsetLeft + thisWidth - deadZoneHorizontal))) {\n return\n }\n }\n\n if (dragging.oldDisplay === undefined) {\n dragging.oldDisplay = dragging.style.display\n }\n\n if (dragging.style.display !== 'none') {\n dragging.style.display = 'none'\n }\n // To avoid flicker, determine where to position the placeholder\n // based on where the mouse pointer is relative to the elements\n // vertical center.\n let placeAfter = false\n try {\n const elementMiddleVertical = offset(element).top + element.offsetHeight / 2\n const elementMiddleHorizontal = offset(element).left + element.offsetWidth / 2\n placeAfter = (options.orientation === 'vertical' && (pageY >= elementMiddleVertical)) ||\n (options.orientation === 'horizontal' && (pageX >= elementMiddleHorizontal))\n } catch (e) {\n placeAfter = placeholderIndex < thisIndex\n }\n\n if (placeAfter) {\n after(element, store(sortableElement).placeholder)\n } else {\n before(element, store(sortableElement).placeholder)\n }\n // get placeholders from all stores & remove all but current one\n Array.from(stores.values())\n // remove empty values\n .filter(data => data.placeholder !== undefined)\n // foreach placeholder in array if outside of current sorableContainer -> remove from DOM\n .forEach((data) => {\n if (data.placeholder !== store(sortableElement).placeholder) {\n data.placeholder.remove()\n }\n })\n } else {\n // get all placeholders from store\n const placeholders = Array.from(stores.values())\n .filter((data) => data.placeholder !== undefined)\n .map((data) => {\n return data.placeholder\n })\n // check if element is not in placeholders\n if (placeholders.indexOf(element) === -1 && sortableElement === element && !filter(element.children, options.items).length) {\n placeholders.forEach((element) => element.remove())\n element.appendChild(store(sortableElement).placeholder)\n }\n }\n }, options.debounce)\n // Handle dragover and dragenter events on draggable items\n const onDragOverEnter = function (e) {\n let element = e.target\n const sortableElement = element.isSortable === true ? element : findSortable(element, e)\n element = findDragElement(sortableElement, element)\n if (!dragging || !listsConnected(sortableElement, dragging.parentElement) || data(sortableElement, '_disabled') === 'true') {\n return\n }\n const options = data(sortableElement, 'opts')\n if (parseInt(options.maxItems) && filter(sortableElement.children, data(sortableElement, 'items')).length >= parseInt(options.maxItems) && dragging.parentElement !== sortableElement) {\n return\n }\n e.preventDefault()\n e.stopPropagation()\n e.dataTransfer.dropEffect = store(sortableElement).getConfig('copy') === true ? 'copy' : 'move'\n debouncedDragOverEnter(sortableElement, element, e.pageX, e.pageY)\n }\n\n on(listItems.concat(sortableElement), 'dragover', onDragOverEnter)\n on(listItems.concat(sortableElement), 'dragenter', onDragOverEnter)\n })\n\n return sortableElements\n}\n\nsortable.destroy = function (sortableElement) {\n destroySortable(sortableElement)\n}\n\nsortable.enable = function (sortableElement) {\n enableSortable(sortableElement)\n}\n\nsortable.disable = function (sortableElement) {\n disableSortable(sortableElement)\n}\n\n/* START.TESTS_ONLY */\nsortable.__testing = {\n // add internal methods here for testing purposes\n data,\n removeItemEvents,\n removeItemData,\n removeSortableData,\n removeContainerEvents\n}\n/* END.TESTS_ONLY */\n","/**\n * make sure a function is only called once within the given amount of time\n * @param {Function} fn the function to throttle\n * @param {number} threshold time limit for throttling\n */\n// must use function to keep this context\nexport default function (fn: Function, threshold: number = 250) {\n // check function\n if (typeof fn !== 'function') {\n throw new Error('You must provide a function as the first argument for throttle.')\n }\n // check threshold\n if (typeof threshold !== 'number') {\n throw new Error('You must provide a number as the second argument for throttle.')\n }\n\n let lastEventTimestamp = null\n\n return (...args) => {\n const now = Date.now()\n if (lastEventTimestamp === null || now - lastEventTimestamp >= threshold) {\n lastEventTimestamp = now\n fn.apply(this, args)\n }\n }\n}\n"],"names":["addData","element","key","value","undefined","h5s","data","filter","nodes","selector","NodeList","HTMLCollection","Array","Error","from","item","nodeType","matches","stores","Map","Store","this","_config","_placeholder","_data","Object","defineProperty","prototype","get","config","forEach","set","mergedConfig","assign","entries","setConfig","has","getConfig","placeholder","HTMLElement","setData","getData","deleteData","delete","store","sortableElement","addEventListener","eventName","callback","i","length","concat","removeEventListener","addAttribute","attribute","setAttribute","removeAttribute","offset","parentElement","getClientRects","rect","left","window","pageXOffset","right","top","pageYOffset","bottom","debounce","func","wait","timeout","args","_i","arguments","clearTimeout","setTimeout","apply","getIndex","elementList","indexOf","isInDom","parentNode","insertNode","referenceNode","newElement","position","insertBefore","nextElementSibling","target","insertAfter","serialize","sortableContainer","customItemSerializer","customContainerSerializer","serializedItem","serializedContainer","isSortable","items","children","serializedItems","map","parent","node","html","outerHTML","index","container","itemCount","makePlaceholder","placeholderClass","includes","tagName","document","createElement","innerHTML","_a","classList","add","split","getElementHeight","style","getComputedStyle","getPropertyValue","parseInt","int","isNaN","reduce","sum","getElementWidth","getHandles","querySelector","shadowRoot","getEventTarget","event","composedPath","defaultDragImage","draggedElement","elementOffset","posX","pageX","posY","pageY","setDragImage","customDragImage","Event","dragImage","dataTransfer","effectAllowed","id","offsetX","offsetY","listsConnected","destination","origin","acceptFrom","sel","defaultConfiguration","connectWith","disableIEFix","copy","draggingClass","hoverClass","dropTargetContainerClass","throttleTime","maxItems","itemSerializer","containerSerializer","orientation","dragging","draggingHeight","draggingWidth","originContainer","originIndex","originElementIndex","originItemsBeforeUpdate","previousContainer","destinationItemsBeforeUpdate","enableHoverClass","enable","hoverClasses_1","fn","threshold","_this","lastEventTimestamp","now","Date","throttle","buttons","contains","_b","remove","removeItemEvents","off","removeStoreData","removeContainerEvents","getDragging","draggedItem","sortable","ditem","attr","cloneNode","appendChild","display","oldDisplay","removeSortableData","removeAttr","removeItemData","findSortable","find","el","findDragElement","options","itemlist","ele","enableSortable","opts","handles","handle","dragDrop","on","reloadSortable","sortableElements","method","String","querySelectorAll","slice","call","test","configKey","hasOwnProperty","console","warn","customPlaceholder","listItems","tempContainer","e","stopImmediatePropagation","getAttribute","dragItem","error","dispatchEvent","CustomEvent","detail","elementIndex","originalTarget","itemsBeforeUpdate","outTarget","relatedTarget","fromElement","currentTarget","visiblePlaceholder","values","preventDefault","stopPropagation","replaceWith","originItems","destinationContainer","destinationItems","destinationElementIndex","destinationIndex","debouncedDragOverEnter","forcePlaceholderSize","height","width","thisHeight","thisWidth","placeholderIndex","thisIndex","deadZoneVertical","deadZoneHorizontal","offsetTop","offsetLeft","placeAfter","elementMiddleVertical","offsetHeight","elementMiddleHorizontal","offsetWidth","after","before","placeholders","onDragOverEnter","dropEffect","destroy","disable","__testing"],"mappings":"qCAQA,SAASA,EAASC,EAAsBC,EAAaC,GACnD,QAAcC,IAAVD,EACF,OAAOF,GAAWA,EAAQI,KAAOJ,EAAQI,IAAIC,MAAQL,EAAQI,IAAIC,KAAKJ,GAEtED,EAAQI,IAAMJ,EAAQI,KAAO,GAC7BJ,EAAQI,IAAIC,KAAOL,EAAQI,IAAIC,MAAQ,GACvCL,EAAQI,IAAIC,KAAKJ,GAAOC,ECP5B,IAAAI,EAAe,SAACC,EAAmDC,GACjE,KAAMD,aAAiBE,UAAYF,aAAiBG,gBAAkBH,aAAiBI,OACrF,MAAM,IAAIC,MAAM,gFAElB,MAAwB,iBAAbJ,EACFG,MAAME,KAAKN,GAGbI,MAAME,KAAKN,GAAOD,OAAO,SAACQ,GAAS,OAAkB,IAAlBA,EAAKC,UAAkBD,EAAKE,QAAQR,MCZnES,EAAkC,IAAIC,IAMnDC,EAAA,WAAA,SAAAA,IACUC,KAAAC,QAA4B,IAAIH,IAChCE,KAAAE,kBAA6BnB,EAC7BiB,KAAAG,MAA0B,IAAIL,IAwHxC,OAlHEM,OAAAC,eAAIN,EAAMO,UAAA,SAAA,CAeVC,IAAA,WAEE,IAAMC,EAAS,GAKf,OAJAR,KAAKC,QAAQQ,QAAQ,SAAC3B,EAAOD,GAC3B2B,EAAO3B,GAAOC,IAGT0B,GAtBTE,IAAA,SAAYF,GACV,GAAsB,iBAAXA,EACT,MAAM,IAAIhB,MAAM,uEAGlB,IAAMmB,EAAeP,OAAOQ,OAAO,GAAIJ,GAEvCR,KAAKC,QAAU,IAAIH,IAAIM,OAAOS,QAAQF,qCAyBxCZ,EAAAO,UAAAQ,UAAA,SAAWjC,EAAaC,GACtB,IAAKkB,KAAKC,QAAQc,IAAIlC,GACpB,MAAM,IAAIW,MAAM,oDAA6CX,IAG/DmB,KAAKC,QAAQS,IAAI7B,EAAKC,IASxBiB,EAASO,UAAAU,UAAT,SAAWnC,GACT,IAAKmB,KAAKC,QAAQc,IAAIlC,GACpB,MAAM,IAAIW,MAAM,gDAAyCX,IAE3D,OAAOmB,KAAKC,QAAQM,IAAI1B,IAQ1BuB,OAAAC,eAAIN,EAAWO,UAAA,cAAA,CAAfC,IAAA,WACE,OAAOP,KAAKE,cASdQ,IAAA,SAAiBO,GACf,KAAMA,aAAuBC,cAAgC,OAAhBD,EAC3C,MAAM,IAAIzB,MAAM,kDAElBQ,KAAKE,aAAee,mCAUtBlB,EAAAO,UAAAa,QAAA,SAAStC,EAAaC,GACpB,GAAmB,iBAARD,EACT,MAAM,IAAIW,MAAM,6BAElBQ,KAAKG,MAAMO,IAAI7B,EAAKC,IAStBiB,EAAOO,UAAAc,QAAP,SAASvC,GACP,GAAmB,iBAARA,EACT,MAAM,IAAIW,MAAM,6BAElB,OAAOQ,KAAKG,MAAMI,IAAI1B,IASxBkB,EAAUO,UAAAe,WAAV,SAAYxC,GACV,GAAmB,iBAARA,EACT,MAAM,IAAIW,MAAM,6BAElB,OAAOQ,KAAKG,MAAMmB,OAAOzC,IAE5BkB,KAKDwB,EAAA,SAAgBC,GAEd,KAAMA,aAA2BN,aAC/B,MAAM,IAAI1B,MAAM,oDAOlB,OAJKK,EAAOkB,IAAIS,IACd3B,EAAOa,IAAIc,EAAiB,IAAIzB,GAG3BF,EAAOU,IAAIiB,IC7IpB,SAASC,EAAkB7C,EAAyC8C,EAAkBC,GACpF,GAAI/C,aAAmBW,MACrB,IAAK,IAAIqC,EAAI,EAAGA,EAAIhD,EAAQiD,SAAUD,EACpCH,EAAiB7C,EAAQgD,GAAIF,EAAWC,QAI5C/C,EAAQ6C,iBAAiBC,EAAWC,GACpCJ,EAAM3C,GAASuC,QAAQ,QAAQW,OAAAJ,GAAaC,GAM9C,SAASI,EAAqBnD,EAAyC8C,GACrE,GAAI9C,aAAmBW,MACrB,IAAK,IAAIqC,EAAI,EAAGA,EAAIhD,EAAQiD,SAAUD,EACpCG,EAAoBnD,EAAQgD,GAAIF,QAIpC9C,EAAQmD,oBAAoBL,EAAWH,EAAM3C,GAASwC,QAAQ,QAAAU,OAAQJ,KACtEH,EAAM3C,GAASyC,WAAW,QAAQS,OAAAJ,ICvBpC,SAASM,EAAcpD,EAAyCqD,EAAkBnD,GAChF,GAAIF,aAAmBW,MACrB,IAAK,IAAIqC,EAAI,EAAGA,EAAIhD,EAAQiD,SAAUD,EACpCI,EAAapD,EAAQgD,GAAIK,EAAWnD,QAIxCF,EAAQsD,aAAaD,EAAWnD,GAMlC,SAASqD,EAAiBvD,EAAyCqD,GACjE,GAAIrD,aAAmBW,MACrB,IAAK,IAAIqC,EAAI,EAAGA,EAAIhD,EAAQiD,SAAUD,EACpCO,EAAgBvD,EAAQgD,GAAIK,QAIhCrD,EAAQuD,gBAAgBF,GCrB1B,IAAAG,EAAA,SAAgBxD,GACd,IAAKA,EAAQyD,eAAqD,IAApCzD,EAAQ0D,iBAAiBT,OACrD,MAAM,IAAIrC,MAAM,0CAGlB,IAAM+C,EAAO3D,EAAQ0D,iBAAiB,GACtC,MAAO,CACLE,KAAMD,EAAKC,KAAOC,OAAOC,YACzBC,MAAOJ,EAAKI,MAAQF,OAAOC,YAC3BE,IAAKL,EAAKK,IAAMH,OAAOI,YACvBC,OAAQP,EAAKO,OAASL,OAAOI,cCRjCE,EAAe,SAACC,EAAgBC,GAC9B,IAAIC,EACJ,YAF8B,IAAAD,IAAAA,EAAgB,GAEvC,eAAC,IAAOE,EAAA,GAAAC,EAAA,EAAPA,EAAOC,UAAAxB,OAAPuB,IAAAD,EAAOC,GAAAC,UAAAD,GACbE,aAAaJ,GACbA,EAAUK,WAAW,WACnBP,EAAIQ,WAAA,EAAIL,IACPF,KCNPQ,EAAe,SAAC7E,EAAsB8E,GACpC,KAAM9E,aAAmBsC,cAAkBwC,aAAuBrE,UAAYqE,aAAuBpE,gBAAkBoE,aAAuBnE,QAC5I,MAAM,IAAIC,MAAM,uDAGlB,OAAOD,MAAME,KAAKiE,GAAaC,QAAQ/E,ICLzCgF,EAAA,SAAgBhF,GACd,KAAMA,aAAmBsC,aACvB,MAAM,IAAI1B,MAAM,kCAGlB,OAA8B,OAAvBZ,EAAQiF,YCJXC,EAAa,SAACC,EAA4BC,EAAyBC,GACvE,KAAMF,aAAyB7C,aAAkB6C,EAAc1B,yBAAyBnB,aACtF,MAAM,IAAI1B,MAAM,qCAElBuE,EAAc1B,cAAc6B,aAC1BF,EACc,WAAbC,EAAwBF,EAAgBA,EAAcI,qBAQrDD,EAAe,SAACE,EAAqBxF,GAAyB,OAAAkF,EAAWM,EAAQxF,EAAS,WAM1FyF,EAAc,SAACD,EAAqBxF,GAAyB,OAAAkF,EAAWM,EAAQxF,EAAS,UCjB/F0F,EAAA,SAAgBC,EAAgCC,EAAqHC,GAEnK,QAF8C,IAAAD,IAAAA,WAAkCE,EAAgCH,GAAmC,OAAAG,SAAgB,IAAAD,IAAAA,EAAuC,SAAAE,GAAgC,OAAAA,MAEpOJ,aAA6BrD,eAAkD,IAAjCqD,EAAkBK,WACpE,MAAM,IAAIpF,MAAM,6DAGlB,GAAoC,mBAAzBgF,GAA4E,mBAA9BC,EACvD,MAAM,IAAIjF,MAAM,uEAGlB,IAEME,EAFUf,EAAQ4F,EAAmB,QAEJM,MAGjCA,EAAQ3F,EAAOqF,EAAkBO,SAAUpF,GAC3CqF,EAAoCF,EAAMG,IAAI,SAACtF,GACnD,MAAO,CACLuF,OAAQV,EACRW,KAAMxF,EACNyF,KAAMzF,EAAK0F,UACXC,MAAO5B,EAAS/D,EAAMmF,MAS1B,MAAO,CACLS,UAAWb,EANK,CAChBS,KAAMX,EACNgB,UAAWR,EAAgBlD,SAK3BgD,MAAOE,EAAgBC,IAAI,SAACtF,GAAiB,OAAA8E,EAAqB9E,EAAM6E,OCnC5EiB,EAAA,SAAgBhE,EAA8BP,EAA2BwE,SACvE,QADuE,IAAAA,IAAAA,EAAiD,0BAClHjE,aAA2BN,aAC/B,MAAM,IAAI1B,MAAM,mDAGlB,KAAMyB,aAAuBC,mBAAgCnC,IAAhBkC,EAC3C,MAAM,IAAIzB,MAAM,6EAmBlB,YAhBoBT,IAAhBkC,IACE,CAAC,KAAM,MAAMyE,SAASlE,EAAgBmE,SACxC1E,EAAc2E,SAASC,cAAc,MAC5B,CAAC,QAAS,SAASH,SAASlE,EAAgBmE,UACrD1E,EAAc2E,SAASC,cAAc,OAEzBC,UAAY,0BAExB7E,EAAc2E,SAASC,cAAc,QAIT,iBAArBJ,IACTM,EAAA9E,EAAY+E,WAAUC,IAAOzC,MAAAuC,EAAAN,EAAiBS,MAAM,MAG/CjF,GC3BTkF,EAAA,SAAgBvH,GACd,KAAMA,aAAmBsC,aACvB,MAAM,IAAI1B,MAAM,wCAGlB,IAAM4G,EAAQ3D,OAAO4D,iBAAiBzH,GAEtC,MAA6C,eAAzCwH,EAAME,iBAAiB,cAClBC,SAASH,EAAME,iBAAiB,UAAW,IAG7C,CAAC,SAAU,cAAe,kBAC9BtB,IAAI,SAACnG,GACJ,IAAM2H,EAAMD,SAASH,EAAME,iBAAiBzH,GAAM,IAClD,OAAO4H,MAAMD,GAAO,EAAIA,IAEzBE,OAAO,SAACC,EAAK7H,GAAU,OAAA6H,EAAM7H,KChBlC8H,EAAA,SAAgBhI,GACd,KAAMA,aAAmBsC,aACvB,MAAM,IAAI1B,MAAM,wCAGlB,IAAM4G,EAAQ3D,OAAO4D,iBAAiBzH,GAEtC,MAAO,CAAC,QAAS,eAAgB,iBAC9BoG,IAAI,SAACnG,GACJ,IAAM2H,EAAMD,SAASH,EAAME,iBAAiBzH,GAAM,IAClD,OAAO4H,MAAMD,GAAO,EAAIA,IAEzBE,OAAO,SAACC,EAAK7H,GAAU,OAAA6H,EAAM7H,KCVlC+H,EAAe,SAAChC,EAA2BzF,GACzC,KAAMyF,aAAiBtF,OACrB,MAAM,IAAIC,MAAM,4DAGlB,MAAwB,iBAAbJ,EACFyF,EAGFA,EAEJ3F,OAAO,SAACQ,GACP,OAAOA,EAAKoH,cAAc1H,aAAqB8B,aAC5CxB,EAAKqH,YAAcrH,EAAKqH,WAAWD,cAAc1H,aAAqB8B,cAG1E8D,IAAI,SAACtF,GACJ,OAAOA,EAAKoH,cAAc1H,IAAcM,EAAKqH,YAAcrH,EAAKqH,WAAWD,cAAc1H,MCpB/F4H,EAAA,SAAgBC,GACd,OAAQA,EAAMC,cAAgBD,EAAMC,eAAe,IAAOD,EAAM7C,QCK5D+C,EAAmB,SAACC,EAA6BC,EAA6BJ,GAClF,MAAO,CACLrI,QAASwI,EACTE,KAAML,EAAMM,MAAQF,EAAc7E,KAClCgF,KAAMP,EAAMQ,MAAQJ,EAAczE,MAUtC8E,EAAA,SAAgBT,EAAkBG,EAA6BO,GAE7D,KAAMV,aAAiBW,OACrB,MAAM,IAAIpI,MAAM,4DAGlB,KAAM4H,aAA0BlG,aAC9B,MAAM,IAAI1B,MAAM,qEAOlB,GAJKmI,IACHA,EAAkBR,GAGhBQ,aAA2BzG,YAAa,CAC1C,IAAMmG,EAAgBjF,EAAOuF,GACvBE,EAAY,CAChBjJ,QAAS+I,EACTL,KAAML,EAAMM,MAAQF,EAAc7E,KAClCgF,KAAMP,EAAMQ,MAAQJ,EAAczE,KAGpCqE,EAAMa,aAAaC,cAAgB,WACnCd,EAAMa,aAAa3G,QAAQ,aAAc6F,EAAeC,GAAOe,IAC/Df,EAAMa,aAAaJ,aAAaG,EAAUjJ,QAASqI,EAAMgB,QAAShB,EAAMiB,cACnE,GAA+B,mBAApBP,GAAkCV,EAAMa,aAAaJ,aAAc,CAOnF,MAFMG,EAAYF,EAAgBP,EAF5BC,EAAgBjF,EAAOgF,GAEoCH,IAEjDrI,mBAAmBsC,cAA0C,iBAAnB2G,EAAUP,MAA+C,iBAAnBO,EAAUL,KACxG,MAAM,IAAIhI,MAAM,uIAGlByH,EAAMa,aAAaC,cAAgB,WAEnCd,EAAMa,aAAa3G,QAAQ,aAAc6F,EAAeC,GAAOe,IAE/Df,EAAMa,aAAaJ,aAAaG,EAAUjJ,QAASiJ,EAAUP,KAAMO,EAAUL,QC1DjFW,EAAe,SAACC,EAAuBC,GAErC,IAA+B,IAA3BD,EAAYxD,WAAqB,CACnC,IAAM0D,EAAa/G,EAAM6G,GAAapH,UAAU,cAEhD,GAAmB,OAAfsH,IAAsC,IAAfA,GAA8C,iBAAfA,EACxD,MAAM,IAAI9I,MAAM,oGAGlB,GAAmB,OAAf8I,EACF,OAAsB,IAAfA,GAEK,EAFmBA,EAAWpC,MAAM,KAAKhH,OAAO,SAAUqJ,GACpE,OAAoB,EAAbA,EAAI1G,QAAcwG,EAAOzI,QAAQ2I,KACvC1G,OAGL,GAAIuG,IAAgBC,EAClB,OAAO,EAGT,QAAoDtJ,IAAhDwC,EAAM6G,GAAapH,UAAU,gBAAgF,OAAhDO,EAAM6G,GAAapH,UAAU,eAC5F,OAAOO,EAAM6G,GAAapH,UAAU,iBAAmBO,EAAM8G,GAAQrH,UAAU,eAGnF,OAAO,GC1BMwH,EAAA,CACb3D,MAAO,KAEP4D,YAAa,KAEbC,aAAc,KACdJ,WAAY,KACZK,MAAM,EACN1H,YAAa,KACbwE,iBAAkB,uBAClBmD,cAAe,oBACfC,YAAY,EACZC,0BAA0B,EAC1B/F,SAAU,EACVgG,aAAc,IACdC,SAAU,EACVC,oBAAgBlK,EAChBmK,yBAAqBnK,EACrB4I,gBAAiB,KACjBwB,YAAa,YCZf,ICiBIC,EACAC,EACAC,EAQAC,EACAC,EACAC,EACAC,EAIAC,EAGAC,EDrCJC,EAAe,SAACtF,EAA6BuF,GAC3C,GAAgE,iBAArDvI,EAAMgD,GAAmBvD,UAAU,cAA4B,CACxE,IAAM+I,EAAexI,EAAMgD,GAAmBvD,UAAU,cAAckF,MAAM,MAE7D,IAAX4D,GACFrI,EAAiB8C,EAAmB,YETjB,SAAAyF,EAAcC,GAAvC,IAmBCC,EAAAlK,KAjBC,QAFqC,IAAAiK,IAAAA,EAAuB,KAE1C,mBAAPD,EACT,MAAM,IAAIxK,MAAM,mEAGlB,GAAyB,iBAAdyK,EACT,MAAM,IAAIzK,MAAM,kEAGlB,IAAI2K,EAAqB,KAEzB,OAAO,eAAC,IAAOhH,EAAA,GAAAC,EAAA,EAAPA,EAAOC,UAAAxB,OAAPuB,IAAAD,EAAOC,GAAAC,UAAAD,GACb,IAAMgH,EAAMC,KAAKD,OACU,OAAvBD,GAA2DF,GAA5BG,EAAMD,KACvCA,EAAqBC,EACrBJ,EAAGxG,MAAM0G,EAAM/G,KFPkCmH,CAAS,SAACrD,GAEnC,IAAlBA,EAAMsD,SACRrL,EAAOqF,EAAkBO,SAAUvD,EAAMgD,GAAmBvD,UAAU,UAAUP,QAAQ,SAAAf,WAClFA,IAASuH,EAAM7C,QAAU1E,EAAK8K,SAASvD,EAAM7C,SAC/C2B,EAAArG,EAAKsG,WAAUC,IAAOzC,MAAAuC,EAAAgE,IAEtBU,EAAA/K,EAAKsG,WAAU0E,OAAUlH,MAAAiH,EAAAV,MAI9BxI,EAAMgD,GAAmBvD,UAAU,kBAEtCS,EAAiB8C,EAAmB,aAAc,WAChDrF,EAAOqF,EAAkBO,SAAUvD,EAAMgD,GAAmBvD,UAAU,UAAUP,QAAQ,SAAAf,UACtFqG,EAAArG,EAAKsG,WAAU0E,OAAUlH,MAAAuC,EAAAgE,SAK7BhI,EAAoBwC,EAAmB,aACvCxC,EAAoBwC,EAAmB,iBCiBvCoG,EAAmB,SAAU9F,GACjC+F,EAAI/F,EAAO,aACX+F,EAAI/F,EAAO,WACX+F,EAAI/F,EAAO,YACX+F,EAAI/F,EAAO,aACX+F,EAAI/F,EAAO,QACX+F,EAAI/F,EAAO,cACX+F,EAAI/F,EAAO,eAQPgG,EAAkB,SAAUhG,GAC5BA,aAAiBtF,OACnBsF,EAAMpE,QAAQ,SAAA7B,GAAW,OAAAiB,EAAOyB,OAAO1C,MAKrCkM,EAAwB,SAAUvB,EAAiBI,GACnDJ,GACFqB,EAAIrB,EAAiB,aAEnBI,GAAsBA,IAAsBJ,GAC9CqB,EAAIjB,EAAmB,cAWrBoB,EAAc,SAAUC,EAAaC,GACzC,IAAIC,EAAQF,EAQZ,OAP0C,IAAtCzJ,EAAM0J,GAAUjK,UAAU,UAE5BmK,EADAD,EAAQF,EAAYI,WAAU,GAClB,cAAe,QAC3BJ,EAAY3I,cAAcgJ,YAAYH,GACtCA,EAAM9E,MAAMkF,QAAU,OACtBJ,EAAMK,WAAaP,EAAY5E,MAAMkF,SAEhCJ,GAMHM,EAAqB,SAAUP,GpBrFrC,IAAqBrM,GAAAA,EoBsFRqM,GpBrFCjM,YACHJ,EAAQI,IAAIC,KoBqFrBwM,EAAWR,EAAU,oBAMjBS,EAAiB,SAAU7G,GAC/B4G,EAAW5G,EAAO,gBAClB4G,EAAW5G,EAAO,eAClB4G,EAAW5G,EAAO,aAClB4G,EAAW5G,EAAO,SAQpB,SAAS8G,EAAc/M,EAASqI,GAC9B,GAAIA,EAAMC,aACR,OAAOD,EAAMC,eAAe0E,KAAK,SAAAC,GAAM,OAAAA,EAAGjH,aAE5C,MAA8B,IAAvBhG,EAAQgG,YACbhG,EAAUA,EAAQyD,cAEpB,OAAOzD,EAQT,SAASkN,EAAiBtK,EAAiB5C,GACzC,IAAMmN,EAAU9M,EAAKuC,EAAiB,QAEhCwK,EADQ9M,EAAOsC,EAAgBsD,SAAUiH,EAAQlH,OAChC3F,OAAO,SAAU+M,GACtC,OAAOA,EAAIzB,SAAS5L,IAAaqN,EAAIlF,YAAckF,EAAIlF,WAAWyD,SAAS5L,KAG7E,OAAyB,EAAlBoN,EAASnK,OAAamK,EAAS,GAAKpN,EAM7C,IA4BMsN,EAAiB,SAAU1K,GAC/B,IAAM2K,EAAOlN,EAAKuC,EAAiB,QAC7BqD,EAAQ3F,EAAOsC,EAAgBsD,SAAUqH,EAAKtH,OAC9CuH,EAAUvF,EAAWhC,EAAOsH,EAAKE,SACvClB,EAAK3J,EAAiB,kBAAmB,QACzCvC,EAAKuC,EAAiB,YAAa,SACnC2J,EAAKiB,EAAS,YAAa,QAE3BvC,EAAiBrI,GAAiB,IAKR,IAAtB2K,EAAKzD,gBAEwB,mBADf9C,UAAYnD,OAAOmD,UAAUC,cAAc,QACzCyG,UAChBC,EAAGH,EAAS,YAAa,WACvB,IAA6B,IAAzBvH,EAAMlB,QAAQ3D,MAChBA,KAAKsM,eACA,CAEL,IADA,IAAIrH,EAASjF,KAAKqC,eACgB,IAA3BwC,EAAMlB,QAAQsB,IACnBA,EAASA,EAAO5C,cAElB4C,EAAOqH,gBAyBXE,EAAiB,SAAUhL,GAC/B,IAAM2K,EAAOlN,EAAKuC,EAAiB,QAC7BqD,EAAQ3F,EAAOsC,EAAgBsD,SAAUqH,EAAKtH,OAC9CuH,EAAUvF,EAAWhC,EAAOsH,EAAKE,QACvCpN,EAAKuC,EAAiB,YAAa,SAEnCmJ,EAAiB9F,GACjBiG,EAAsBvB,EAAiBI,GACvCiB,EAAIwB,EAAS,aAEbxB,EAAIpJ,EAAiB,YACrBoJ,EAAIpJ,EAAiB,aACrBoJ,EAAIpJ,EAAiB,SAQT,SAAUyJ,EAAUwB,EAAkBV,GAElD,IAAMW,EAASC,OAAOZ,GAatB,OAZAA,EAAUA,GAAW,GAEW,iBAArBU,IACTA,EAAmB7G,SAASgH,iBAAiBH,IAG3CA,aAA4BvL,cAC9BuL,EAAmB,CAACA,IAGtBA,EAAmBlN,MAAMe,UAAUuM,MAAMC,KAAKL,GAE1C,YAAYM,KAAKL,GACZD,EAAiBzH,IAAI,SAACT,GAC3B,IAAM4H,EAAOlN,EAAKsF,EAAmB,QACrC,OAAOD,EAAUC,EAAmB4H,EAAKlD,eAAgBkD,EAAKjD,wBAIlEuD,EAAiBhM,QAAQ,SAAUe,GACjC,GAAI,yBAAyBuL,KAAKL,GAChC,OAAOzB,EAASyB,GAAQlL,GAG1B,CAAC,cAAe,gBAAgBf,QAAQ,SAACuM,GACnC5M,OAAOE,UAAU2M,eAAeH,KAAKf,EAASiB,IAAqC,OAAvBjB,EAAQiB,IACtEE,QAAQC,KAAK,qEAA8DH,EAAS,6GAIxFjB,EAAU3L,OAAOQ,OAAO,GAAI4H,EAAsBjH,EAAMC,GAAiBhB,OAAQuL,GAEjFxK,EAAMC,GAAiBhB,OAASuL,EAEhC9M,EAAKuC,EAAiB,OAAQuK,GAE9BvK,EAAgBoD,YAAa,EAE7B4H,EAAehL,GAEf,IAEI4L,EAFEC,EAAYnO,EAAOsC,EAAgBsD,SAAUiH,EAAQlH,OAG3D,GAA4B,OAAxBkH,EAAQ9K,kBAAgDlC,IAAxBgN,EAAQ9K,YAA2B,CACrE,IAAMqM,EAAgB1H,SAASC,cAAcrE,EAAgBmE,SACzDoG,EAAQ9K,uBAAuBC,YACjCoM,EAAcjC,YAAYU,EAAQ9K,aAElCqM,EAAcxH,UAAYiG,EAAQ9K,YAEpCmM,EAAoBE,EAAcxI,SAAS,GAG7CvD,EAAMC,GAAiBP,YAAcuE,EAAgBhE,EAAiB4L,EAAmBrB,EAAQtG,kBAEjGxG,EAAKuC,EAAiB,QAASuK,EAAQlH,OAEnCkH,EAAQzD,WACVrJ,EAAKuC,EAAiB,aAAcuK,EAAQzD,YACnCyD,EAAQtD,aACjBxJ,EAAKuC,EAAiB,cAAeuK,EAAQtD,aAG/CyD,EAAe1K,GACf2J,EAAKkC,EAAW,OAAQ,UACxBlC,EAAKkC,EAAW,eAAgB,SAMhCd,EAAG/K,EAAiB,YAAa,SAAU+L,GAEzC,IAAMnJ,EAAS4C,EAAeuG,GAC9B,IAA0B,IAAtBnJ,EAAOQ,aAGX2I,EAAEC,6BAEGzB,EAAQM,QAAWjI,EAAOxE,QAAQmM,EAAQM,UAAiD,UAArCjI,EAAOqJ,aAAa,cAA/E,CAIA,IAAMlJ,EAAoBoH,EAAavH,EAAQmJ,GACzCG,EAAW5B,EAAgBvH,EAAmBH,GAGpDsF,EAA0BxK,EAAOqF,EAAkBO,SAAUiH,EAAQlH,OACrE2E,EAAcE,EAAwB/F,QAAQ+J,GAC9CjE,EAAqBhG,EAASiK,EAAUnJ,EAAkBO,UAC1DyE,EAAkBhF,EAGlB,IAAIsD,EAAgC,KACG,iBAA5BkE,EAAQpE,gBAEjBE,OADAA,EAAYjC,SAASkB,cAAciF,EAAQpE,mBAC9BuF,QAAQS,MAAM,8DACU,aAA5B5B,EAAQpE,kBACjBE,EAAYkE,EAAQpE,iBAEtBD,EAAa6F,EAAGG,EAAU7F,GAE1BwB,EAAiBlD,EAAiBuH,GAClCpE,EAAgB1C,EAAgB8G,GAChCA,EAAS1H,UAAUC,IAAI8F,EAAQnD,eAE/BuC,EADA/B,EAAW2B,EAAY2C,EAAUnJ,GAClB,eAAgB,QAG/BA,EAAkBqJ,cAAc,IAAIC,YAAY,YAAa,CAC3DC,OAAQ,CACNzF,OAAQ,CACN0F,aAActE,EACdpE,MAAOmE,EACPlE,UAAWiE,GAEb7J,KAAM0J,EACN4E,eAAgB5J,SAQtBmI,EAAG/K,EAAiB,YAAa,SAAC+L,GAChC,IAAMnJ,EAAS4C,EAAeuG,GACxBhJ,EAAoBoH,EAAavH,EAAQmJ,GAE3ChJ,GAAqBA,IAAsBoF,IAC7CC,EAA+B1K,EAAOqF,EAAkBO,SAAU7F,EAAKsF,EAAmB,UACvFrF,OAAO,SAAAQ,GAAQ,OAAAA,IAAS6B,EAAMC,GAAiBP,cAE9C8K,EAAQjD,0BACVvE,EAAkByB,UAAUC,IAAI8F,EAAQjD,0BAE1CvE,EAAkBqJ,cAAc,IAAIC,YAAY,YAAa,CAC3DC,OAAQ,CACNzF,OAAQ,CACN0F,aAActE,EACdpE,MAAOmE,EACPlE,UAAWiE,GAEbnB,YAAa,CACX9C,UAAWf,EACX0J,kBAAmBrE,GAErBlK,KAAM0J,EACN4E,eAAgB5J,MAIpBmI,EAAGhI,EAAmB,YAAa,SAAAgJ,GAGjC,IAAMW,EAAYX,EAAEY,eAAiBZ,EAAEa,YAClCb,EAAEc,cAAc7D,SAAS0D,KACxBnC,EAAQjD,0BACVvE,EAAkByB,UAAU0E,OAAOqB,EAAQjD,0BAE7CvE,EAAkBqJ,cAAc,IAAIC,YAAY,YAAa,CAC3DC,OAAQ,CACNzF,OAAQ,CACN0F,aAActE,EACdpE,MAAOmE,EACPlE,UAAWf,GAEb7E,KAAM0J,EACN4E,eAAgB5J,UAM1BuF,EAAoBpF,IAQtBgI,EAAG/K,EAAiB,UAAW,SAAU+L,GACvC,GAAKnE,EAAL,CAIAA,EAASpD,UAAU0E,OAAOqB,EAAQnD,eAClCuC,EAAK/B,EAAU,eAAgB,SAEc,SAAzCA,EAASqE,aAAa,gBAA2D,SAA9BxO,EAAKmK,EAAU,YACpEA,EAASsB,cAEiB3L,IAAxBqK,EAASmC,aACXnC,EAAShD,MAAMkF,QAAUlC,EAASmC,kBAC3BnC,EAASmC,YAElB,IAAM+C,EAAqB/O,MAAME,KAAKI,EAAO0O,UAAUvJ,IAAI,SAAA/F,GAAQ,OAAAA,EAAKgC,cACrE/B,OAAO,SAAA+B,GAAe,OAAAA,aAAuBC,cAC7ChC,OAAO0E,GAAS,GAEf0K,GACFA,EAAmB5D,SAIrBlJ,EAAgBoM,cAAc,IAAIC,YAAY,WAAY,CACxDC,OAAQ,CACNzF,OAAQ,CACN0F,aAActE,EACdpE,MAAOmE,EACPlE,UAAWiE,GAEb7J,KAAM0J,MAOVE,EADAD,EADAD,EADAO,EAAoB,QAUtB4C,EAAG/K,EAAiB,OAAQ,SAAU+L,GACpC,GAAKpF,EAAe3G,EAAiB4H,EAAS/G,eAA9C,CAGAkL,EAAEiB,iBACFjB,EAAEkB,kBAEFxP,EAAKmK,EAAU,UAAW,QAE1B,IAAMkF,EAAqB/O,MAAME,KAAKI,EAAO0O,UAAUvJ,IAAI,SAAC/F,GAC1D,OAAOA,EAAKgC,cAGX/B,OAAO,SAAA+B,GAAe,OAAAA,aAAuBC,cAE7ChC,OAAO0E,GAAS,GACnB,GAAI0K,EAAJ,CACEA,EAAmBI,YAAYtF,QAEHrK,IAAxBqK,EAASmC,aACXnC,EAAShD,MAAMkF,QAAUlC,EAASmC,kBAC3BnC,EAASmC,YAUpB/J,EAAgBoM,cAAc,IAAIC,YAAY,WAAY,CACxDC,OAAQ,CACNzF,OAAQ,CACN0F,aAActE,EACdpE,MAAOmE,EACPlE,UAAWiE,GAEb7J,KAAM0J,MAIV,IAAMnI,EAAcM,EAAMC,GAAiBP,YACrC0N,EAAczP,EAAOqK,EAAgBzE,SAAUiH,EAAQlH,OAC1D3F,OAAO,SAAAQ,GAAQ,OAAAA,IAASuB,IACrB2N,GAA2C,IAApB5O,KAAK4E,WAAsB5E,KAAOA,KAAKqC,cAC9DwM,EAAmB3P,EAAO0P,EAAqB9J,SAAU7F,EAAK2P,EAAsB,UACvF1P,OAAO,SAAAQ,GAAQ,OAAAA,IAASuB,IACrB6N,EAA0BrL,EAAS2F,EAAU7J,MAAME,KAAK2J,EAAS/G,cAAcyC,UAClF5F,OAAO,SAAAQ,GAAQ,OAAAA,IAASuB,KACrB8N,EAAmBtL,EAAS2F,EAAUyF,GAExC9C,EAAQjD,0BACV8F,EAAqB5I,UAAU0E,OAAOqB,EAAQjD,0BAO5CW,IAAuBqF,GAA2BvF,IAAoBqF,GACxEpN,EAAgBoM,cAAc,IAAIC,YAAY,aAAc,CAC1DC,OAAQ,CACNzF,OAAQ,CACN0F,aAActE,EACdpE,MAAOmE,EACPlE,UAAWiE,EACX0E,kBAAmBvE,EACnB7E,MAAO8J,GAETvG,YAAa,CACX/C,MAAO0J,EACPhB,aAAce,EACdxJ,UAAWsJ,EACXX,kBAAmBrE,EACnB/E,MAAOgK,GAETnP,KAAM0J,WApDVnK,EAAKmK,EAAU,UAAW,YA0D9B,IAAM4F,EAAyBjM,EAAS,SAACvB,EAAiB5C,EAAS2I,EAAOE,GACxE,GAAK2B,EAWL,GANI2C,EAAQkD,uBACV1N,EAAMC,GAAiBP,YAAYmF,MAAM8I,OAAS7F,EAAiB,KACnE9H,EAAMC,GAAiBP,YAAYmF,MAAM+I,MAAQ7F,EAAgB,OAIN,EAAzD/J,MAAME,KAAK+B,EAAgBsD,UAAUnB,QAAQ/E,GAAe,CAC9D,IAAMwQ,EAAajJ,EAAiBvH,GAC9ByQ,EAAYzI,EAAgBhI,GAC5B0Q,EAAmB7L,EAASlC,EAAMC,GAAiBP,YAAarC,EAAQyD,cAAcyC,UACtFyK,EAAY9L,EAAS7E,EAASA,EAAQyD,cAAcyC,UAE1D,GAAiBuE,EAAb+F,GAA2C9F,EAAZ+F,EAA2B,CAE5D,IAAMG,EAAmBJ,EAAa/F,EAChCoG,EAAqBJ,EAAY/F,EACjCoG,EAAYtN,EAAOxD,GAASgE,IAC5B+M,EAAavN,EAAOxD,GAAS4D,KACnC,GAAI8M,EAAmBC,IACO,aAAxBxD,EAAQ5C,aAA8B1B,EAAQiI,GACnB,eAAxB3D,EAAQ5C,aAAgC5B,EAAQoI,GACvD,OAEF,GAAuBJ,EAAnBD,IAC0B,aAAxBvD,EAAQ5C,aAAsCuG,EAAYN,EAAaI,EAAjC/H,GACX,eAAxBsE,EAAQ5C,aAAwCwG,EAAaN,EAAYI,EAAjClI,GAC/C,YAIwBxI,IAAxBqK,EAASmC,aACXnC,EAASmC,WAAanC,EAAShD,MAAMkF,SAGR,SAA3BlC,EAAShD,MAAMkF,UACjBlC,EAAShD,MAAMkF,QAAU,QAK3B,IAAIsE,GAAa,EACjB,IACE,IAAMC,EAAwBzN,EAAOxD,GAASgE,IAAMhE,EAAQkR,aAAe,EACrEC,EAA0B3N,EAAOxD,GAAS4D,KAAO5D,EAAQoR,YAAc,EAC7EJ,EAAsC,aAAxB7D,EAAQ5C,aAAwC0G,GAATpI,GACxB,eAAxBsE,EAAQ5C,aAA0C4G,GAATxI,EAC9C,MAAOgG,GACPqC,EAAaN,EAAmBC,EAG9BK,EACFK,EAAMrR,EAAS2C,EAAMC,GAAiBP,aAEtCiP,EAAOtR,EAAS2C,EAAMC,GAAiBP,aAGzC1B,MAAME,KAAKI,EAAO0O,UAEfrP,OAAO,SAAAD,GAAQ,YAAqBF,IAArBE,EAAKgC,cAEpBR,QAAQ,SAACxB,GACJA,EAAKgC,cAAgBM,EAAMC,GAAiBP,aAC9ChC,EAAKgC,YAAYyJ,eAGlB,CAEL,IAAMyF,EAAe5Q,MAAME,KAAKI,EAAO0O,UACpCrP,OAAO,SAACD,GAAS,YAAqBF,IAArBE,EAAKgC,cACtB+D,IAAI,SAAC/F,GACJ,OAAOA,EAAKgC,eAGuB,IAAnCkP,EAAaxM,QAAQ/E,IAAmB4C,IAAoB5C,GAAYM,EAAON,EAAQkG,SAAUiH,EAAQlH,OAAOhD,SAClHsO,EAAa1P,QAAQ,SAAC7B,GAAY,OAAAA,EAAQ8L,WAC1C9L,EAAQyM,YAAY9J,EAAMC,GAAiBP,gBAG9C8K,EAAQhJ,UAELqN,EAAkB,SAAU7C,GAChC,IAAI3O,EAAU2O,EAAEnJ,OACV5C,GAAyC,IAAvB5C,EAAQgG,WAAsBhG,EAAU+M,EAAa/M,EAAS2O,GAEtF,GADA3O,EAAUkN,EAAgBtK,EAAiB5C,GACtCwK,GAAajB,EAAe3G,EAAiB4H,EAAS/G,gBAAyD,SAAvCpD,EAAKuC,EAAiB,aAAnG,CAGA,IAAMuK,EAAU9M,EAAKuC,EAAiB,QAClC+E,SAASwF,EAAQ/C,WAAa9J,EAAOsC,EAAgBsD,SAAU7F,EAAKuC,EAAiB,UAAUK,QAAU0E,SAASwF,EAAQ/C,WAAaI,EAAS/G,gBAAkBb,IAGtK+L,EAAEiB,iBACFjB,EAAEkB,kBACFlB,EAAEzF,aAAauI,YAA0D,IAA7C9O,EAAMC,GAAiBR,UAAU,QAAmB,OAAS,OACzFgO,EAAuBxN,EAAiB5C,EAAS2O,EAAEhG,MAAOgG,EAAE9F,UAG9D8E,EAAGc,EAAUvL,OAAON,GAAkB,WAAY4O,GAClD7D,EAAGc,EAAUvL,OAAON,GAAkB,YAAa4O,KAG9C3D,UAGTxB,EAASqF,QAAU,SAAU9O,GAxgBL,IAAUA,EAC1B2K,EACAtH,EACAuH,EAFAD,EAAOlN,EADmBuC,EAygBhBA,EAxgBmB,SAAW,GACxCqD,EAAQ3F,EAAOsC,EAAgBsD,SAAUqH,EAAKtH,OAC9CuH,EAAUvF,EAAWhC,EAAOsH,EAAKE,QAEvCxC,EAAiBrI,GAAiB,GAElCoJ,EAAIpJ,EAAiB,YACrBoJ,EAAIpJ,EAAiB,aACrBoJ,EAAIpJ,EAAiB,aACrBoJ,EAAIpJ,EAAiB,WACrBoJ,EAAIpJ,EAAiB,QAErBgK,EAAmBhK,GAEnBoJ,EAAIwB,EAAS,aACbzB,EAAiB9F,GACjB6G,EAAe7G,GACfgG,EAAgBhG,GAChBgG,EAAgB,CAACrJ,IACjBsJ,EAAsBvB,EAAiBI,GAEvCnI,EAAgBoD,YAAa,GAsf/BqG,EAASnB,OAAS,SAAUtI,GAC1B0K,EAAe1K,IAGjByJ,EAASsF,QAAU,SAAU/O,GAldL,IAAUA,EAC1B2K,EACAtH,EACAuH,EAFAD,EAAOlN,EADmBuC,EAmdhBA,EAldmB,QAC7BqD,EAAQ3F,EAAOsC,EAAgBsD,SAAUqH,EAAKtH,OAC9CuH,EAAUvF,EAAWhC,EAAOsH,EAAKE,QACvClB,EAAK3J,EAAiB,kBAAmB,QACzCvC,EAAKuC,EAAiB,YAAa,QACnC2J,EAAKiB,EAAS,YAAa,SAC3BxB,EAAIwB,EAAS,aACbvC,EAAiBrI,GAAiB,IA+cpCyJ,EAASuF,UAAY,CAEnBvR,KAAIA,EACJ0L,iBAAgBA,EAChBe,eAAcA,EACdF,mBAAkBA,EAClBV,sBAAqBA"} \ No newline at end of file diff --git a/public/js/mage/adminhtml/browser.js b/public/js/mage/adminhtml/browser.js index 98e26aa77..ae9e95f11 100644 --- a/public/js/mage/adminhtml/browser.js +++ b/public/js/mage/adminhtml/browser.js @@ -5,56 +5,61 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2023 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ -MediabrowserUtility = { - openDialog: function(url, width, height, title, options) { - if ($('browser_window') && typeof(Windows) != 'undefined') { - Windows.focus('browser_window'); + +const MediabrowserUtility = { + + dialogWindow: null, + dialogWindowId: 'browser_window', + + async openDialog(url, width, height, title, options) { + if (document.getElementById(this.dialogWindowId)) { return; } - this.dialogWindow = Dialog.info(null, Object.extend({ - closable: true, - resizable: false, - draggable: true, - className: 'magento', - windowClassName: 'popup-window', - title: title || 'Insert File...', - top: 50, - width: width || 950, - height: height || 600, - zIndex: options && options.zIndex || 1000, - recenterAuto: false, - hideEffect: Element.hide, - showEffect: Element.show, - id: 'browser_window', - onClose: this.closeDialog.bind(this) - }, options || {})); - new Ajax.Updater(document.querySelector('#' + this.dialogWindow.id + ' .dialog-content'), url, {evalScripts: true}); - }, - closeDialog: function(window) { - if (!window) { - window = this.dialogWindow; - } - if (window) { - window.close(); + try { + const result = await mahoFetch(url); + + this.dialogWindow = Dialog.info(result, { + id: this.dialogWindowId, + title: title || 'Insert File...', + className: 'magento', + windowClassName: 'popup-window', + width: width || 950, + height: height || 600, + onClose: this.closeDialog.bind(this), + ...options, + }); + } catch (error) { + alert(error.message); } - } + }, + + closeDialog(window) { + window ??= this.dialogWindow; + window?.close(); + }, }; -Mediabrowser = Class.create(); -Mediabrowser.prototype = { - targetElementId: null, - contentsUrl: null, - onInsertUrl: null, - newFolderUrl: null, - deleteFolderUrl: null, - deleteFilesUrl: null, - headerText: null, - tree: null, - currentNode: null, - storeId: null, - initialize: function (setup) { +class Mediabrowser { + + targetElementId = null; + contentsUrl = null; + onInsertUrl = null; + newFolderUrl = null; + deleteFolderUrl = null; + deleteFilesUrl = null; + headerText = null; + tree = null; + currentNode = null; + storeId = null; + + constructor() { + this.initialize(...arguments); + } + + initialize(setup) { this.newFolderPrompt = setup.newFolderPrompt; this.deleteFolderConfirmationMessage = setup.deleteFolderConfirmationMessage; this.deleteFileConfirmationMessage = setup.deleteFileConfirmationMessage; @@ -65,22 +70,29 @@ Mediabrowser.prototype = { this.deleteFolderUrl = setup.deleteFolderUrl; this.deleteFilesUrl = setup.deleteFilesUrl; this.headerText = setup.headerText; - }, - setTree: function (tree) { + } + + setTree(tree) { this.tree = tree; this.currentNode = tree.getRootNode(); - }, + } - getTree: function (tree) { + getTree(tree) { return this.tree; - }, + } + + selectFolder(node) { + if (this.currentNode === node) { + return; + } - selectFolder: function (node, event) { this.currentNode = node; + this.currentNode.select(); + this.hideFileButtons(); this.activateBlock('contents'); - if(node.id == 'root') { + if (node.id === 'root') { this.hideElement('button_delete_folder'); } else { this.showElement('button_delete_folder'); @@ -89,112 +101,115 @@ Mediabrowser.prototype = { this.updateHeader(this.currentNode); this.drawBreadcrumbs(this.currentNode); - this.showElement('loading-mask'); - new Ajax.Request(this.contentsUrl, { - parameters: {node: this.currentNode.id}, - evalJS: true, - onSuccess: function(transport) { - try { - this.currentNode.select(); - this.onAjaxSuccess(transport); - this.hideElement('loading-mask'); - if ($('contents') != undefined) { - $('contents').update(transport.responseText); - $$('div.filecnt').each(function(s) { - Event.observe(s.id, 'click', this.selectFile.bind(this)); - Event.observe(s.id, 'dblclick', this.insert.bind(this)); - }.bind(this)); - } - } catch(e) { - alert(e.message); - } - }.bind(this) - }); - }, + this.updateContent(); + } - selectFolderById: function (nodeId) { - var node = this.tree.getNodeById(nodeId); - if (node.id) { - this.selectFolder(node); + async updateContent() { + try { + const html = await mahoFetch(this.contentsUrl, { + method: 'POST', + body: new URLSearchParams({ + node: this.currentNode.id, + }), + }); + + const contentsEl = document.getElementById('contents'); + if (contentsEl) { + updateElementHtmlAndExecuteScripts(contentsEl, html); + contentsEl.querySelectorAll('div.filecnt').forEach((el) => { + el.addEventListener('click', this.selectFile.bind(this)); + el.addEventListener('dblclick', this.insert.bind(this)); + }); + } + } catch(error) { + alert(error.message); } - }, + } - selectFile: function (event) { - var div = Event.findElement(event, 'DIV'); - $$('div.filecnt.selected[id!="' + div.id + '"]').each(function(e) { - e.removeClassName('selected'); - }); - div.toggleClassName('selected'); - if(div.hasClassName('selected')) { + selectFolderById(nodeId) { + this.tree.getNodeById(nodeId)?.select(); + } + + selectFile(event) { + const div = event.target.closest('div.filecnt'); + const selected = !div.classList.contains('selected'); + + document.querySelectorAll('div.filecnt.selected').forEach((el) => el.classList.remove('selected')); + div.classList.toggle('selected', selected); + + if (selected) { this.showFileButtons(); } else { this.hideFileButtons(); } - }, + } - showFileButtons: function () { + showFileButtons() { this.showElement('button_delete_files'); this.showElement('button_insert_files'); - }, + } - hideFileButtons: function () { + hideFileButtons() { this.hideElement('button_delete_files'); this.hideElement('button_insert_files'); - }, + } - handleUploadComplete: function(files) { - $$('div[class*="file-row complete"]').each(function(e) { - $(e.id).remove(); + handleUploadComplete(files) { + document.querySelectorAll('div[class*="file-row complete"]').forEach((el) => { + document.getElementById(el.id)?.remove(); }); - this.selectFolder(this.currentNode); - }, + this.updateContent(); + } - insert: function(event) { - var div; - if (event != undefined) { - div = Event.findElement(event, 'DIV'); - } else { - $$('div.selected').each(function (e) { - div = $(e.id); - }); - } - if ($(div.id) == undefined) { + async insert(event) { + const div = event + ? event.target.closest('div.filecnt') + : Array.from(document.querySelectorAll('div.filecnt.selected')).pop(); + + if (!div) { return false; } - var targetEl = this.getTargetElement(); + + const targetEl = this.getTargetElement(); if (!targetEl) { - alert("Target element not found for content update"); - Windows.close('browser_window'); + alert('Target element not found for content update'); + MediabrowserUtility.closeDialog(); return; } - var params = {filename:div.id, node:this.currentNode.id, store:this.storeId}; - if (targetEl.tagName && targetEl.tagName.toLowerCase() == 'textarea') { - params.as_is = 1; - } + try { + const params = new URLSearchParams({ + filename: div.id, + node: this.currentNode.id, + store: this.storeId + }); - new Ajax.Request(this.onInsertUrl, { - parameters: params, - onSuccess: function(transport) { - try { - this.onAjaxSuccess(transport); - if (this.getMediaBrowserCallback()) { - self.blur(); - } - Windows.close('browser_window'); - if (targetEl.tagName && targetEl.tagName.toLowerCase() == 'input') { - targetEl.value = transport.responseText; - } else if (targetEl.tagName && targetEl.tagName.toLowerCase() == 'textarea') { - updateElementAtCursor(targetEl, transport.responseText); - } else { - targetEl(transport.responseText); - } - } catch (e) { - alert(e.message); - } - }.bind(this) - }); - }, + if (targetEl.tagName && targetEl.tagName === 'TEXTAREA') { + params.set('as_is', 1); + } + + const text = await mahoFetch(this.onInsertUrl, { + method: 'POST', + body: params, + }) + + if (this.getMediaBrowserCallback()) { + window.blur(); + } + MediabrowserUtility.closeDialog(); + + if (targetEl.tagName === 'INPUT') { + targetEl.value = text; + } else if (targetEl.tagName === 'TEXTAREA') { + updateElementAtCursor(targetEl, text); + } else { + targetEl(text); + } + + } catch (error) { + alert(error.message); + } + } /** * Find document target element in next order: @@ -206,164 +221,131 @@ Mediabrowser.prototype = { * * return HTMLelement | null */ - getTargetElement: function() { - if (typeof(tinyMCE) != 'undefined' && tinyMCE.get(this.targetElementId)) { - if ((callbak = this.getMediaBrowserCallback())) { - return callbak; - } else { - return null; - } + getTargetElement() { + if (typeof tinyMCE !== 'undefined' && tinyMCE.get(this.targetElementId)) { + return this.getMediaBrowserCallback(); } else { return document.getElementById(this.targetElementId); } - }, + } /** * return object|null */ - getMediaBrowserCallback: function() { - if (typeof(tinyMCE) != 'undefined' && tinyMCE.get(this.targetElementId) && typeof(tinyMceEditors) != 'undefined') { + getMediaBrowserCallback() { + if (typeof tinyMCE !== 'undefined' && tinyMCE.get(this.targetElementId) && typeof tinyMceEditors !== 'undefined') { return tinyMceEditors.get(this.targetElementId).getMediaBrowserCallback(); } return null; - }, + } - newFolder: function() { - var folderName = prompt(this.newFolderPrompt); + async newFolder() { + const folderName = prompt(this.newFolderPrompt); if (!folderName) { return false; } - new Ajax.Request(this.newFolderUrl, { - parameters: {name: folderName}, - onSuccess: function(transport) { - try { - this.onAjaxSuccess(transport); - if (transport.responseText.isJSON()) { - var response = transport.responseText.evalJSON(); - var newNode = new Ext.tree.AsyncTreeNode({ - text: response.short_name, - draggable:false, - id:response.id, - expanded: true - }); - var child = this.currentNode.appendChild(newNode); - this.tree.expandPath(child.getPath(), '', function(success, node) { - this.selectFolder(node); - }.bind(this)); - } - } catch (e) { - alert(e.message); - } - }.bind(this) - }); - }, + try { + const result = await mahoFetch(this.newFolderUrl, { + method: 'POST', + body: new URLSearchParams({ + name: folderName, + }), + }); + + const child = new MahoTreeNode(this.tree, { + text: result.short_name, + id: result.id, + }); + + this.currentNode.appendChild(child); + this.currentNode.sortChildren(); + this.tree.expandPath(this.currentNode.getPath()).then((node) => { + this.selectFolderById(result.id); + }); + } catch (error) { + alert(error.message); + } + } - deleteFolder: function() { + async deleteFolder() { if (!confirm(this.deleteFolderConfirmationMessage)) { return false; } - new Ajax.Request(this.deleteFolderUrl, { - onSuccess: function(transport) { - try { - this.onAjaxSuccess(transport); - var parent = this.currentNode.parentNode; - parent.removeChild(this.currentNode); - this.selectFolder(parent); - } - catch (e) { - alert(e.message); - } - }.bind(this) - }); - }, + try { + await mahoFetch(this.deleteFolderUrl, { method: 'POST' }); - deleteFiles: function() { + const parent = this.currentNode.parentNode; + parent.removeChild(this.currentNode); + this.selectFolder(parent); + + } catch (error) { + alert(error.message); + } + } + + async deleteFiles() { if (!confirm(this.deleteFileConfirmationMessage)) { return false; } - var ids = []; - var i = 0; - $$('div.selected').each(function (e) { - ids[i] = e.id; - i++; - }); - new Ajax.Request(this.deleteFilesUrl, { - parameters: {files: Object.toJSON(ids)}, - onSuccess: function(transport) { - try { - this.onAjaxSuccess(transport); - this.selectFolder(this.currentNode); - } catch(e) { - alert(e.message); - } - }.bind(this) - }); - }, - drawBreadcrumbs: function(node) { - if ($('breadcrumbs') != undefined) { - $('breadcrumbs').remove(); - } - if (node.id == 'root') { - return; + const ids = []; + for (const el of document.querySelectorAll('div.selected')) { + ids.push(el.id); } - var path = node.getPath().split('/'); - var breadcrumbs = ''; - for(var i = 0, length = path.length; i < length; i++) { - if (path[i] == '') { - continue; - } - var currNode = this.tree.getNodeById(path[i]); - if (currNode.id) { - breadcrumbs += '
    • '; - breadcrumbs += '' + currNode.text + ''; - if(i < (length - 1)) { - breadcrumbs += ' /'; - } - breadcrumbs += '
    • '; - } + + try { + await mahoFetch(this.deleteFilesUrl, { + method: 'POST', + body: new URLSearchParams({ + files: JSON.stringify(ids), + }), + }); + + this.updateContent(); + + } catch (error) { + alert(error.message); } + } - if (breadcrumbs != '') { - breadcrumbs = ''; - $('content_header').insert({after: breadcrumbs}); + drawBreadcrumbs(node) { + let breadcrumbsEl = document.getElementById('breadcrumbs'); + if (!breadcrumbsEl) { + breadcrumbsEl = document.createElement('ul'); + breadcrumbsEl.id = 'breadcrumbs'; + breadcrumbsEl.className = 'breadcrumbs'; + document.getElementById('content_header')?.after(breadcrumbsEl); } - }, - updateHeader: function(node) { - var header = (node.id == 'root' ? this.headerText : node.text); - if ($('content_header_text') != undefined) { - $('content_header_text').innerHTML = header; + if (node.id === 'root') { + breadcrumbsEl.innerHTML = ''; + return; } - }, - activateBlock: function(id) { - //$$('div [id^=contents]').each(this.hideElement); - this.showElement(id); - }, + const crumbs = node.getPath().split('/').map((id) => { + const currNode = this.tree.getNodeById(id); + return `
    • ${currNode.text}
    • `; + }); - hideElement: function(id) { - if ($(id) != undefined) { - $(id).addClassName('no-display'); - $(id).hide(); - } - }, + breadcrumbsEl.innerHTML = crumbs.join(' / '); + } - showElement: function(id) { - if ($(id) != undefined) { - $(id).removeClassName('no-display'); - $(id).show(); + updateHeader(node) { + const headerEl = document.getElementById('content_header_text'); + if (headerEl) { + headerEl.textContent = node.id === 'root' ? this.headerText : node.text; } - }, + } - onAjaxSuccess: function(transport) { - if (transport.responseText.isJSON()) { - var response = transport.responseText.evalJSON(); - if (response.error) { - throw response; - } else if (response.ajaxExpired && response.ajaxRedirect) { - setLocation(response.ajaxRedirect); - } - } + activateBlock(id) { + this.showElement(id); + } + + hideElement(id) { + document.getElementById(id)?.classList.add('no-display'); + } + + showElement(id) { + document.getElementById(id)?.classList.remove('no-display'); } }; diff --git a/public/js/mage/adminhtml/catalog/category.js b/public/js/mage/adminhtml/catalog/category.js new file mode 100644 index 000000000..343f8075e --- /dev/null +++ b/public/js/mage/adminhtml/catalog/category.js @@ -0,0 +1,371 @@ +/** + * Maho + * + * @category Mage + * @package Mage_Adminhtml + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + +/** + * Class to control the "Manage Categories" page + */ +class CategoryEditForm { + + /** + * @param {Object} config + * @param {string} config.treeDiv - DOM ID of div to build tree + * @param {string} config.containerEl - DOM ID of main page container + * @param {string} config.loadTreeUrl - URL to load tree nodes + * @param {string} config.editUrl - URL for edit page action + * @param {string} config.moveUrl - URL for category move action + * @param {string} [config.switchTreeUrl] - URL for AJAX store switch + * @param {string} [config.addRootCategoryBtn] - DOM ID of "Add Root Category" button + * @param {string} [config.addSubCategoryBtn] - DOM ID of "Add Subcategory" button + * @param {string} [config.categoryProductsEl] - DOM ID of "Category Products" grid container + * @param {boolean} [config.useAjax] - Whether to load the main content via AJAX + * @param {string} [config.tabsJsObjectName] - varien tabs JS variable name + */ + constructor(config) { + this.containerEl = document.getElementById(config.containerDiv); + if (!this.containerEl) { + throw new Error(`Container with ID ${config.containerDiv} not found in DOM`); + } + + this.config = { + treeDiv: null, + loadTreeUrl: null, + editUrl: null, + moveUrl: null, + switchTreeUrl: null, + addRootCategoryBtn: null, + addSubCategoryBtn: null, + categoryProductsEl: null, + useAjax: true, + tabsJsObjectName: null, + ...config, + }; + + this.ui = { + addRootCategoryBtn: document.getElementById(this.config.addRootCategoryBtn), + addSubCategoryBtn: document.getElementById(this.config.addSubCategoryBtn), + }; + + this.initVarienForm(); + this.initProductsGrid(); + + this.tree = new MahoTree(this.config.treeDiv, { + showRootNode: true, + treatAllNodesAsFolders: true, + selectable: { + mode: 'radio', + hideInputs: true, + onSelect: this.changeCategory.bind(this), + }, + sortable: { + onEnd: this.moveCategory.bind(this), + }, + lazyload: { + nodeParameter: 'id', + dataUrl: this.config.loadTreeUrl, + onBeforeLoad: (node, params) => { + if (this.wasExpanded) { + params.append('expand_all', '1'); + } + }, + onLoadException: (node, error) => { + setMessagesDiv(Translator.translate('Error loading children: %s', error), 'error'); + } + } + }); + } + + getEditUrl() { + return setRouteParams(this.config.editUrl, { + active_tab: window[this.config.tabsJsObjectName]?.activeTab?.name, + }); + } + + initVarienForm() { + this.formEl = this.containerEl.querySelector('form'); + if (!this.formEl) { + throw new Error(`Container with ID ${config.containerDiv} does not contain a form element.`); + } + this.varienForm = new varienForm(this.formEl.id); + if (this.config.useAjax) { + this.varienForm._submit = this.submitCategory.bind(this); + } + } + + initProductsGrid() { + const { gridJsObjectName, products } = window.productsInfo ?? {}; + const gridObj = window[gridJsObjectName]; + const inputEl = document.getElementById(this.config.categoryProductsEl); + + if (!gridObj || !products || !inputEl) { + return + } + + gridObj.updateSelected = () => { + gridObj.reloadParams = { 'selected_products[]': Object.keys(products) }; + inputEl.value = new URLSearchParams(products).toString(); + } + + gridObj.initRowCallback = (gridObj, row) => { + const checkboxEl = row.querySelector('.checkbox'); + const positionEl = row.querySelector('.input-text'); + if (checkboxEl && positionEl) { + positionEl.disabled = !checkboxEl.checked; + positionEl.addEventListener('change', (event) => { + if (checkboxEl.checked) { + products[checkboxEl.value] = positionEl.value; + gridObj.updateSelected(); + } + }); + } + } + + gridObj.rowClickCallback = (gridObj, event) => { + if (event.target.closest('td').querySelector('a, input:not([type=checkbox])')) { + return; + } + const checkboxEl = event.target.closest('tr').querySelector('input[type=checkbox]'); + if (checkboxEl) { + const checked = event.target === checkboxEl ? checkboxEl.checked : !checkboxEl.checked; + gridObj.setCheckboxChecked(checkboxEl, checked); + } + } + + gridObj.checkboxCheckCallback = (gridObj, element, checked) => { + const positionEl = event.target.closest('tr')?.querySelector('input[name=position]'); + if (positionEl) { + positionEl.disabled = !checked; + } + if (checked) { + products[element.value] = positionEl?.value ?? 0; + } else { + delete products[element.value]; + } + gridObj.updateSelected(); + } + + gridObj.rows.forEach((row) => gridObj.initRowCallback(gridObj, row)); + gridObj.updateSelected(); + } + + renderTree(config) { + const { root_visible, can_add_root, category_id, store_id, expanded, ...rest } = config.parameters; + + this.storeId = parseInt(store_id) || 0; + this.ui.addRootCategoryBtn?.classList.toggle('no-display', !can_add_root); + + this.tree.setRootVisible(root_visible); + this.tree.setRootNode({ + ...rest, + children: config.data, + expanded: true, + }); + + if (expanded) { + this.expandTree(); + } + + this.tree.getNodeById(category_id)?.select(); + } + + collapseTree() { + this.wasExpanded = false; + this.tree.collapseAll(); + } + + expandTree() { + this.wasExpanded = true; + this.tree.expandAll(); + } + + getSelectedCategory() { + return this.tree.getChecked().pop(); + } + + changeCategory() { + const category = this.getSelectedCategory(); + if (category && (category.id != window.categoryInfo?.category_id || this.storeId != window.categoryInfo?.store_id)) { + this.updateContent( + setRouteParams(this.getEditUrl(), { + store: this.storeId > 0 ? this.storeId : null, + id: category.id, + }) + ); + } + } + + resetCategory(url) { + this.updateContent( + setRouteParams(url, { + active_tab: null, + }) + ); + } + + saveCategory(url) { + this.varienForm.submit(); + } + + async submitCategory() { + try { + const result = await mahoFetch(this.formEl.action, { + method: 'POST', + body: new FormData(this.formEl), + }); + + this.updateContent( + setRouteParams(this.getEditUrl(), { + store: this.storeId > 0 ? this.storeId : null, + id: result.category_id, + }) + ); + } catch (error) { + setMessagesDiv(error.message, 'error'); + } + } + + async deleteCategory(url) { + const confirmed = confirm(Translator.translate('Are you sure you want to delete this category?')); + if (!confirmed) { + return; + } + if (!this.config.useAjax) { + return setLocation(setRouteParams(url, { form_key: FORM_KEY })); + } + try { + const result = await mahoFetch(url, { method: 'POST' }); + + this.tree.getNodeById(result.category_id)?.remove(); + this.tree.getNodeById(result.parent_id)?.select(); + + } catch (error) { + setMessagesDiv(error.message, 'error'); + } + } + + addCategory(url, isRoot) { + const parent = isRoot ? { id: 1 } : this.getSelectedCategory(); + if (!parent) { + alert(Translator.translate('Please select a parent category before adding a new one.')); + return; + } + if (parent.id === 1) { + this.tree.deselectAll(); + } + this.updateContent( + setRouteParams(url, { + active_tab: null, + store: this.storeId > 0 ? this.storeId : null, + parent: parent.id, + }) + ); + } + + async updateContent(url) { + if (!this.config.useAjax) { + return setLocation(url); + } + try { + const result = await mahoFetch(url, { method: 'POST' }); + + clearMessagesDiv(); + + if (result.title) { + document.title = result.title; + } + if (result.content) { + updateElementHtmlAndExecuteScripts(this.containerEl, result.content); + } + if (result.messages) { + setMessagesDivHtml(result.messages); + } + + if (window.categoryInfo) { + let node = this.tree.rootNode; + for (const breadcrumb of window.categoryInfo.breadcrumbs) { + if (!this.tree.getNodeById(breadcrumb.id)) { + await this.tree.expandPath(node.getPath()); + } + if (!this.tree.getNodeById(breadcrumb.id)) { + node.appendChild(new MahoTreeNode(this.tree, breadcrumb)); + } + node = this.tree.getNodeById(breadcrumb.id); + node.updateAttributes(breadcrumb); + } + if (this.ui.addSubCategoryBtn) { + this.ui.addSubCategoryBtn.disabled = !window.categoryInfo.can_add_sub; + } + } + + window[this.config.tabsJsObjectName]?.moveTabContentInDest(); + this.initVarienForm(); + this.initProductsGrid(); + + history.replaceState(null, '', setQueryParams(url, { isAjax: null })); + + } catch (error) { + setMessagesDiv(error.message, 'error'); + } + toolbarToggle.start(); + } + + async switchStore(event, switcher) { + if (switcher.useConfirm) { + const confirmed = confirm(Translator.translate('Please confirm site switching. All data that hasn\'t been saved will be lost.')); + if (!confirmed) { + event.target.value = this.storeId === 0 ? '' : this.storeId; + return; + } + } + + const storeId = parseInt(event.target.value) || 0; + const category = this.getSelectedCategory(); + + if (!this.config.useAjax) { + return setLocation( + setRouteParams(this.getEditUrl(), { + store: storeId > 0 ? storeId: null, + id: category?.id, + }) + ); + } + try { + const url = setRouteParams(this.config.switchTreeUrl, { + store: storeId > 0 ? storeId: null, + id: category?.id, + }); + + const result = await mahoFetch(url, { method: 'POST' }); + this.renderTree(result); + this.changeCategory(); + + } catch (error) { + setMessagesDiv(error.message, 'error'); + } + toolbarToggle.start(); + } + + async moveCategory(event) { + if (event.from === event.to && event.oldIndex === event.newIndex) { + return; + } + try { + const node = this.tree.getNodeByEl(event.item); + const result = await mahoFetch(this.config.moveUrl, { + method: 'POST', + body: new URLSearchParams({ + id: node.id, + pid: node.parentNode.id, + aid: node.previousNode?.id ?? 0, + }), + }); + } catch (error) { + setMessagesDiv(error.message, 'error'); + } + } +} diff --git a/public/js/mage/adminhtml/catalog/review.js b/public/js/mage/adminhtml/catalog/review.js new file mode 100644 index 000000000..3751476f8 --- /dev/null +++ b/public/js/mage/adminhtml/catalog/review.js @@ -0,0 +1,120 @@ +/** + * Maho + * + * @category Mage + * @package Mage_Adminhtml + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + +/** + * Class to control the "Edit Review" page + */ +class ReviewEditForm { + /** + * @param {Object} config + * @param {string} config.productEditUrl - URL to link to product edit page + * @param {string} config.ratingItemsUrl - URL to POST rating changes + */ + constructor(config) { + this.config = { + productEditUrl: null, + ratingItemsUrl: null, + ...config + }; + this.bindEventListeners(); + } + + bindEventListeners() { + document.addEventListener('DOMContentLoaded', () => { + document.getElementById('select_stores')?.addEventListener('change', this.updateRating.bind(this)); + }); + } + + toggleSaveButton(isEnabled = true) { + const saveBtn = document.getElementById('save_button'); + if (saveBtn) { + saveBtn.disabled = !isEnabled; + } + } + + toggleForm(isVisible = true) { + document.getElementById('add_review_form')?.parentNode.classList.toggle('no-display', !isVisible); + document.getElementById('reviewProductGrid')?.classList.toggle('no-display', !!isVisible); + document.getElementById('save_button')?.classList.toggle('no-display', !isVisible); + document.getElementById('reset_button')?.classList.toggle('no-display', !isVisible); + } + + showForm() { + this.toggleForm(true); + } + + hideForm() { + this.toggleForm(false); + } + + async gridRowClick(data, event) { + const url = event.target.closest('tr')?.title; + const success = await this.loadProductData(url); + if (success) { + this.showForm(); + } + } + + async loadProductData(url) { + let success = false; + try { + // Backwards compatibility: old code would store URL in `this.productInfoUrl` + // from `gridRowClick()` and then call this function with no parameters + if (!(url ??= this.productInfoUrl)) { + throw new Error('Product info URL not found'); + } + + const result = await mahoFetch(url, { method: 'POST' }); + + if (this.config.productEditUrl) { + const linkEl = document.createElement('a'); + linkEl.setAttribute('href', this.config.productEditUrl + `id/${result.id}`); + linkEl.setAttribute('target', '_blank'); + linkEl.textContent = result.name; + document.getElementById('product_name').replaceChildren(linkEl); + } else { + document.getElementById('product_name').textContent = result.name; + } + + document.getElementById('product_id').value = result.id; + success = true; + + } catch (error) { + setMessagesDiv(`Error loading product: ${error.message}`, 'error'); + } + return success; + } + + async updateRating() { + this.toggleSaveButton(false); + + try { + if (!this.config.ratingItemsUrl) { + throw new Error('Rating Items URL not found'); + } + + const body = new URLSearchParams({ + form_key: FORM_KEY, + stores: [...document.getElementById('select_stores').selectedOptions].map((opt) => opt.value), + }); + + document.querySelectorAll('#rating_detail input[type=radio]:checked').forEach((el) => { + body.append(el.name, el.value); + }); + + const html = await mahoFetch(this.config.ratingItemsUrl, { method: 'POST', body }); + updateElementHtmlAndExecuteScripts(document.getElementById('rating_detail'), html); + + } catch (error) { + setMessagesDiv(`Error loading rating details: ${error.message}`, 'error'); + } + + this.toggleSaveButton(true); + } +} diff --git a/public/js/mage/adminhtml/eav/set.js b/public/js/mage/adminhtml/eav/set.js new file mode 100644 index 000000000..afe0501ec --- /dev/null +++ b/public/js/mage/adminhtml/eav/set.js @@ -0,0 +1,348 @@ +/** + * Maho + * + * @category Mage + * @package Mage_Adminhtml + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + +/** + * Class to control the "Edit Attribute Set" page + */ +class EavAttributeSetForm { + + /** + * @param {Object} [config] + * @param {string} config.saveUrl - controller action to save set + * @param {string} config.deleteURL - controller action to delete set + * @param {string} [config.containerGroups] - id of div to build groups tree + * @param {string} [config.containerAttributes] - id of div to build unassigned attributes tree + * @param {string} [config.formId] - id of edit set form + * @param {string} [config.saveBtn] - id of save set button + * @param {string} [config.deleteBtn] - id of delete set button + * @param {string} [config.addGroupBtn] - id of add group button + * @param {string} [config.deleteGroupBtn] - id of delete group button + * @param {string} [config.renameGroupBtn] - id of rename group button + * @param {string} [config.removeDropZone] - id of drop zone div for removing attributes + * @param {boolean} [config.isReadOnly=false] - set true to disable sorting attributes + * @param {function():boolean} [config.canDeleteSet] - callback to prompt user before deleting set + * @param {function(MahoTreeNode):boolean|string} [config.canRemoveAttribute] - callback to determine if an attribute can be removed from set, return string for custom error message + * @param {function(MahoTreeNode):boolean|string} [config.canDeleteGroup] - callback to determine if a group can be deleted, return string for custom error message + */ + constructor(config) { + + this.config = { + saveUrl: null, + deleteUrl: null, + + containerGroups: 'tree-div1', + containerAttributes: 'tree-div2', + formId: 'set_prop_form', + saveBtn: 'save-button', + deleteBtn: 'delete-button', + addGroupBtn: 'add-group-button', + deleteGroupBtn: 'delete-group-button', + renameGroupBtn: 'rename-group-button', + removeDropZone: 'remove-drop-zone', + + isReadOnly: false, + canDeleteSet: null, + canRemoveAttribute: null, + canDeleteGroup: null, + + ...config, + }; + + this.formEl = document.getElementById(this.config.formId); + if (!this.formEl) { + throw new Error(`Form with ID ${this.config.formId} not found in DOM`); + } + + this.removeGroups = []; + if (typeof this.config.canDeleteSet === 'function') { + this.canDeleteSet = this.config.canDeleteSet.bind(this); + } + if (typeof this.config.canRemoveAttribute === 'function') { + this.canRemoveAttribute = this.config.canRemoveAttribute.bind(this); + } + if (typeof this.config.canDeleteGroup === 'function') { + this.canDeleteGroup = this.config.canDeleteGroup.bind(this); + } + + this.ui = { + saveBtn: document.getElementById(this.config.saveBtn), + deleteBtn: document.getElementById(this.config.deleteBtn), + addGroupBtn: document.getElementById(this.config.addGroupBtn), + deleteGroupBtn: document.getElementById(this.config.deleteGroupBtn), + renameGroupBtn: document.getElementById(this.config.renameGroupBtn), + } + + this.tree1 = new MahoTree(this.config.containerGroups, { + showRootNode: false, + selectable: this.config.isReadOnly ? false : { + mode: 'single', + hideInputs: true, + onSelect: this.onSelectGroup.bind(this), + }, + sortable: this.config.isReadOnly ? false : { + group: 'attributes', + containDepth: true, + mahoTreeDropZone: { + dropZone: document.getElementById(this.config.removeDropZone), + onHover: this.onRemoveAttributeHover.bind(this), + onDrop: this.onRemoveAttribute.bind(this), + }, + }, + }); + this.tree2 = new MahoTree(this.config.containerAttributes, { + showRootNode: false, + sortable: this.config.isReadOnly ? false : { + group: { + name: 'attributes.2', + put: false, + }, + sort: false, + }, + cssVars: { + 'indent': '0', + 'line-style': 'none', + }, + }); + + this.bindEventListeners(); + } + + bindEventListeners() { + this.ui.saveBtn?.addEventListener('click', this.saveSet.bind(this)); + this.ui.deleteBtn?.addEventListener('click', this.deleteSet.bind(this)); + this.ui.addGroupBtn?.addEventListener('click', this.addGroup.bind(this)); + this.ui.deleteGroupBtn?.addEventListener('click', this.deleteGroup.bind(this)); + this.ui.renameGroupBtn?.addEventListener('click', this.renameGroup.bind(this)); + } + + buildGroupTree(data) { + this.tree1.setRootNode(data); + this.tree1.expandAll(); + } + + buildAttributeTree(data) { + this.tree2.setRootNode(data); + } + + async saveSet() { + const validator = new Validation(this.formEl, { onSubmit: false }); + if (!validator.validate()) { + return; + } + + showLoader(); + + try { + const formData = new FormData(this.formEl); + + const data = { + attributes: [], + groups: [], + not_attributes: [], + }; + + for (const [ field, value ] of formData.entries()) { + if (field !== 'form_key') { + data[field] = value; + formData.delete(field); + } + } + + const tree1 = this.tree1.rootNode.toObject(); + for (const [ i, group ] of Object.entries(tree1.children)) { + data.groups.push([ group.id, group.text, parseInt(i) + 1 ]); + for (const [ j, attr ] of Object.entries(group.children)) { + data.attributes.push([ attr.id, group.id, parseInt(j) + 1, attr.entity_id ]); + } + } + + const tree2 = this.tree2.rootNode.toObject(); + for (const attr of tree2.children) { + if (attr.entity_id) { + data.not_attributes.push(attr.entity_id); + } + } + + data.removeGroups = this.removeGroups; + formData.set('data', JSON.stringify(data)); + + const result = await mahoFetch(this.config.saveUrl, { + method: 'POST', + body: formData, + }); + + setLocation(result.url); + + } catch (error) { + setMessagesDiv(error.message, 'error'); + hideLoader(); + } + } + + deleteSet() { + if (!this.canDeleteSet()) { + return; + } + showLoader(); + this.formEl.action = this.config.deleteUrl; + this.formEl.submit(); + } + + canDeleteSet() { + const response = prompt(Translator.translate('Are you sure you want to delete this set? Type "confirm" to proceed.')); + return response === 'confirm'; + } + + onSelectGroup([node]) { + const disabled = node?.type !== 'folder'; + if (this.ui.deleteGroupBtn) { + this.ui.deleteGroupBtn.disabled = disabled; + this.ui.deleteGroupBtn.classList.toggle('disabled', disabled); + } + if (this.ui.renameGroupBtn) { + this.ui.renameGroupBtn.disabled = disabled; + this.ui.renameGroupBtn.classList.toggle('disabled', disabled); + } + } + + addGroup() { + let groupName = prompt(Translator.translate('Please enter a new group name')); + if (groupName === null) { + return; + } + + groupName = escapeHtml(groupName).trim(); + if (groupName === '') { + return this.addGroup(); + } + + if (!this.validateGroupName(groupName)) { + alert(Translator.translate('Attribute group with the "%s" name already exists', groupName)); + return; + } + + const newNode = new MahoTreeNode(this.tree1, { + text: groupName, + type: 'folder', + allowDrop : true, + allowDrag : true, + }); + + this.tree1.rootNode.prependChild(newNode); + } + + renameGroup() { + const selected = this.tree1.getChecked()[0]; + if (!selected) { + return; + } + + let groupName = prompt(Translator.translate('Please enter a new group name'), selected.text); + if (groupName === null) { + return; + } + + groupName = escapeHtml(groupName).trim(); + + if (groupName === '') { + return this.renameGroup(); + } + + if (!this.validateGroupName(groupName)) { + alert(Translator.translate('Attribute group with the "%s" name already exists', groupName)); + return; + } + + selected.updateAttributes({ + text: groupName, + }); + } + + deleteGroup() { + const selected = this.tree1.getChecked()[0]; + if (!selected) { + return; + } + + const result = this.canDeleteGroup(selected) || Translator.translate('Cannot delete group.'); + if (result !== true) { + return alert(result); + } + + const animateSortables = [ + Sortable.get(selected.ui.ctNode), + Sortable.get(this.tree2.rootNode.ui.ctNode), + ]; + animateSortables.forEach((sortable) => sortable.captureAnimationState()); + + this.removeGroups.push(selected.id); + for (const child of selected.childNodes) { + this.tree2.rootNode.appendChild(child); + } + selected.remove(); + this.sortUnusedAttributes(); + + animateSortables.forEach((sortable) => sortable.animateAll()); + } + + validateGroupName(groupName, exceptNodeId) { + for (const node of this.tree1.rootNode.childNodes) { + if (node.id != exceptNodeId && node.text.toLowerCase() === groupName.toLowerCase()) { + return false; + } + } + return true; + } + + onRemoveAttributeHover({ dragNode }) { + if (dragNode.type === 'folder') { + return { icon: 'invalid', message: Translator.translate('Cannot unassign group') }; + } + const result = this.canRemoveAttribute(dragNode) || Translator.translate('Cannot unassign attribute'); + if (result !== true) { + return { icon: 'invalid', message: result }; + } + return { icon: 'delete', message: Translator.translate('Remove attribute from set') }; + } + + onRemoveAttribute({ dragNode }) { + + if (this.canRemoveAttribute(dragNode) !== true) { + return; + } + return () => { + const target = this.tree2.rootNode.ui.ctNode; + let referenceNode = null; + for (referenceNode of target.children) { + if (referenceNode.dataset.text > dragNode.text) { + break; + } + } + target.insertBefore(dragNode.ui.wrap, referenceNode); + } + } + + canRemoveAttribute(node) { + if (!node.attributes.is_user_defined) { + return Translator.translate('Cannot unassign system attribute'); + } + return true; + } + + canDeleteGroup(group) { + if (group.childNodes.some((node) => !node.attributes.is_user_defined)) { + return Translator.translate('Cannot delete group. Please move system attributes to another group and try again.'); + } + return true; + } + + sortUnusedAttributes() { + this.tree2.rootNode.sortChildren(); + } +} diff --git a/public/js/mage/adminhtml/product.js b/public/js/mage/adminhtml/product.js index eb40efcf9..5111cc565 100644 --- a/public/js/mage/adminhtml/product.js +++ b/public/js/mage/adminhtml/product.js @@ -5,6 +5,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2023 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -428,13 +429,12 @@ Product.Configurable.prototype = { Event.observe(li.down('.attribute-use-default-label'), 'change', this.onLabelUpdate); }.bind(this)); if (!this.readonly) { - sortable(this.container, { + new Sortable(this.container, { handle: '.attribute-name-container', - items: ':not(.disabled)', - forcePlaceholderSize: true + filter: '.disabled', + animation: 150, + onUpdate: this.updatePositions.bind(this), }); - - this.container.addEventListener('sortupdate', this.updatePositions.bind(this)); } this.updateSaveInput(); }, diff --git a/public/js/mage/adminhtml/tools.js b/public/js/mage/adminhtml/tools.js index 27b3a1b02..2c54e8271 100644 --- a/public/js/mage/adminhtml/tools.js +++ b/public/js/mage/adminhtml/tools.js @@ -5,6 +5,7 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2019-2023 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ function setLocation(url){ @@ -754,3 +755,74 @@ function copyText(event) { copyIcon.classList.remove('icon-copy-copied'); }, 1000); } + +/** + * Clear
      + */ +function clearMessagesDiv() { + setMessagesDivHtml(''); +} + +/** + * Set a message in
      + * + * @param {string} message - text value of the message to display + * @param {string} type - one of `success|error|notice` + */ +function setMessagesDiv(message, type = 'success') { + setMessagesDivHtml(`
        • ${message}
      `); +} + +/** + * Raw function to update
      + * + * @param {string} html +*/ +function setMessagesDivHtml(html) { + const div = document.getElementById('messages'); + if (div) { + div.innerHTML = html; + } +} + +/** + * Set Varien type route params, i.e. /id/1/ + * + * @param {string} url - the base URL + * @param {Object} params - key value pairs to add, update, or remove + */ +function setRouteParams(url, params = {}) { + url = new URL(url); + if (!url.pathname.endsWith('/')) { + url.pathname += '/'; + } + for (const [ key, val ] of Object.entries(params)) { + const regex = new RegExp(String.raw`\/${key}\/\w+\/`); + if (val === null || val === false) { + url.pathname = url.pathname.replace(regex, '/'); + } else if (url.pathname.match(regex)) { + url.pathname = url.pathname.replace(regex, `/${key}/${val}/`); + } else { + url.pathname += `${key}/${val}/`; + } + } + return url.toString(); +} + +/** + * Set query params, i.e. ?id=1 + * + * @param {string} url - the base URL + * @param {Object} params - key value pairs to add, update, or remove + */ +function setQueryParams(url, params = {}) { + url = new URL(url); + for (const [ key, val ] of Object.entries(params)) { + if (val === null || val === false) { + url.searchParams.delete(key); + } else { + url.searchParams.set(key, val); + } + } + return url.toString(); +} diff --git a/public/js/mage/adminhtml/wysiwyg/widget.js b/public/js/mage/adminhtml/wysiwyg/widget.js index 978b5d9d3..9258b4906 100644 --- a/public/js/mage/adminhtml/wysiwyg/widget.js +++ b/public/js/mage/adminhtml/wysiwyg/widget.js @@ -5,386 +5,374 @@ * @package Mage_Adminhtml * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2022-2023 The OpenMage Contributors (https://openmage.org) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ -var widgetTools = { - getDivHtml: function(id, html) { - if (!html) html = ''; - return '
      ' + html + '
      '; +const widgetTools = { + dialogWindow: null, + dialogWindowId: 'widget_window', + + getDivHtml(id, html) { + return `
      ${html ?? ''}
      `; }, - onAjaxSuccess: function(transport) { - if (transport.responseText.isJSON()) { - var response = transport.responseText.evalJSON(); - if (response.error) { - throw response; - } else if (response.ajaxExpired && response.ajaxRedirect) { - setLocation(response.ajaxRedirect); - } - } + appendHtml(targetEl, html) { + const fragment = document.createRange().createContextualFragment(html); + fragment.querySelectorAll('script[src]').forEach(script => script.remove()); + targetEl.append(...fragment.children); }, - openDialog: function(widgetUrl) { - if ($('widget_window') && typeof(Windows) != 'undefined') { - Windows.focus('widget_window'); + async openDialog(widgetUrl) { + if (document.getElementById(this.dialogWindowId)) { return; } - this.dialogWindow = Dialog.info(null, { - draggable:true, - resizable:false, - closable:true, - className:'magento', - windowClassName:"popup-window", - title:Translator.translate('Insert Widget...'), - top:50, - width:950, - //height:450, - zIndex:9000, - recenterAuto:false, - hideEffect:Element.hide, - showEffect:Element.show, - id:'widget_window', - onClose: this.closeDialog.bind(this) - }); - new Ajax.Updater(document.querySelector('#' + this.dialogWindow.id + ' .dialog-content'), widgetUrl, {evalScripts: true}); - }, - closeDialog: function(window) { - if (!window) { - window = this.dialogWindow; - } - if (window) { - window.close(); + try { + const result = await mahoFetch(widgetUrl); + + this.dialogWindow = Dialog.info(result, { + id: this.dialogWindowId, + title: 'Insert Widget...', + className: 'magento', + windowClassName: 'popup-window', + width: 950, + onClose: this.closeDialog.bind(this) + }); + } catch (error) { + console.error(error); + alert(error.message); } - } + }, + + closeDialog(window) { + window ??= this.dialogWindow; + window?.close(); + }, }; -var WysiwygWidget = {}; -WysiwygWidget.Widget = Class.create(); -WysiwygWidget.Widget.prototype = { +const WysiwygWidget = {} + +WysiwygWidget.Widget = class { + constructor() { + this.initialize(...arguments); + } - initialize: function(formEl, widgetEl, widgetOptionsEl, optionsSourceUrl, widgetTargetId) { - $(formEl).insert({bottom: widgetTools.getDivHtml(widgetOptionsEl)}); - this.formEl = formEl; - this.widgetEl = $(widgetEl); - this.widgetOptionsEl = $(widgetOptionsEl); + initialize(formId, widgetId, widgetOptionsId, optionsSourceUrl, widgetTargetId) { + this.formEl = document.getElementById(formId); + widgetTools.appendHtml(this.formEl, widgetTools.getDivHtml(widgetOptionsId)) + + this.widgetEl = document.getElementById(widgetId); + this.widgetOptionsEl = document.getElementById(widgetOptionsId); this.optionsUrl = optionsSourceUrl; - this.optionValues = new Hash({}); + this.optionValues = new Map(); this.widgetTargetId = widgetTargetId; - if (typeof(tinyMCE) != "undefined" && tinyMCE.activeEditor) { + + if (typeof tinyMCE !== 'undefined' && tinyMCE.activeEditor) { this.bMark = tinyMCE.activeEditor.selection.getBookmark(); } - Event.observe(this.widgetEl, "change", this.loadOptions.bind(this)); - + this.widgetEl.addEventListener('change', this.loadOptions.bind(this)); this.initOptionValues(); - }, + } - getOptionsContainerId: function() { - return this.widgetOptionsEl.id + '_' + this.widgetEl.value.gsub(/\//, '_'); - }, + getOptionsContainerId() { + return this.widgetOptionsEl.id + '_' + this.widgetEl.value.replaceAll(/\//g, '_'); + } - switchOptionsContainer: function(containerId) { - $$('#' + this.widgetOptionsEl.id + ' div[id^=' + this.widgetOptionsEl.id + ']').each(function(e) { - this.disableOptionsContainer(e.id); - }.bind(this)); - if(containerId != undefined) { + switchOptionsContainer(containerId) { + this.widgetOptionsEl.querySelectorAll(`div[id^=${this.widgetOptionsEl.id}]`).forEach((el) => { + this.disableOptionsContainer(el.id); + }); + + if (containerId != undefined) { this.enableOptionsContainer(containerId); } this._showWidgetDescription(); - }, + } - enableOptionsContainer: function(containerId) { - $$('#' + containerId + ' .widget-option').each(function(e) { - e.removeClassName('skip-submit'); - if (e.hasClassName('obligatory')) { - e.removeClassName('obligatory'); - e.addClassName('required-entry'); + enableOptionsContainer(containerId) { + const containerEl = document.getElementById(containerId); + if (!containerEl) { + return; + } + containerEl.querySelectorAll(`.widgetOption`).forEach((el) => { + el.classList.remove('skip-submit'); + if (el.classList.contains('obligatory')) { + el.classList.remove('obligatory'); + el.classList.add('required-entry'); } }); - $(containerId).removeClassName('no-display'); - }, + containerEl.classList.remove('no-display'); + } - disableOptionsContainer: function(containerId) { - if ($(containerId).hasClassName('no-display')) { + disableOptionsContainer(containerId) { + const containerEl = document.getElementById(containerId); + if (!containerEl || containerEl.classList.contains('no-display')) { return; } - $$('#' + containerId + ' .widget-option').each(function(e) { + containerEl.querySelectorAll(`.widgetOption`).forEach((el) => { // Avoid submitting fields of unactive container - if (!e.hasClassName('skip-submit')) { - e.addClassName('skip-submit'); - } + el.classList.add('skip-submit'); + // Form validation workaround for unactive container - if (e.hasClassName('required-entry')) { - e.removeClassName('required-entry'); - e.addClassName('obligatory'); + if (el.classList.contains('required-entry')) { + el.classList.remove('required-entry'); + el.classList.add('obligatory'); } }); - $(containerId).addClassName('no-display'); - }, + containerEl.classList.add('no-display'); + } // Assign widget options values when existing widget selected in WYSIWYG - initOptionValues: function() { - + initOptionValues() { if (!this.wysiwygExists()) { return false; } - var e = this.getWysiwygNode(); - if (e != undefined && e.id) { - var widgetCode = Base64.idDecode(e.id); - if (widgetCode.indexOf('{{widget') != -1) { - this.optionValues = new Hash({}); - widgetCode.gsub(/([a-z0-9\_]+)\s*\=\s*[\"]{1}([^\"]+)[\"]{1}/i, function(match){ - if (match[1] == 'type') { - this.widgetEl.value = match[2]; - } else { - this.optionValues.set(match[1], match[2]); - } - }.bind(this)); + const el = this.getWysiwygNode(); + if (!el || !el.id) { + return; + } - this.loadOptions(); - } + const widgetCode = Base64.idDecode(e.id); + if (widgetCode.indexOf('{{widget') === -1) { + return; } - }, - loadOptions: function() { + this.optionValues = new Map(); + + widgetCode.replaceAll(/([a-z0-9\_]+)\s*\=\s*[\"]{1}([^\"]+)[\"]{1}/gi, (...match) => { + if (match[1] == 'type') { + this.widgetEl.value = match[2]; + } else { + this.optionValues.set(match[1], match[2]); + } + }); + + this.loadOptions(); + } + + async loadOptions() { if (!this.widgetEl.value) { this.switchOptionsContainer(); return; } - var optionsContainerId = this.getOptionsContainerId(); - if ($(optionsContainerId) != undefined) { - this.switchOptionsContainer(optionsContainerId); + const containerId = this.getOptionsContainerId(); + const containerEl = document.getElementById(containerId); + + if (containerEl) { + this.switchOptionsContainer(containerId); return; } this._showWidgetDescription(); - var params = {widget_type: this.widgetEl.value, values: this.optionValues}; - new Ajax.Request(this.optionsUrl, - { - parameters: {widget: Object.toJSON(params)}, - onSuccess: function(transport) { - try { - widgetTools.onAjaxSuccess(transport); - this.switchOptionsContainer(); - if ($(optionsContainerId) == undefined) { - this.widgetOptionsEl.insert({bottom: widgetTools.getDivHtml(optionsContainerId, transport.responseText)}); - } else { - this.switchOptionsContainer(optionsContainerId); - } - } catch(e) { - alert(e.message); - } - }.bind(this) + try { + const params = { + widget_type: this.widgetEl.value, + values: Object.fromEntries(this.optionValues), + }; + + const html = await mahoFetch(this.optionsUrl, { + method: 'POST', + body: new URLSearchParams({ + widget: JSON.stringify(params), + }), + }); + + this.switchOptionsContainer(); + + if (containerEl) { + this.switchOptionsContainer(containerId); + } else { + widgetTools.appendHtml(this.widgetOptionsEl, widgetTools.getDivHtml(containerId, html)); } - ); - }, - _showWidgetDescription: function() { - var noteCnt = this.widgetEl.next().down('small'); - var descrCnt = $('widget-description-' + this.widgetEl.selectedIndex); - if(noteCnt != undefined) { - var description = (descrCnt != undefined ? descrCnt.innerHTML : ''); - noteCnt.update(descrCnt.innerHTML); + } catch(error) { + console.error(error); + alert(error.message); } - }, + } + + _showWidgetDescription() { + const noteEl = this.widgetEl.nextElementSibling?.querySelector('small'); + const descEl = document.getElementById(`widget-description-${this.widgetEl.selectedIndex}`); + if (noteEl) { + noteEl.textContent = descEl?.textContent; + } + } - insertWidget: function() { - widgetOptionsForm = new varienForm(this.formEl); - if(widgetOptionsForm.validator && widgetOptionsForm.validator.validate() || !widgetOptionsForm.validator){ - var formElements = []; - var i = 0; - Form.getElements($(this.formEl)).each(function(e) { - if(!e.hasClassName('skip-submit')) { - formElements[i] = e; - i++; + async insertWidget() { + const widgetOptionsForm = new varienForm(this.formEl); + if (widgetOptionsForm.validator && widgetOptionsForm.validator.validate() || !widgetOptionsForm.validator) { + try { + const formData = new FormData(); + for (const el of this.formEl.elements) { + if (!el.classList.contains('skip-submit')) { + formData.append(el.name, el.value); + } } - }); - // Add as_is flag to parameters if wysiwyg editor doesn't exist - var params = Form.serializeElements(formElements); - if (!this.wysiwygExists()) { - params = params + '&as_is=1'; - } + // Add as_is flag to parameters if wysiwyg editor doesn't exist + if (!this.wysiwygExists()) { + formData.set('as_is', 1); + } + + const html = await mahoFetch(this.formEl.action, { + method: 'POST', + body: formData, + }) + + Windows.close('widget_window'); - new Ajax.Request($(this.formEl).action, - { - parameters: params, - onComplete: function(transport) { - try { - widgetTools.onAjaxSuccess(transport); - Windows.close("widget_window"); - - if (typeof(tinyMCE) != "undefined" && tinyMCE.activeEditor) { - tinyMCE.activeEditor.focus(); - if (this.bMark) { - tinyMCE.activeEditor.selection.moveToBookmark(this.bMark); - } - } - - this.updateContent(transport.responseText); - } catch(e) { - alert(e.message); + if (typeof tinyMCE !== 'undefined' && tinyMCE.activeEditor) { + tinyMCE.activeEditor.focus(); + if (this.bMark) { + tinyMCE.activeEditor.selection.moveToBookmark(this.bMark); } - }.bind(this) - }); + } + + this.updateContent(html); + + } catch(error) { + console.error(error); + alert(error.message); + } } - }, + } - updateContent: function(content) { + updateContent(content) { if (this.wysiwygExists()) { - this.getWysiwyg().execCommand("mceInsertContent", false, content); + this.getWysiwyg().execCommand('mceInsertContent', false, content); } else { - var textarea = document.getElementById(this.widgetTargetId); + const textarea = document.getElementById(this.widgetTargetId); updateElementAtCursor(textarea, content); varienGlobalEvents.fireEvent('tinymceChange'); } - }, + } - wysiwygExists: function() { - return (typeof tinyMCE != 'undefined') && tinyMCE.get(this.widgetTargetId); - }, + wysiwygExists() { + return typeof tinyMCE !== 'undefined' && tinyMCE.get(this.widgetTargetId); + } - getWysiwyg: function() { + getWysiwyg() { return tinyMCE.activeEditor; - }, + } - getWysiwygNode: function() { + getWysiwygNode() { return tinyMCE.activeEditor.selection.getNode(); } }; -WysiwygWidget.chooser = Class.create(); -WysiwygWidget.chooser.prototype = { +WysiwygWidget.chooser = class { // HTML element A, on which click event fired when choose a selection - chooserId: null, + chooserId = null; // Source URL for Ajax requests - chooserUrl: null, + chooserUrl = null; // Chooser config - config: null, + config = null; // Chooser dialog window - dialogWindow: null, + dialogWindow = null; // Chooser content for dialog window - dialogContent: null, + dialogContent = null; - overlayShowEffectOptions: null, - overlayHideEffectOptions: null, + constructor() { + this.initialize(...arguments); + } - initialize: function(chooserId, chooserUrl, config) { + initialize(chooserId, chooserUrl, config) { this.chooserId = chooserId; this.chooserUrl = chooserUrl; this.config = config; - }, + } - getResponseContainerId: function() { + getResponseContainerId() { return 'responseCnt' + this.chooserId; - }, + } - getChooserControl: function() { - return $(this.chooserId + 'control'); - }, + getChooserControl() { + return document.getElementById(this.chooserId + 'control'); + } - getElement: function() { - return $(this.chooserId + 'value'); - }, + getElement() { + return document.getElementById(this.chooserId + 'value'); + } - getElementLabel: function() { - return $(this.chooserId + 'label'); - }, + getElementLabel() { + return document.getElementById(this.chooserId + 'label'); + } - open: function() { - $(this.getResponseContainerId()).show(); - }, + open() { + document.getElementById(this.getResponseContainerId())?.classList.remove('no-display'); + } - close: function() { - $(this.getResponseContainerId()).hide(); + close() { + document.getElementById(this.getResponseContainerId())?.classList.add('no-display'); this.closeDialogWindow(); - }, + } - choose: function(event) { + async choose(event) { // Open dialog window with previously loaded dialog content if (this.dialogContent) { this.openDialogWindow(this.dialogContent); return; } - // Show or hide chooser content if it was already loaded - var responseContainerId = this.getResponseContainerId(); - - // Otherwise load content from server - new Ajax.Request(this.chooserUrl, - { - parameters: {element_value: this.getElementValue(), element_label: this.getElementLabelText()}, - onSuccess: function(transport) { - try { - widgetTools.onAjaxSuccess(transport); - this.dialogContent = widgetTools.getDivHtml(responseContainerId, transport.responseText); - this.openDialogWindow(this.dialogContent); - } catch(e) { - alert(e.message); - } - }.bind(this) - } - ); - }, + try { + const html = await mahoFetch(this.chooserUrl, { + method: 'POST', + body: new URLSearchParams({ + element_value: this.getElementValue(), + element_label: this.getElementLabelText(), + }), + }); + + this.dialogContent = widgetTools.getDivHtml(this.getResponseContainerId(), html); + this.openDialogWindow(this.dialogContent); - openDialogWindow: function(content) { - this.overlayShowEffectOptions = Windows.overlayShowEffectOptions; - this.overlayHideEffectOptions = Windows.overlayHideEffectOptions; - Windows.overlayShowEffectOptions = {duration:0}; - Windows.overlayHideEffectOptions = {duration:0}; + } catch (error) { + console.error(error); + alert(error.message); + } + } + + openDialogWindow(content) { this.dialogWindow = Dialog.info(content, { - draggable:true, - resizable:true, - closable:true, - className:"magento", - windowClassName:"popup-window", - title:this.config.buttons.open, - top:50, + id: 'widget-chooser', + title: this.config.buttons.open, + className: 'magento', + windowClassName: 'popup-window', width:950, - height:500, - zIndex:9000, - recenterAuto:false, - hideEffect:Element.hide, - showEffect:Element.show, - id:"widget-chooser", onClose: this.closeDialogWindow.bind(this) }); - content.evalScripts.bind(content).defer(); - }, + } - closeDialogWindow: function(dialogWindow) { - if (!dialogWindow) { - dialogWindow = this.dialogWindow; - } + closeDialogWindow(dialogWindow) { + dialogWindow ??= this.dialogWindow; if (dialogWindow) { dialogWindow.close(); - Windows.overlayShowEffectOptions = this.overlayShowEffectOptions; - Windows.overlayHideEffectOptions = this.overlayHideEffectOptions; } this.dialogWindow = null; - }, + } - getElementValue: function(value) { + getElementValue(value) { return this.getElement().value; - }, + } - getElementLabelText: function(value) { + getElementLabelText(value) { return this.getElementLabel().innerHTML; - }, + } - setElementValue: function(value) { + setElementValue(value) { this.getElement().value = value; - }, + } - setElementLabel: function(value) { + setElementLabel(value) { this.getElementLabel().innerHTML = value; } }; diff --git a/public/js/maho-autocomplete.js b/public/js/maho-autocomplete.js index aedd08261..a878a0cdf 100644 --- a/public/js/maho-autocomplete.js +++ b/public/js/maho-autocomplete.js @@ -1,3 +1,12 @@ +/** + * Maho + * + * @category Maho + * @package js + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + class MahoAutocomplete { constructor(field, destinationElement, url, options = {}) { this.field = field; diff --git a/public/js/maho-dialog.js b/public/js/maho-dialog.js index d6cd3940c..4a1350003 100644 --- a/public/js/maho-dialog.js +++ b/public/js/maho-dialog.js @@ -1,3 +1,12 @@ +/** + * Maho + * + * @category Maho + * @package js + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + (function() { if (typeof Windows === 'undefined') { Windows = {}; @@ -10,8 +19,14 @@ // Insert CSS for backdrop and dialog layout const style = document.createElement('style'); style.textContent = ` + body:has(dialog) { + overflow: hidden; + } dialog::backdrop { - background-color: rgba(0, 0, 0, 0.7); + background: none; + } + dialog:last-of-type::backdrop { + background: rgba(0, 0, 0, 0.7); } dialog { border: none; @@ -28,9 +43,11 @@ height: 80vh; display: flex; flex-direction: column; + overflow: visible; } .dialog-header { display: flex; + align-items: center; padding: 15px 20px; border-bottom: 1px solid #e0e0e0; } @@ -50,53 +67,56 @@ .dialog-buttons { display: flex; justify-content: flex-end; + gap: 10px; padding: 15px 20px; border-top: 1px solid #e0e0e0; } .dialog-buttons button { - margin-left: 10px; padding: 8px 16px; border: none; border-radius: 4px; + color: black; background-color: #f0f0f0; cursor: pointer; } .dialog-buttons button:hover { background-color: #e0e0e0; } - #dialog-ok { - background-color: #4CAF50; + .dialog-buttons button[id$=-ok] { + background-color: #0090ff; color: white; } - #dialog-ok:hover { - background-color: #45a049; + .dialog-buttons button[id$=-ok]:hover { + background-color: #1a9bff; } - dialog .x-tree-node>div {height:auto!important} - dialog .x-tree-node-ct {position:relative !important} `; document.head.appendChild(style); function createDialog(options) { const dialogCount = document.querySelectorAll('dialog').length; const dialog = document.createElement('dialog'); - dialog.id = `dialog-${dialogCount + 1}`; + dialog.id = options.id ?? `dialog-${dialogCount + 1}`; dialog.options = options; + dialog.innerHTML = `

      ${options.title || ''}

      -
      ${options.content || ''}
      +
      `; if (options.ok || options.cancel) { - dialog.innerHTML = dialog.innerHTML + ` + dialog.innerHTML += `
      ${options.cancel ? `` : ''} ${options.ok ? `` : ''}
      `; } - document.body.appendChild(dialog); + + if (options.content) { + updateElementHtmlAndExecuteScripts(dialog.querySelector('.dialog-content'), options.content); + } // Set width and height if provided if (options.width) { @@ -106,6 +126,8 @@ dialog.style.height = `${options.height}px`; } + document.body.appendChild(dialog); + function closeDialog(action) { if (action === 'ok' && typeof options.ok === 'function') { options.ok(dialog); @@ -142,7 +164,7 @@ if (openDialogs.length > 0) { const dialog = openDialogs[openDialogs.length - 1]; dialog.close(); - dialog.remove; + dialog.remove(); } } }); @@ -169,7 +191,7 @@ if (openDialogs.length > 0) { const dialog = openDialogs[openDialogs.length - 1]; dialog.close(); - dialog.remove; + dialog.remove(); } }; -})(); \ No newline at end of file +})(); diff --git a/public/js/maho-effects.js b/public/js/maho-effects.js index 70a8dc5a1..209c5d383 100644 --- a/public/js/maho-effects.js +++ b/public/js/maho-effects.js @@ -1,3 +1,12 @@ +/** + * Maho + * + * @category Maho + * @package js + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + if (typeof Effect === 'undefined') { const Effect = { Appear: function(element, options = {}) { diff --git a/public/js/maho-tree.js b/public/js/maho-tree.js new file mode 100644 index 000000000..09b28561b --- /dev/null +++ b/public/js/maho-tree.js @@ -0,0 +1,928 @@ +/** + * Maho + * + * @category Maho + * @package js + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) + * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + +/** + * Maho Tree - create a nested tree with checkboxes, drag-and-drop, and lazy-loading + */ +class MahoTree { + static nodeDataMap = new WeakMap(); + + /** + * @typedef {Object} SelectableOpts + * @prop {string} [mode='nested'] - `radio|single|simple|nested` + * @prop {boolean} [hideInputs=false] - hide radio / checkbox inputs and show outline around selected nodes + * @prop {string} [radioName] - if radio mode, then form name of the radio elements + * @prop {function(Array):null} [onSelect] - callback when a node is selected + * + * @typedef {Object} SortableOpts - plus any from {@link https://github.com/SortableJS/Sortable?tab=readme-ov-file#options} + * @prop {Object|string} [group] - use the same group name to allow nodes to be dragged across trees + * @prop {boolean} [mahoTreeNestedFolder=true] - enable the nested folder sortable.js plugin + * @prop {boolean} [containDepth=false] - contain draggable nodes to the same depth + * @prop {boolean} [rootSortable=true] - allow the root node to be sortable + * + * @typedef {Object} LazyloadOpts + * @prop {string} [dataUrl] - URL to load children from + * @prop {string} [nodeParameter='node'] - POST param to send node's ID as + * @prop {function(MahoTreeNode, URLSearchParams):null} [onBeforeLoad] - callback before children are loaded + * @prop {function(MahoTreeNode, Error):null} [onLoadException] - callback when loading children fails + * + * @typedef {Object} MahoTreeCssVars + * @prop {string} [indent='1.25rem'] - length to indent each node level + * @prop {string} [spacing='0.25rem'] - length to space in between each node + * @prop {string} [line-style='1px dotted #aaa'] - border style for connecting lines + * @prop {string} [outline-style='2px solid #0090FF'] - outline style for selected nodes when `config.selectable.hideInputs=true` + * @prop {string} [marker-size='10px'] - size for the [+] and [-] expand icon + * @prop {string} [icon-size='16px'] - size for the folder or leaf node icons + * @prop {string} [label-gap='0.25rem'] - length between icon, checkbox, and label + * @prop {string} [disabled-color='#999'] - text color for disabled nodes + * @prop {string} [drop-color='#ccc'] - background color for dropping nodes onto folders + * + * @param {string} container - the container element's DOM ID + * @param {Object} [config] - config options + * @param {SelectableOpts|boolean|string} [config.selectable=false] - `true` for default options, string `radio|single|simple|nested`, or object + * @param {SortableOpts|boolean|string} [config.sortable=false] - `true` for default options, string for sortable group name, or object + * @param {LazyloadOpts|boolean|string} [config.lazyload=false] - `true` for default options, string for dataUrl, or object + * @param {boolean} [config.showRootNode=true] - toggle visibility of the root node + * @param {boolean} [config.showIcons=true] - toggle visibility of icons + * @param {boolean} [config.treatAllNodesAsFolders=false] - make all node type folder + * @param {boolean} [config.varienSetHasChanges] - emit event marking the tab as having changes + * @param {MahoTreeCssVars} [config.cssVars] - + */ + constructor(container, config = {}) { + const containerEl = document.getElementById(container); + if (!containerEl) { + throw new Error(`Element with ID ${container} not found in DOM`); + } + + this.uniqId = generateRandomString(6); + + this.config = { + selectable: false, + sortable: false, + lazyload: false, + showRootNode: true, + treatAllNodesAsFolders: false, + showIcons: true, + varienSetHasChanges: true, + cssVars: {}, + ...config, + }; + + this.selectableOpts = { + mode: 'nested', + hideInputs: false, + radioName: this.uniqId, + onSelect: null, + }; + + this.sortableOpts = { + group: null, + animation: 150, + invertSwap: true, + fallbackOnBody: true, + revertOnSpill: true, + mahoTreeNestedFolder: true, + containDepth: false, + rootSortable: true, + }; + + this.lazyloadOpts = { + dataUrl: null, + nodeParameter: 'node', + onBeforeLoad: null, + onLoadException: null, + }; + + // Check for options using the string short-hand and set the appropriate option + if (typeof this.config.selectable === 'string') { + this.selectableOpts.mode = this.config.selectable; + this.config.selectable = true; + } + if (typeof this.config.sortable === 'string') { + this.sortableOpts.group = this.config.sortable; + this.config.sortable = true; + } + if (typeof this.config.lazyload === 'string') { + this.lazyloadOpts.dataUrl = this.config.lazyload; + this.config.lazyload = true; + } + + // Check for options using full object definitions, and bind any callbacks to this tree instance + for (const key of ['selectable', 'sortable', 'lazyload']) { + if (typeof this.config[key] === 'object' && this.config[key] !== null) { + const obj = Object.assign(this[key + 'Opts'], this.config[key]); + for (const callback of Object.keys(obj)) { + if (typeof obj[callback] === 'function') { + obj[callback] = obj[callback].bind(this); + } + } + this.config[key] = true; + } + } + + this.createElement(); + containerEl.appendChild(this.rootEl); + + this.bindEventListeners(); + } + + setRootNode(node) { + if (node instanceof MahoTreeNode) { + this.rootNode = node; + } else if (Array.isArray(node)) { + this.rootNode = new MahoTreeNode(this, { + id: '__root__', + text: 'Root', + expanded: true, + children: node, + }); + } else if (typeof node === 'object' && node !== null) { + this.rootNode = new MahoTreeNode(this, { + expanded: true, + children: [], + ...node, + }); + } else { + throw new TypeError('Root node must be an object, array, or MahoTreeNode'); + } + this.rootNode.isRoot = true; + this.rootEl.replaceChildren(this.rootNode.ui.wrap); + } + + setRootVisible(flag) { + this.config.showRootNode = flag; + this.rootEl.classList.toggle('hide-root-node', !flag); + } + + createElement() { + this.rootEl = document.createElement('ul'); + this.rootEl.classList.add('maho-tree'); + this.setRootVisible(this.config.showRootNode); + + for (const [cssVar, cssVal] of Object.entries(this.config.cssVars)) { + this.rootEl.style.setProperty(`--${cssVar}`, cssVal); + } + if (this.selectableOpts.hideInputs === true) { + this.rootEl.classList.add('hide-checkbox'); + } + } + + bindEventListeners() { + this.rootEl.addEventListener('change', (event) => { + // Check conditions where checkboxes might change + const shouldUpdate = [ + typeof event.originalEvent === 'DragEvent', + event.target.tagName === 'INPUT' && ['checkbox', 'radio'].includes(event.target.type), + ]; + if (!shouldUpdate.any(Boolean)) { + return; + } + if (this.selectableOpts.mode === 'nested') { + this.updateNestedCheckboxes(); + } else if (this.selectableOpts.mode === 'single') { + this.rootEl.querySelectorAll('input[type=checkbox]:checked').forEach((el) => { + el.checked = el === event.target; + }); + } + if (typeof this.selectableOpts.onSelect === 'function') { + this.selectableOpts.onSelect(this.getChecked()); + } + }); + + this.mutationObserver = new MutationObserver((mutationList, observer) => { + for (const mutation of mutationList) { + for (const el of mutation.addedNodes) { + if (el.tagName === 'LI') { + el.querySelectorAll(':scope ul').forEach(this.bindSortable.bind(this)); + } + } + } + if (this.selectableOpts.mode === 'nested') { + this.updateNestedCheckboxes(); + } + }); + + this.mutationObserver.observe(this.rootEl, { childList: true, subtree: true }); + } + + bindSortable(el) { + if (!this.config.sortable || Sortable.get(el)) { + return; + } + if (this.sortableOpts.rootSortable === false && this.rootNode.ui.ctNode === el) { + return; + } + + const group = typeof this.sortableOpts.group === 'object' && this.sortableOpts.group !== null + ? this.sortableOpts.group + : { name: this.sortableOpts.group }; + + group.name ??= `sortable.${this.uniqId}`; + + if (this.sortableOpts.containDepth === true) { + let current = el, depth = 0; + while (current !== this.rootEl) { + current = current.parentNode.closest('ul'); + depth++; + } + group.name += '.' + depth; + } + + MahoTreeDropZonePlugin.mount(); + MahoTreeNestedFolderPlugin.mount(); + + new Sortable(el, { ...this.sortableOpts, group }); + } + + updateNestedCheckboxes() { + if (this.selectableOpts.mode !== 'nested') { + return; + } + Array.from(this.rootEl.querySelectorAll('li')).reverse().forEach((el) => { + const parent = el.querySelector('input[type=checkbox]'); + const children = Array.from(el.querySelectorAll(':scope ul input[type=checkbox]')); + if (parent && children.length) { + if (children.every((el) => el.checked)) { + parent.checked = true; + parent.indeterminate = false; + } else if (children.all((el) => !el.checked)) { + parent.checked = false; + parent.indeterminate = false; + } else { + parent.checked = false; + parent.indeterminate = true; + } + } + }); + } + + storeNode(node) { + MahoTree.nodeDataMap.set(node.ui.wrap, node); + } + + getRootNode() { + return this.rootNode; + } + + getNodeByEl(el) { + return MahoTree.nodeDataMap.get(el); + } + + getNodeById(id) { + return this.getNodeByEl(this.rootEl.querySelector(`li[data-id='${id}']`)); + } + + getNodeByText(text) { + return this.getNodeByEl(this.rootEl.querySelector(`li[data-text='${text}']`)); + } + + getChecked() { + return Array.from(this.rootEl.querySelectorAll('input:checked')).map((el) => { + return this.getNodeByEl(el.closest('li')); + }); + } + + async expandPath(path) { + const parts = path.split('/').filter(Boolean); + let current = this.rootNode; + for (const part of parts) { + const node = this.getNodeById(part); + if (node) { + current = await node.expand(); + } else { + break; + } + } + return current; + } + + async expandAll() { + await this.rootNode.expandAll(); + } + + collapseAll() { + this.rootNode.collapseAll(); + } + + selectAll() { + this.rootEl.querySelectorAll('input[type=checkbox]').forEach((el) => { + el.indeterminate = false; + el.checked = true; + }); + if (typeof this.selectableOpts.onSelect === 'function') { + this.selectableOpts.onSelect(this.getChecked()); + } + } + + deselectAll() { + this.rootEl.querySelectorAll('input[type=checkbox],input[type=radio]').forEach((el) => { + el.indeterminate = false; + el.checked = false; + }); + if (typeof this.selectableOpts.onSelect === 'function') { + this.selectableOpts.onSelect(this.getChecked()); + } + } + + dispatchEvent() { + this.rootEl.dispatchEvent(...arguments); + } +} + +/** + * MahoTreeNode - a node belonging to a MahoTree instance + */ +class MahoTreeNode { + /** + * @typedef {Object} MahoTreeNodeData + * @prop {string|number} [id] - a unique id for this node + * @prop {string} [type] - type of node, can be `folder|leaf`, or blank for auto-detection + * @prop {string} [text] - label for the node + * @prop {string} [name] - alias for text + * @prop {string|boolean} [icon] - icon for the node, or false to hide + * @prop {string} [cls] - extra classes to add to icon node + * @prop {boolean} [selectable] - is the node selectable, defaults to tree setting + * @prop {boolean} [disabled=false] - is the node disabled + * @prop {boolean} [expanded=false] - if type folder, is the node expanded + * @prop {boolean} [allowDrag=true] - can the node be dragged + * @prop {boolean} [allowDrop=true] - if type folder, can nodes be dropped into it + * @prop {MahoTreeNodeData[]} [children] - if type folder, list of child nodes + * + * @param {MahoTree} tree - the MahoTree instance this node is attached to + * @param {MahoTreeNodeData|MahoTreeNodeData[]} data - + */ + constructor(tree, data) { + if (!tree instanceof MahoTree) { + throw new TypeError('Tree parameter must be instance of MahoTree'); + } + if (typeof data !== 'object' || Array.isArray(data) || data === null) { + throw new TypeError('Data parameter must be an object'); + } + + const { children, ...attributes } = data; + + /** + * @type {{ + * wrap: HTMLLIElement, + * label: HTMLDivElement|HTMLLabelElement, + * textNode: HTMLSpanElement, + * iconNode: HTMLSpanElement, + * ctNode: HTMLUListElement|null, + * details: HTMLDetailsElement|null, + * summary: HTMLSummaryElement|null, + * checkbox: HTMLInputElement|null, + * }} + */ + this.ui = {}; + this.tree = tree; + this.attributes = attributes; + this.attributes.id ??= 'node-' + generateRandomString(6); + + if (this.attributes.type) { + this.type = this.attributes.type; + } else if (this.tree.config.treatAllNodesAsFolders || Array.isArray(children)) { + this.type = 'folder'; + } else { + this.type = 'leaf'; + } + + this.createElement(); + this.updateAttributes(); + this.bindEventListeners(); + + if (this.type === 'folder') { + if (Array.isArray(children)) { + for (const child of children) { + this.appendChild(new MahoTreeNode(this.tree, child)) + } + this.hasLoadedChildren = true; + } + this.ui.wrap.append(this.ui.details); + } else { + this.ui.wrap.append(this.ui.label); + } + + this.isRoot = false; + this.tree.storeNode(this); + } + + createElement() { + this.ui.wrap = document.createElement('li'); + this.createLabelElement(); + if (this.type === 'folder') { + this.createDetailsElement() + this.ui.wrap.append(this.ui.details); + this.ui.summary.append(this.ui.label); + } else { + this.ui.wrap.append(this.ui.label); + } + } + + createLabelElement() { + if (this.attributes.selectable ?? this.tree.config.selectable) { + this.ui.label = document.createElement('label'); + this.ui.label.innerHTML = ''; + this.ui.checkbox = this.ui.label.querySelector('input'); + if (this.tree.selectableOpts.mode === 'radio') { + this.ui.checkbox.type = 'radio'; + } + } else { + this.ui.label = document.createElement('div'); + this.ui.label.innerHTML = ''; + } + this.ui.label.classList.add('label'); + this.ui.textNode = this.ui.label.querySelector('span:not(.icon)'); + this.ui.iconNode = this.ui.label.querySelector('span.icon'); + } + + createDetailsElement() { + this.ui.details = document.createElement('details'); + this.ui.details.open = this.attributes.expanded; + this.ui.details.innerHTML = '
        '; + this.ui.summary = this.ui.details.children[0]; + this.ui.ctNode = this.ui.details.children[1]; + } + + updateAttributes(data = {}) { + Object.assign(this.attributes, data); + + this.id = this.attributes.id; + this.ui.wrap.dataset.id = this.id; + + this.text = this.attributes.text ?? this.attributes.name; + this.text ??= this.type.charAt(0).toUpperCase() + this.type.slice(1); + this.ui.wrap.dataset.text = this.text; + this.ui.textNode.textContent = unescapeHtml(this.text); + + this.ui.label.classList.toggle('disabled', this.attributes.disabled ?? false); + + this.icons = []; + if (typeof this.attributes.icon === 'string') { + this.icons.push(...this.attributes.icon.trim().split(/\s+/).filter(Boolean)); + } else if (this.attributes.icon === false || this.tree.config.showIcons === false) { + this.icons.push('no-icon'); + } + if (typeof this.attributes.cls === 'string') { + this.icons.push(...this.attributes.cls.trim().split(/\s+/).filter(Boolean)); + } + if (this.icons.length === 0) { + this.icons.push(this.type); + } + this.ui.iconNode.className = 'icon ' + this.icons.join(' '); + + if (this.ui.checkbox) { + this.ui.checkbox.checked = this.attributes.checked; + this.ui.checkbox.disabled = this.attributes.disabled; + this.ui.checkbox.name = this.ui.checkbox.type === 'radio' + ? this.tree.selectableOpts.radioName + : this.attributes.name; + } + } + + bindEventListeners() { + this.ui.checkbox?.addEventListener('change', () => { + if (this.tree.selectableOpts.mode === 'nested') { + if (this.ui.checkbox.checked) { + this.selectChildren(); + } else { + this.deselectChildren(); + } + } + if (this.tree.config.varienSetHasChanges) { + window.varienElementMethods?.setHasChanges(this.ui.checkbox); + } + }); + this.ui.label?.addEventListener('dblclick', () => { + if (this.ui.details) { + if (this.ui.details.open) { + this.collapse() + } else { + this.expand(); + } + if (this.ui.checkbox && this.ui.checkbox.type !== 'radio') { + this.ui.checkbox.checked ? this.deselect() : this.select(); + } + } + }); + this.ui.details?.addEventListener('toggle', () => { + if (this.ui.details.open && !this.hasLoadedChildren) { + this.loadChildren(); + } + }); + } + + getUI() { + return this.ui; + } + + getPath() { + const parts = []; + let current = this; + while (current) { + parts.push(current.id); + current = current.parentNode; + } + return parts.reverse().join('/'); + } + + contains(node) { + return this.ui.wrap.contains(node.ui.wrap); + } + + get allowDrag() { + return this.attributes.allowDrag ?? true; + } + + get allowDrop() { + return this.attributes.allowDrop ?? true; + } + + get parentNode() { + const el = this.ui.wrap.parentElement?.closest('li'); + if (el) { + return this.tree.getNodeByEl(el); + } + } + + get previousNode() { + return this.tree.getNodeByEl(this.ui.wrap.previousSibling); + } + + get nextNode() { + return this.tree.getNodeByEl(this.ui.wrap.nextSibling); + } + + get childNodes() { + if (this.type !== 'folder') { + return []; + } + return Array.from(this.ui.ctNode?.children).map((el) => { + return this.tree.getNodeByEl(el); + }); + } + + toObject() { + if (this.type === 'folder') { + return { + ...this.attributes, + children: this.childNodes.map((child) => child.toObject()), + } + } + return this.attributes; + } + + async expand() { + if (this.ui.details) { + if (!this.hasLoadedChildren) { + await this.loadChildren(); + } + this.ui.details.open = true; + } + return this; + } + + async expandAll() { + await this.expand(); + await Promise.all(this.childNodes.map((child) => child.expandAll())) + } + + collapse() { + if (this.ui.details) { + this.ui.details.open = false; + } + } + + collapseAll() { + if (!this.isRoot || this.tree.config.showRootNode) { + this.collapse(); + } + this.childNodes.map((child) => child.collapseAll()); + } + + select() { + if (this.ui.checkbox && !(this.attributes.disabled ?? false)) { + this.ui.checkbox.indeterminate = false; + if (this.ui.checkbox.checked === false) { + this.ui.checkbox.checked = true; + this.ui.checkbox.dispatchEvent(new Event('change', { bubbles: true })); + } + } + } + + deselect() { + if (this.ui.checkbox && !(this.attributes.disabled ?? false)) { + this.ui.checkbox.indeterminate = false; + if (this.ui.checkbox.checked === true) { + this.ui.checkbox.checked = false; + this.ui.checkbox.dispatchEvent(new Event('change', { bubbles: true })); + } + } + } + + remove() { + this.ui.wrap.remove(); + } + + appendChild(node) { + if (this.type !== 'folder') { + throw new Error('Cannot add child to leaf node'); + } + this.ui.ctNode.append(node.ui.wrap); + } + + prependChild(node) { + if (this.type !== 'folder') { + throw new Error('Cannot add child to leaf node'); + } + this.ui.ctNode.prepend(node.ui.wrap); + } + + removeChild(node) { + if (this.type !== 'folder') { + throw new Error('Cannot remove child from leaf node'); + } + if (this.ui.ctNode.contains(node.ui.wrap)) { + node.ui.wrap.remove(); + } + } + + removeAllChildren() { + this.ui.ctNode.replaceChildren(); + } + + sortChildren() { + Array.from(this.ui.ctNode.children) + .sort((a, b) => a.dataset.text > b.dataset.text ? 1 : -1) + .forEach(node => this.ui.ctNode.appendChild(node)); + } + + selectChildren() { + this.ui.ctNode?.querySelectorAll('input[type=checkbox]').forEach((el) => { + el.checked = true; + el.indeterminate = false; + }); + } + + deselectChildren() { + this.ui.ctNode?.querySelectorAll('input[type=checkbox]').forEach((el) => { + el.checked = false; + el.indeterminate = false; + }); + } + + async loadChildren() { + if (!this.tree.lazyloadOpts.dataUrl) { + return; + } + + const timeoutID = setTimeout(() => { + this.ui.iconNode.classList.add('loading'); + }, LOADING_TIMEOUT); + + try { + const params = new URLSearchParams({ + [this.tree.lazyloadOpts.nodeParameter]: this.id, + }); + + if (typeof this.tree.lazyloadOpts.onBeforeLoad === 'function') { + await this.tree.lazyloadOpts.onBeforeLoad(this, params); + } + + const children = await mahoFetch(this.tree.lazyloadOpts.dataUrl, { + method: 'POST', + body: params, + }); + + for (const child of this.childNodes) { + if (child.isNew && !children.some((node) => node.id === child.id)) { + child.isNew = false; + } else { + child.remove(); + } + } + + for (const child of children) { + this.appendChild(new MahoTreeNode(this.tree, child)) + } + this.hasLoadedChildren = true; + + } catch (error) { + console.error('Error loading children:', error) + if (typeof this.tree.lazyloadOpts.onLoadException === 'function') { + this.tree.lazyloadOpts.onLoadException(this, error); + } + } + + clearTimeout(timeoutID) + this.ui.iconNode.classList.remove('loading'); + } +} + +/** + * MahoTreeNestedFolderPlugin - a Sortable.js plugin for dropping nodes onto folders + */ +class MahoTreeNestedFolderPlugin +{ + static pluginName = 'mahoTreeNestedFolder'; + static mounted = false; + static state = {}; + + constructor(sortable, el, options) { + this.dragEnter = this.dragEnter.bind(this); + this.defaults = { + dropClass: 'drop' + }; + } + + static mount() { + if (MahoTreeNestedFolderPlugin.mounted === false) { + MahoTreeNestedFolderPlugin.mounted = true; + Sortable.mount(MahoTreeNestedFolderPlugin); + } + } + + dragStart({ dragEl, cancel, originalEvent }) { + const dragNode = MahoTree.nodeDataMap.get(dragEl); + if (!dragNode.allowDrag) { + originalEvent.preventDefault(); + return cancel(); + } + MahoTreeNestedFolderPlugin.state = { dragNode, dropNode: null, hoverNode: null }; + window.addEventListener('dragenter', this.dragEnter); + } + + dragEnter(event) { + const state = MahoTreeNestedFolderPlugin.state; + if (state.dropNode && !state.dropNode.ui.label.contains(event.target)) { + state.dropNode.ui.wrap.classList.remove('drop'); + state.dropNode = null; + } + } + + dragOver({ target, originalEvent, fromSortable, cancel }) { + if (target.tagName === 'UL' && target.childNodes.length === 0) { + return cancel(); + } + + const state = MahoTreeNestedFolderPlugin.state; + + // The node we are currently hovering over, potential to become new dropNode + state.hoverNode = MahoTree.nodeDataMap.get(originalEvent.target.closest('li')); + if (!state.hoverNode || state.dropNode === state.hoverNode) { + return; + } + + if (target.tagName === 'UL' && !state.hoverNode.allowDrop) { + cancel(); + } + + if (target.tagName === 'LI' && !state.hoverNode.parentNode?.allowDrop) { + cancel(); + } + + state.dropNode?.ui.wrap.classList.remove(this.options.dropClass); + state.dropNode = null; + + if (!originalEvent.target.closest('.label')) { + return; + } + if (state.hoverNode.type !== 'folder' || !state.hoverNode.allowDrop) { + return; + } + if (state.hoverNode.contains(state.dragNode) || state.dragNode.contains(state.hoverNode)) { + return; + } + if (fromSortable.options.group.name !== Sortable.get(state.hoverNode.ui.ctNode).options.group.name) { + return; + } + + state.dropNode = state.hoverNode; + state.dropNode.ui.wrap.classList.add(this.options.dropClass); + } + + drop({ activeSortable, putSortable }) { + const state = MahoTreeNestedFolderPlugin.state; + if (!state.dropNode) { + return; + } + + if (state.dropNode.childNodes[0] === state.dragNode) { + return false; + } + + // Prepare the dragged node to be inserted + state.dragNode.isNew = true; + state.dropNode.ui.details.open = true; + + // Insert into new sortable and animate + const animateSortables = new Set([ putSortable || this.sortable, activeSortable ]); + + animateSortables.forEach((sortable) => sortable.captureAnimationState()); + state.dropNode.prependChild(state.dragNode); + animateSortables.forEach((sortable) => sortable.animateAll()); + + return false; + } + + nulling() { + const state = MahoTreeNestedFolderPlugin.state; + state.dropNode?.ui.wrap.classList.remove(this.options.dropClass); + window.removeEventListener('dragenter', this.dragEnter); + MahoTreeNestedFolderPlugin.state = {}; + } +} + +/** + * MahoTreeDropZonePlugin - a Sortable.js plugin for dropping nodes into a drop zone + */ +class MahoTreeDropZonePlugin +{ + static pluginName = 'mahoTreeDropZone'; + static mounted = false; + static state = {}; + + constructor(sortable, el, options) { + if (typeof options.mahoTreeDropZone === 'object' && options.mahoTreeDropZone !== null) { + this.dropZone = options.mahoTreeDropZone.dropZone; + this.dragEnter = this.dragEnter.bind(this); + if (typeof options.mahoTreeDropZone.onHover === 'function') { + this.onHover = options.mahoTreeDropZone.onHover; + } + if (typeof options.mahoTreeDropZone.onDrop === 'function') { + this.onDrop = options.mahoTreeDropZone.onDrop; + } + } + } + + static mount() { + if (MahoTreeDropZonePlugin.mounted === false) { + MahoTreeDropZonePlugin.mounted = true; + MahoTreeDropZonePlugin.dropMsg = document.createElement('div'); + Sortable.mount(MahoTreeDropZonePlugin); + } + } + + dragStart({ dragEl, cancel, originalEvent }) { + if (!this.dropZone || this.dropZone.contains(dragEl)) { + return; + } + const dragNode = MahoTree.nodeDataMap.get(dragEl); + MahoTreeDropZonePlugin.state = { dragNode, hovering: false }; + window.addEventListener('dragenter', this.dragEnter); + } + + dragEnter(event) { + const state = MahoTreeDropZonePlugin.state; + const dropMsg = MahoTreeDropZonePlugin.dropMsg; + if (this.dropZone.contains(event.target) === state.hovering) { + return; + } + state.hovering = !state.hovering; + this.dropZone.classList.toggle('hovering', state.hovering) + + if (state.hovering) { + const { icon, message } = this.onHover({ dragNode: state.dragNode }); + dropMsg.className = `drop-message ${icon}`; + dropMsg.textContent = message; + this.dropZone.prepend(dropMsg); + } else { + dropMsg.remove(); + } + } + + drop({ activeSortable, putSortable }) { + const state = MahoTreeDropZonePlugin.state; + if (state.hovering) { + const moveFn = this.onDrop({ ...arguments, dragNode: state.dragNode }); + if (typeof moveFn === 'function') { + const animateSortables = new Set([ putSortable || this.sortable, activeSortable ]); + + animateSortables.forEach((sortable) => sortable.captureAnimationState()); + moveFn(); + animateSortables.forEach((sortable) => sortable.animateAll()); + + return false; + } + } + } + + onHover({ dragNode }) { + return { icon: '', message: 'Drop' }; + } + + onDrop({ dragNode }) { + } + + nulling() { + this.dropZone.classList.remove('hovering'); + window.removeEventListener('dragenter', this.dragEnter); + MahoTreeDropZonePlugin.dropMsg.remove(); + MahoTreeDropZonePlugin.state = {}; + } +} diff --git a/public/js/sortable.min.js b/public/js/sortable.min.js new file mode 100644 index 000000000..95423a649 --- /dev/null +++ b/public/js/sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY args.shift() ?? match); + if (typeof Translator !== 'undefined') { + super(Translator.translate(message, ...args)); + } else { + super(formatted); + } + this.name = 'MahoError'; + this.originalMessage = message; + } +} + +/** + * @param {string} url - fetch url + * @param {Object} [options] - fetch options + * @param {Object} [options.loaderArea] - parameter to pass to showLoader(), false to disable + */ +async function mahoFetch(url, options) { + const { loaderArea, ...fetchOptions } = options ?? {}; + try { + if (loaderArea !== false && typeof showLoader === 'function') { + showLoader(loaderArea) + } + if (fetchOptions?.method?.toUpperCase() === 'POST') { + fetchOptions.body ??= new URLSearchParams(); + if (fetchOptions.body instanceof URLSearchParams || fetchOptions.body instanceof FormData) { + fetchOptions.body.set('form_key', fetchOptions.body.get('form_key') ?? FORM_KEY); + } + } + + url = new URL(url); + url.searchParams.set('isAjax', true); + + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new MahoError('Server returned status %s', response.status); + } + + const result = response.headers.get('Content-Type') === 'application/json' + ? await response.json() + : await response.text(); + + if (typeof result === 'object' && result !== null) { + if (result.error) { + throw new MahoError(result.message ?? result.error); + } else if (result.ajaxExpired && result.ajaxRedirect) { + setLocation(result.ajaxRedirect); + await new Promise((resolve) => {}); + } + } + if (loaderArea !== false && typeof hideLoader === 'function') { + hideLoader(); + } + return result; + + } catch (error) { + console.error('mahoFetch error:', error); + if (loaderArea !== false && typeof hideLoader === 'function') { + hideLoader(); + } + throw error; + } +} + function popWin(url,win,para) { var win = window.open(url,win,para); win.focus(); @@ -37,6 +110,66 @@ function parseSidUrl(baseUrl, urlExt) { return baseUrl+urlExt+sid; } +/** + * Generate a random string format [a-z0-9] + * + * @see {@link https://stackoverflow.com/a/47496558} + */ +function generateRandomString(length) { + if (length > 0) { + return [...Array(length)].map(() => Math.random().toString(36)[2]).join(''); + } + return ''; +} + +/** + * Alternative to PrototypeJS's string.escapeHTML() method + */ +function escapeHtml(str, escapeQuotes = false) { + const div = document.createElement('div'); + div.textContent = str; + return escapeQuotes + ? div.innerHTML.replaceAll('"', '"').replaceAll("'", ''') + : div.innerHTML; +} + +/** + * Alternative to PrototypeJS's string.unescapeHTML() method + */ +function unescapeHtml(str) { + const doc = new DOMParser().parseFromString(str, 'text/html'); + return doc.documentElement.textContent; +} + +/** + * Alternative to PrototypeJS's string.stripTags() method + */ +function stripTags(str) { + const div = document.createElement('div'); + div.innerHTML = str; + return div.textContent; +} + +/** + * Alternative to PrototypeJS's evalScripts option for Ajax.Updater + * + * Note that unlike Prototype, scripts will executed in the global scope + * + * @param {HTMLElement} targetEl - The element to update + * @param {string} html - The element's new HTML + * @param {boolean} executeExternalScripts - Whether to execute `` tags + * @see {@link https://stackoverflow.com/a/47614491} + * @see {@link http://api.prototypejs.org/ajax/Ajax/Updater/index.html} +*/ +function updateElementHtmlAndExecuteScripts(targetEl, html, executeExternalScripts = false) { + const range = document.createRange(); + const fragment = range.createContextualFragment(html); + if (!executeExternalScripts) { + fragment.querySelectorAll('script[src]').forEach(script => script.remove()); + } + targetEl.replaceChildren(fragment); +} + /** * Formats currency using patern * format - JSON (pattern, decimal, decimalsDelimeter, groupsDelimeter) @@ -572,21 +705,6 @@ function buttonDisabler() { }); } -function stripTags(str) { - const div = document.createElement('div'); - div.innerHTML = str; - return div.textContent; -} - -function updateElementHtmlAndExecuteScripts(targetEl, html, executeExternalScripts = false) { - const range = document.createRange(); - const fragment = range.createContextualFragment(html); - if (!executeExternalScripts) { - fragment.querySelectorAll('script[src]').forEach(script => script.remove()); - } - targetEl.replaceChildren(fragment); -} - const Calendar = {}; Calendar.setup = function(config) { const { inputField = '' } = config; diff --git a/public/skin/adminhtml/default/default/boxes.css b/public/skin/adminhtml/default/default/boxes.css index 44d8eedd3..1026eb357 100644 --- a/public/skin/adminhtml/default/default/boxes.css +++ b/public/skin/adminhtml/default/default/boxes.css @@ -5,7 +5,7 @@ * @package default_default * @copyright Copyright (c) 2006-2020 Magento, Inc. (https://magento.com) * @copyright Copyright (c) 2017-2023 The OpenMage Contributors (https://openmage.org) - * @copyright Copyright (c) 2024 Maho (https://mahocommerce.com) + * @copyright Copyright (c) 2024-2025 Maho (https://mahocommerce.com) * @license https://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ @@ -315,9 +315,6 @@ input.validation-failed, textarea.validation-failed { background:#fef0ed; border /* FORMS *******************************************************************************/ select.countries option { background-repeat:no-repeat; } -.entry-edit .fieldset .tree li, -.entry-edit .tree li { margin:0; } - /* Entry Edit */ /* Site-wide form fieldset */ table.form-edit { width:100%; } @@ -491,7 +488,6 @@ button.icon-btn span { text-indent:-999em; display:block; width:16px; padding:0; .side-col .switcher .link-store-scope { float:right; margin-right:-19px; margin-left:3px; } .side-col .switcher select { width:100%; float:left; } /*.side-col .switcher { margin-right:20px; margin-bottom:20px; }*/ -.catalog-categories .side-col .switcher { margin-right:0; margin-bottom:15px; } .link-store-scope { display:inline-block; vertical-align:middle; margin:0 0 1px; width:16px; height:16px; background:url(images/i_question-mark.png) 0 0 no-repeat; text-decoration:none !important; text-indent:-999em; overflow:hidden; } .store-scope .link-store-scope { float:left; margin-right:10px; } .store-scope .tree-store-scope { float:left; padding:7px 10px; border:1px dotted #dedede; } @@ -622,8 +618,6 @@ div.autocomplete ul li { padding:.5em .7em; min-height:32px; cursor:pointer; tex /* COLUMNS ********************************************************************************************/ -.catalog-categories .side-col { width:25em; padding-right:25px; } /* Catalog/Categories */ -.catalog-categories .main-col { padding-left:25px; margin-left:25em; } /* Catalog/Categories */ .order-summary .side-col { padding-right:25px; } /* Order/Create */ .order-summary .main-col { padding-left:25px; } /* Order/Create */ @@ -647,7 +641,6 @@ div.autocomplete ul li { padding:.5em .7em; min-height:32px; cursor:pointer; tex .content-header button, .filter-actions button { margin:0 0 0 5px; } .side-col .content-header { border-bottom:0; margin-right:12px; margin-bottom:.6em; } -.catalog-categories .side-col .content-header { margin-right:0; } /* Catalog/Categories */ .left-col-block { width:200px; } @@ -975,14 +968,6 @@ ul.item-options li { padding-left:.7em; } .no-active-category a span { color:#aaa !important; } -#tree-div { overflow:auto!important; padding-bottom:15px; width:200px; } - -.x-tree-node { margin:0 !important; } -.x-tree-node .leaf .x-tree-node-icon { background-image:url(images/fam_leaf.png); } -.x-tree-node .system-leaf .x-tree-node-icon { background-image:url(images/fam_application_form_delete.png); } - -.adminhtml-catalog-category-edit .x-tree-node-ct { overflow: visible; } - /* Product - Websites */ .website-name .checkbox { vertical-align:top; margin-top:2px; } .webiste-groups { padding:10px 20px; } @@ -993,7 +978,6 @@ ul.item-options li { padding-left:.7em; } .bundle-option-row table tbody td label { float:left; } .bundle-option-row input.option-label { width:50% !important; } .bundle-option-row input.option-position{ width:70px !important; } -.catalog-categories .side-col { width:240px; } /* Products - Tier Price */ .tier-price-input { margin-bottom:8px; } @@ -1013,9 +997,35 @@ ul.item-options li { padding-left:.7em; } .image-preview { position:absolute; cursor:pointer; } /* Attributes */ -.edit-attribute-set .form-list td.label { width:105px; } -.edit-attribute-set .form-list td.label label { width:105px; } -.edit-attribute-set .entry-edit fieldset input.input-text { width:200px; } +.edit-attribute-set { + display: flex; + gap: 28px; + height: 800px; +} +.edit-attribute-set > div { + flex-shrink: 0; +} +.edit-attribute-set-column { + display: flex; + flex-direction: column; + width: 320px; + padding-left: 27px; + border-left:1px solid #ddd; +} +.edit-attribute-set-column > div:last-child { + flex-grow: 1; +} +.edit-attribute-set .content-header { + display: flex; + justify-content: space-between; +} +.edit-attribute-set .content-header:after { + display: none; +} +.edit-attribute-set .form-list td.label { + width: 105px; +} + /* Review & Ratings */ .ratings { margin:0; } .rating-box { @@ -1044,9 +1054,10 @@ ul.item-options li { padding-left:.7em; } /* Price Rules */ .rule-tree ul { padding-left:16px !important; border-left:dotted 1px #888; } -.rule-tree .x-tree ul { padding-left:0 !important; border-left:none !important; } +.rule-tree ul.maho-tree, +.rule-tree ul.maho-tree ul { padding-left:0 !important; border-left:none !important; } .rule-param .label { font-weight:bold; color:black; } -.rule-param .label:hover { font-weight:bold; color:blue; } +.rule-param .label:hover { font-weight:bold; color:#0090FF; } .rule-param .label-disabled { color:black; cursor:default; text-decoration:none; } .rule-param .label-disabled:hover { color:black;} .rule-param .element { display:none; } @@ -1055,12 +1066,6 @@ ul.item-options li { padding-left:.7em; } .rule-param select.multiselect { vertical-align:top; } .rule-param-edit .label { display:none; } .rule-param-edit .element { display:inline; } -.rule-param-add { font-weight:normal; color:green; text-decoration:none; } -.rule-param-add:hover { font-weight:normal; color:blue; text-decoration:none; } -.rule-param-apply { font-weight:normal; color:green; text-decoration:none; } -.rule-param-apply:hover { font-weight:normal; color:blue; text-decoration:none; } -.rule-param-remove { font-weight:normal; color:red; text-decoration:none; } -.rule-param-remove:hover { font-weight:normal; color:blue; text-decoration:none; } .rule-chooser { border:solid 1px #CCC; margin:5px; padding:5px; display:none; } .rule-param-wait { padding-left:20px; background-image:url(images/loading.svg); background-repeat:no-repeat; background-position:0 50%; } @@ -1521,6 +1526,7 @@ dialog #contents .nm img { vertical-align:bottom; } .np { padding:0 !important; } .no-display { display:none; } .no-show { display:none; } +.inline-block { display:inline-block; } .nowrap, .nobr { white-space:nowrap; } .wrap { white-space:normal !important; } .no-float { float:none !important; } diff --git a/public/skin/adminhtml/default/default/override.css b/public/skin/adminhtml/default/default/override.css index 91cd5fc03..f1c150524 100644 --- a/public/skin/adminhtml/default/default/override.css +++ b/public/skin/adminhtml/default/default/override.css @@ -636,17 +636,6 @@ div.autocomplete ul li.selected { background-position: left center; } -.x-tree-node a { - font-size: 12px; -} -.x-tree-node a span { - color: #2f2f2f; -} -.x-tree-node .x-tree-selected a span { - background: lightgrey; - color: #2f2f2f; -} - .categories-side-col .tree-holder { margin-right: 0; } @@ -895,17 +884,6 @@ table.actions td { vertical-align: middle; } -.x-dd-drag-ghost a { - font-size: 12px; -} -.x-dd-drag-ghost a span { - color: #2f2f2f; -} - -.no-active-category a span { - color: #999 !important; -} - .dialog table.table_window { background: #fff; border: 0; @@ -936,10 +914,6 @@ table.actions td { width: 50px !important; } -.rule-param .label { - font-weight: 600; -} - a.rule-param-remove img { padding-left: 13px; box-sizing: border-box; @@ -1048,4 +1022,193 @@ ul.tabs a.changed span.changed { select:disabled option { color:#202856!important; } } +/* Maho Tree */ + +.maho-tree { + --indent: 1.25rem; + --spacing: 0.25rem; + --line-style: 1px dotted #aaa; + --outline-style: 2px solid #0090FF; + --marker-size: 10px; + --icon-size: 16px; + --label-gap: 0.25rem; + --disabled-color: #999; + --drop-color: #ccc; +} + +.hor-scroll .maho-tree { + white-space: nowrap; +} + +.maho-tree li { + position: relative; + margin: 0 !important; + margin-left: calc(var(--icon-size) / 2) !important; + padding-top: var(--spacing); + padding-bottom: var(--spacing); + padding-left: calc(var(--indent) - var(--icon-size) / 2); + border-left: var(--line-style); +} +.maho-tree li:before { + content: ""; + display: inline-block; + position: absolute; + top: 0; left: 0; + width: calc(var(--indent) - var(--icon-size) / 2 - var(--label-gap)); + height: calc(var(--spacing) + 0.5rem); + border-bottom: var(--line-style); +} +.maho-tree li:first-child { + padding-top: calc(2 * var(--spacing)); +} +.maho-tree li:first-child:before { + height: calc(2 * var(--spacing) + 0.5rem); +} +.maho-tree li:last-child { + padding-bottom: 0; + border-left-color: transparent; +} +.maho-tree li:last-child:before { + margin-left: -1px; + border-left: var(--line-style); +} + +.maho-tree li.sortable-drag, .maho-tree li.sortable-drag:before { + border-color: transparent; +} +.maho-tree li.sortable-drag .label { + outline: none !important; +} +.maho-tree li.sortable-drag ul { + visibility: hidden; +} + +.maho-tree .label { + display: flex; + flex-grow: 1; + gap: var(--label-gap); + user-select: none; + align-items: safe center; + width: unset !important; +} +.maho-tree li.drop > details > summary .label { + background: var(--drop-color); +} + +.maho-tree .label.disabled, .maho-tree .label:has(.icon.no-active-category) { + color: var(--disabled-color) !important; +} + +.maho-tree.hide-root-node > li { + margin: 0 !important; + padding: 0 !important; + border: none !important; +} +.maho-tree.hide-root-node > li:before { + display: none; +} +.maho-tree.hide-root-node > li > details > summary { + display: none; +} + +.maho-tree.hide-checkbox { + padding-right: 3px; + padding-bottom: 3px; +} +.maho-tree.hide-checkbox .label:has(input:is([type=checkbox], [type=radio])) { + cursor: pointer; +} +.maho-tree.hide-checkbox .label input:is([type=checkbox], [type=radio]) { + display: none; +} +.maho-tree.hide-checkbox .label:has(input:checked) { + outline: var(--outline-style); + outline-offset: 1px; +} + +.maho-tree summary { + display: flex; + align-items: stretch; + list-style: none; + cursor: pointer; +} +.maho-tree summary::-webkit-details-marker { + display: none; +} +.maho-tree summary::before { + content: ''; + position: absolute; + top: calc(var(--spacing) + 0.5rem - var(--marker-size) / 2); + left: calc(-1 * var(--marker-size) / 2); + width: var(--marker-size); + height: var(--marker-size); + background: url('data:image/svg+xml,') no-repeat; + background-size: contain; + user-select: none; +} +.maho-tree li:first-child > details > summary::before { + top: calc(2 * var(--spacing) + 0.5rem - var(--marker-size) / 2); +} +.maho-tree details[open] > summary::before { + background-image: url('data:image/svg+xml,'); +} + +.maho-tree .icon { + background-size: contain; + background-repeat: no-repeat; + width: var(--icon-size); + height: var(--icon-size); + flex-shrink: 0; +} +.maho-tree .icon.no-icon { + display: none; +} +.maho-tree .icon.folder { + background-image: url('data:image/svg+xml,'); +} +.maho-tree details[open] > summary .icon.folder { + background-image: url('data:image/svg+xml,'); +} +.maho-tree .icon.leaf, .maho-tree .icon.paper { + background-image: url('data:image/svg+xml,'); +} +.maho-tree .icon.system-leaf { + background-image: url('data:image/svg+xml,'); +} +.maho-tree .icon.configurable { + background-image: url('data:image/svg+xml,'); +} +.maho-tree .icon.loading { + background-image: url(images/loading.svg) !important; +} + +.drop-zone { + position: relative !important; +} +.drop-zone.hovering .maho-tree { + opacity: 0.2; +} +.drop-zone .drop-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + z-index: 1; + top: 0; bottom: 0; left: 0; right: 0; +} +.drop-zone .drop-message:before { + content: ''; + width: 48px; + height: 48px; + background-size: contain; + background-repeat: no-repeat; +} +.drop-zone .drop-message.delete:before { + background-image: url('data:image/svg+xml,'); +} +.drop-zone .drop-message.invalid:before { + background-image: url('data:image/svg+xml,'); +} + /*# sourceMappingURL=override.css.map */