├── Api └── TranslationRepositoryInterface.php ├── Block └── Adminhtml │ ├── Form │ ├── Field │ │ ├── Magento.php │ │ ├── Mapped.php │ │ └── YesNo.php │ ├── IntegrationKey.php │ └── Multiselect.php │ └── System │ └── Config │ └── Form │ └── Info.php ├── Model ├── Config.php ├── Config │ └── Source │ │ ├── CategoryList.php │ │ └── SynchronizationType.php ├── GetTranslationEntitiesList.php ├── GetTranslationEntity.php ├── Mapped.php ├── System │ └── Config │ │ └── Backend │ │ └── Mapped.php ├── TranslationRepository.php └── UpdateTranslationEntity.php ├── README.md ├── Setup └── InstallData.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ └── system.xml ├── config.xml ├── di.xml ├── integration │ └── api.xml ├── module.xml └── webapi.xml ├── registration.php └── view └── adminhtml ├── templates └── integration_key.phtml └── web ├── css └── chosen.min.css ├── images ├── chosen-sprite.png └── chosen-sprite@2x.png └── js └── chosen.jquery.min.js /Api/TranslationRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | translationEntity = $translationEntity; 29 | $this->config = $config; 30 | parent::__construct($context, $data); 31 | } 32 | 33 | /** 34 | * @param string $value 35 | * @return $this 36 | */ 37 | public function setInputName($value) 38 | { 39 | return $this->setName($value); 40 | } 41 | 42 | /** 43 | * Render block HTML 44 | * 45 | * @return string 46 | */ 47 | public function _toHtml() 48 | { 49 | if (!$this->getOptions()) { 50 | 51 | //$savedFields = $this->config->getMappedFields(); 52 | 53 | $this->addOption(0, ' '); 54 | 55 | foreach ($this->translationEntity->getTranslatableAttributes() as $key => $translatableAttributes) { 56 | $this->addOption( 57 | '', 58 | '---- ' . ucfirst($key) . ' ---', 59 | ['disabled' => true] 60 | ); 61 | 62 | foreach ($translatableAttributes as $translatableAttribute) { 63 | $params = []; 64 | // $params = isset($savedFields[$key .'/'. $translatableAttribute['code']]) ? ['disabled' => true] : []; 65 | 66 | $this->addOption( 67 | $key .'/'. $translatableAttribute['code'], 68 | ucfirst($key) . ' - ' . $translatableAttribute['label'], 69 | $params 70 | ); 71 | } 72 | } 73 | } 74 | 75 | return parent::_toHtml(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Block/Adminhtml/Form/Field/Mapped.php: -------------------------------------------------------------------------------- 1 | _groupRenderer) { 32 | $this->_groupRenderer = $this->createBlock(\Magefan\Crowdin\Block\Adminhtml\Form\Field\YesNo::class); 33 | $this->_groupRenderer->setClass('yesno_fields_select'); 34 | } 35 | 36 | return $this->_groupRenderer; 37 | } 38 | 39 | /** 40 | * @return \Magento\Framework\View\Element\BlockInterface 41 | * @throws \Magento\Framework\Exception\LocalizedException 42 | */ 43 | protected function _getTranslatableFiels() 44 | { 45 | if (!$this->magentoFieldsRenderer) { 46 | $this->magentoFieldsRenderer = $this->createBlock(\Magefan\Crowdin\Block\Adminhtml\Form\Field\Magento::class); 47 | $this->magentoFieldsRenderer->setClass('translatable_fields_select'); 48 | } 49 | return $this->magentoFieldsRenderer; 50 | } 51 | 52 | /** 53 | * @param $object 54 | * @return \Magento\Framework\View\Element\BlockInterface 55 | * @throws \Magento\Framework\Exception\LocalizedException 56 | */ 57 | protected function createBlock($object) 58 | { 59 | return $this->getLayout()->createBlock( 60 | $object, 61 | '', 62 | ['data' => 63 | ['is_render_to_js_template' => true] 64 | ]); 65 | } 66 | 67 | /** 68 | * Prepare to render 69 | * 70 | * @return void 71 | * @throws \Magento\Framework\Exception\LocalizedException 72 | */ 73 | protected function _prepareToRender() 74 | { 75 | 76 | $this->addColumn( 77 | 'magento_translatable_fields', 78 | [ 79 | 'label' => __('Translatable Field'), 80 | 'renderer' => $this->_getTranslatableFiels() 81 | ] 82 | ); 83 | $this->addColumn( 84 | 'is_crowdin_sync_enabled', 85 | [ 86 | 'label' => __('Enable Sync'), 87 | 'renderer' => $this->_getYesNoFields() 88 | ] 89 | ); 90 | 91 | $this->_addAfter = false; 92 | $this->_addButtonLabel = __('Add'); 93 | } 94 | 95 | /** 96 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 97 | * @return bool 98 | */ 99 | protected function _isInheritCheckboxRequired(\Magento\Framework\Data\Form\Element\AbstractElement $element) 100 | { 101 | return false; 102 | } 103 | 104 | /** 105 | * Prepare existing row data object 106 | * 107 | * @param \Magento\Framework\DataObject $row 108 | * @return void 109 | * @throws \Magento\Framework\Exception\LocalizedException 110 | */ 111 | protected function _prepareArrayRow(\Magento\Framework\DataObject $row) 112 | { 113 | $optionExtraAttr = []; 114 | 115 | $optionExtraAttr['option_' . $this->_getTranslatableFiels()->calcOptionHash($row->getData('magento_translatable_fields'))] = 'selected="selected"'; 116 | $optionExtraAttr['option_' . $this->_getYesNoFields()->calcOptionHash($row->getData('is_crowdin_sync_enabled'))] = 'selected="selected"'; 117 | 118 | $row->setData('option_extra_attrs', $optionExtraAttr); 119 | } 120 | 121 | /** 122 | * @return \Magento\Framework\Phrase|string 123 | * @throws \Exception 124 | */ 125 | protected function _toHtml() 126 | { 127 | try { 128 | return parent::_toHtml(); 129 | } catch (\Mautic\Exception\RequiredParameterMissingException $e) { 130 | return __( 131 | 'Cannot Display Translatable Fields.', 132 | $this->escapeHtml($e->getMessage()) 133 | ); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Block/Adminhtml/Form/Field/YesNo.php: -------------------------------------------------------------------------------- 1 | 'yes', 43 | 0 => 'no' 44 | ]; 45 | } 46 | 47 | /** 48 | * @param string $value 49 | * @return $this 50 | */ 51 | public function setInputName($value) 52 | { 53 | return $this->setName($value); 54 | } 55 | 56 | /** 57 | * @return string 58 | * @throws \Mautic\Exception\RequiredParameterMissingException 59 | */ 60 | protected function _toHtml() 61 | { 62 | if (!$this->getOptions()) { 63 | foreach ($this->_getYesNoFields() as $groupId => $groupLabel) { 64 | $this->addOption($groupId, addslashes($groupLabel)); 65 | } 66 | } 67 | 68 | return parent::_toHtml(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Block/Adminhtml/Form/IntegrationKey.php: -------------------------------------------------------------------------------- 1 | integrationService = $integrationService; 41 | } 42 | 43 | /** 44 | * Set template to itself 45 | * 46 | * @return $this 47 | */ 48 | protected function _prepareLayout() 49 | { 50 | parent::_prepareLayout(); 51 | if (!$this->getTemplate()) { 52 | $this->setTemplate(static::BUTTON_TEMPLATE); 53 | } 54 | return $this; 55 | } 56 | 57 | /** 58 | * Render button 59 | * 60 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 61 | * @return string 62 | */ 63 | public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) 64 | { 65 | $element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); 66 | return parent::render($element); 67 | } 68 | 69 | /** 70 | * 71 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 72 | * @return string 73 | */ 74 | protected function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) 75 | { 76 | return $this->_toHtml() . $this->getIntegrationKey(); 77 | 78 | } 79 | 80 | /** 81 | * @return bool 82 | */ 83 | public function isIntegrationActivated(): bool 84 | { 85 | return (bool)$this->getIntegration()->getStatus(); 86 | } 87 | 88 | /** 89 | * @return mixed 90 | */ 91 | private function getIntegration() 92 | { 93 | if (is_null($this->integration)) { 94 | $this->integration = $this->integrationService->get($this->integrationService->findByName(Config::INTEGRATION_NAME)->getId()); 95 | } 96 | 97 | return $this->integration; 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | private function getIntegrationKey(): string 104 | { 105 | $result = ''; 106 | $integrationId = $this->integrationService->findByName(Config::INTEGRATION_NAME)->getId(); 107 | $integration = $this->integrationService->get($integrationId); 108 | 109 | if ($this->isIntegrationActivated()) { 110 | $key = implode( 111 | '-', 112 | [ 113 | $integration->getData('consumer_key'), 114 | $integration->getData('consumer_secret'), 115 | $integration->getData('token'), 116 | $integration->getData('token_secret'), 117 | ] 118 | ); 119 | 120 | $result = ''; 121 | } 122 | 123 | return $result; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Block/Adminhtml/Form/Multiselect.php: -------------------------------------------------------------------------------- 1 | getViewFileUrl('Magefan_Crowdin::css/chosen.min.css') . "\"> 23 | 27 | "; 37 | } 38 | } -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Form/Info.php: -------------------------------------------------------------------------------- 1 | moduleList = $moduleList; 32 | } 33 | 34 | /** 35 | * Return info block html 36 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 37 | * @return string 38 | */ 39 | public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) 40 | { 41 | $useUrl = true; 42 | $m = $this->moduleList->getOne($this->getModuleName()); 43 | $html = '
44 | ' . $this->escapeHtml($this->getModuleTitle()) . ' v' . $this->escapeHtml($m['setup_version']) . ' was developed by '; 45 | if ($useUrl) { 46 | $html .= 'Magefan'; 47 | } else { 48 | $html .= 'Magefan'; 49 | } 50 | 51 | $html .= '

'; 52 | $html .= 'Crowdin'; 53 | $html .= ' is a cloud-based localization management software with 2M+ user accounts and 130K+ localization projects that helps teams and companies reach new markets. People from over 160 countries use Crowdin to translate & localize their software, mobile and desktop apps, games, websites, help articles, marketing communication, and other content.'; 54 | $html .= '
'; 55 | 56 | return $html; 57 | } 58 | 59 | /** 60 | * Return extension url 61 | * @return string 62 | */ 63 | protected function getModuleUrl() 64 | { 65 | return 'https://mage' . 'fan.com/magento2-extensions?utm_source=m2admin_crowdin_config&utm_medium=link&utm_campaign=regular'; 66 | } 67 | 68 | /** 69 | * Return extension title 70 | * @return string 71 | */ 72 | protected function getModuleTitle() 73 | { 74 | return 'Crowdin Integration Extension'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Model/Config.php: -------------------------------------------------------------------------------- 1 | scopeConfig = $scopeConfig; 42 | } 43 | 44 | /** 45 | * Retrieve true if module is enabled 46 | * 47 | * @return bool 48 | */ 49 | public function isEnabled($storeId = null): bool 50 | { 51 | return (bool)$this->getConfig(self::XML_PATH_EXTENSION_ENABLED, $storeId); 52 | } 53 | 54 | /** 55 | * @param null $storeId 56 | * @return array 57 | */ 58 | public function getMappedFields($storeId = null): array 59 | { 60 | return (array)json_decode((string)$this->getConfig(self::XML_PATH_FIELDS_MAPPING, $storeId), true); 61 | } 62 | 63 | public function getLocaleByStoreId($storeId = null) 64 | { 65 | return (string) $this->getConfig('general/locale/code', $storeId); 66 | } 67 | 68 | /** 69 | * Retrieve store config value 70 | * @param string $path 71 | * @param null $storeId 72 | * @return mixed 73 | */ 74 | public function getConfig($path, $storeId = null) 75 | { 76 | return $this->scopeConfig->getValue( 77 | $path, 78 | ScopeInterface::SCOPE_STORE, 79 | $storeId 80 | ); 81 | } 82 | 83 | /** 84 | * @param null $storeId 85 | * @return int 86 | */ 87 | public function getCatalogSynchronizationOption($storeId = null): int 88 | { 89 | return (int)$this->getConfig(self::XML_PATH_CATALOG_SYNCHRONIZATION, $storeId); 90 | } 91 | 92 | /** 93 | * @param null $storeId 94 | * @return array 95 | */ 96 | public function getCatalogSynchronizationValues($storeId = null) 97 | { 98 | return explode(',', $this->getConfig(self::XML_PATH_CATALOG_SPECIFIC_SYNCHRONIZATION, $storeId)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Model/Config/Source/CategoryList.php: -------------------------------------------------------------------------------- 1 | categoryFactory = $categoryFactory; 39 | $this->categoryCollectionFactory = $categoryCollectionFactory; 40 | } 41 | 42 | /** 43 | * @return array 44 | * @throws LocalizedException 45 | */ 46 | public function toOptionArray() 47 | { 48 | $preparedDataArray = $this->toArray(); 49 | $options = []; 50 | foreach ($preparedDataArray as $key => $value) { 51 | $options[] = [ 52 | 'value' => $key, 53 | 'label' => $value 54 | ]; 55 | } 56 | 57 | return $options; 58 | } 59 | 60 | /** 61 | * @return array 62 | * @throws LocalizedException 63 | */ 64 | public function toArray() 65 | { 66 | $categories = $this->getCategoryCollection(); 67 | 68 | $categoryList = []; 69 | foreach ($categories as $category) { 70 | $categoryList[$category->getEntityId()] = [ 71 | 'name' => $category->getName(), 72 | 'path' => $category->getPath(), 73 | 'cat_id' => $category->getId() 74 | ]; 75 | } 76 | 77 | $categoryArray = []; 78 | foreach ($categoryList as $key => $value) { 79 | if ($path = $this->getCategoryPath($value['path'], $categoryList)) { 80 | $categoryArray[$key] = $path . ' (ID: ' . $value['cat_id'] . ')'; 81 | } 82 | } 83 | 84 | asort($categoryArray); 85 | 86 | return $categoryArray; 87 | } 88 | 89 | /** 90 | * @return Collection 91 | * @throws LocalizedException 92 | */ 93 | public function getCategoryCollection() 94 | { 95 | $collection = $this->categoryCollectionFactory->create(); 96 | $collection->addAttributeToSelect(['path', 'name']); 97 | 98 | return $collection; 99 | } 100 | 101 | /** 102 | * @param $path 103 | * @param $categoryList 104 | * 105 | * @return string 106 | */ 107 | public function getCategoryPath($path, $categoryList) 108 | { 109 | $categoryPath = []; 110 | $rootCategories = [1, 2]; 111 | $path = explode('/', $path); 112 | 113 | if ($path) { 114 | foreach ($path as $categoryId) { 115 | if (!in_array($categoryId, $rootCategories)) { 116 | if (!empty($categoryList[$categoryId]['name'])) { 117 | $categoryPath[] = $categoryList[$categoryId]['name']; 118 | } 119 | } 120 | } 121 | } 122 | 123 | if (!empty($categoryPath)) { 124 | return implode(' » ', $categoryPath); 125 | } 126 | 127 | return false; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Model/Config/Source/SynchronizationType.php: -------------------------------------------------------------------------------- 1 | 0, 'label' => __(' All (default)')], 18 | ['value' => 1, 'label' => __('Specific categories and products')], 19 | ['value' => 2, 'label' => __('All except specific categories and products')] 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Model/GetTranslationEntitiesList.php: -------------------------------------------------------------------------------- 1 | self::CATEGORY_ENTITY, 51 | self::PRODUCTS => self::PRODUCT_ENTITY, 52 | self::EMAILS => self::EMAIL_ENTITY, 53 | self::CMS_PAGES => self::CMS_PAGE_ENTITY, 54 | self::CMS_BLOCKS => self::CMS_BLOCK_ENTITY, 55 | self::PRODUCT_ATTRIBUTES => self::PRODUCT_ATTRIBUTE_ENTITY, 56 | ]; 57 | 58 | /** 59 | * @var ProductRepository 60 | */ 61 | protected $productRepository; 62 | 63 | /** 64 | * @var EmailCollectionFactory 65 | */ 66 | protected $emailCollectionFactory; 67 | 68 | /** 69 | * @var SearchCriteriaBuilder 70 | */ 71 | protected $searchCriteriaBuilder; 72 | 73 | /** 74 | * @var FilterBuilder 75 | */ 76 | protected $filterBuilder; 77 | 78 | /** 79 | * @var Attribute 80 | */ 81 | protected $attributeFactory; 82 | 83 | /** 84 | * @var CategoryAttributeRepositoryInterface 85 | */ 86 | protected $categoryAttributeRepository; 87 | 88 | /** 89 | * @var ProductAttributeRepositoryInterface 90 | */ 91 | protected $productAttributeRepository; 92 | 93 | /** 94 | * @var FilterGroupBuilder 95 | */ 96 | protected $filterGroupBuilder; 97 | 98 | /** 99 | * @var PageRepositoryInterface 100 | */ 101 | protected $pageRepository; 102 | 103 | /** 104 | * @var BlockRepositoryInterface 105 | */ 106 | protected $blockRepository; 107 | 108 | /** 109 | * @var Config 110 | */ 111 | private $config; 112 | 113 | /** 114 | * @var \string[][] 115 | */ 116 | private $entitiesList = [ 117 | [ 118 | 'name' => 'Products', 119 | 'id' => self::PRODUCTS 120 | ], 121 | [ 122 | 'name' => 'Email Templates', 123 | 'id' => self::EMAILS 124 | ], 125 | [ 126 | 'name' => 'Product Attributes', 127 | 'id' => self::PRODUCT_ATTRIBUTES 128 | ], 129 | [ 130 | 'name' => 'CMS Pages', 131 | 'id' => self::CMS_PAGES 132 | ], 133 | [ 134 | 'name' => 'CMS Block', 135 | 'id' => self::CMS_BLOCKS 136 | ] 137 | ]; 138 | 139 | /** 140 | * @param CategoryCollectionFactory $categoryCollectionFactory 141 | * @param ProductRepository $productRepository 142 | * @param EmailCollectionFactory $emailCollectionFactory 143 | * @param SearchCriteriaBuilder $searchCriteriaBuilder 144 | * @param FilterBuilder $filterBuilder 145 | * @param Attribute $attributeFactory 146 | * @param CategoryAttributeRepositoryInterface $categoryAttributeRepository 147 | * @param ProductAttributeRepositoryInterface $productAttributeRepository 148 | * @param FilterGroupBuilder $filterGroupBuilder 149 | * @param PageRepositoryInterface $pageRepository 150 | * @param BlockRepositoryInterface $blockRepository 151 | * @param Config $config 152 | */ 153 | public function __construct( 154 | CategoryCollectionFactory $categoryCollectionFactory, 155 | ProductRepository $productRepository, 156 | EmailCollectionFactory $emailCollectionFactory, 157 | SearchCriteriaBuilder $searchCriteriaBuilder, 158 | FilterBuilder $filterBuilder, 159 | Attribute $attributeFactory, 160 | CategoryAttributeRepositoryInterface $categoryAttributeRepository, 161 | ProductAttributeRepositoryInterface $productAttributeRepository, 162 | FilterGroupBuilder $filterGroupBuilder, 163 | PageRepositoryInterface $pageRepository, 164 | BlockRepositoryInterface $blockRepository, 165 | Config $config 166 | ) { 167 | $this->categoryCollectionFactory = $categoryCollectionFactory; 168 | $this->productRepository = $productRepository; 169 | $this->emailCollectionFactory = $emailCollectionFactory; 170 | $this->searchCriteriaBuilder = $searchCriteriaBuilder; 171 | $this->filterBuilder = $filterBuilder; 172 | $this->attributeFactory = $attributeFactory; 173 | $this->categoryAttributeRepository = $categoryAttributeRepository; 174 | $this->productAttributeRepository = $productAttributeRepository; 175 | $this->filterGroupBuilder = $filterGroupBuilder; 176 | $this->pageRepository = $pageRepository; 177 | $this->blockRepository = $blockRepository; 178 | $this->config = $config; 179 | } 180 | 181 | /** 182 | * @return array 183 | */ 184 | public function execute($storeId): array 185 | { 186 | $this->storeId = $storeId; 187 | 188 | $this->addCategoriesEntities(); 189 | $this->addProductsEntities(); 190 | $this->addEmailsEntities(); 191 | $this->addCmsPagesEntities(); 192 | $this->addCmsBlocksEntities(); 193 | $this->addProductAttributesEntities(); 194 | 195 | return (array) $this->entitiesList; 196 | } 197 | 198 | private function addCategoriesEntities() 199 | { 200 | 201 | $categoryCollection = $this->categoryCollectionFactory->create()->addAttributeToSelect('name'); 202 | 203 | if ($option = $this->config->getCatalogSynchronizationOption()){ 204 | 205 | $categoriesIds = $this->config->getCatalogSynchronizationValues(); 206 | $categoryCollection->addAttributeToFilter('entity_id', array($option != 1 ? 'nin' : 'in' => $categoriesIds)); 207 | } 208 | 209 | foreach ($categoryCollection as $item) { 210 | $isRootCategory = ((int)$item->getLevel() === 0); 211 | $parentId = (((int)$item->getLevel()) === 1) ? self::CATEGORY_ENTITY : self::CATEGORY_ENTITY. '_' . $item->getParentId(); 212 | 213 | $category = [ 214 | 'name' => $isRootCategory ? __('Categories') : $item->getName(), 215 | 'id' => $isRootCategory ? self::CATEGORY_ENTITY : self::CATEGORY_ENTITY . '_' . $item->getId(), 216 | 'parentId' => $parentId, 217 | 'type' => 'html' 218 | ]; 219 | 220 | if ($isRootCategory) { 221 | unset($category['parentId']); 222 | } 223 | 224 | $this->entitiesList[] = $category; 225 | } 226 | } 227 | 228 | private function addProductsEntities() 229 | { 230 | $filters = []; 231 | $filters[] = $this->filterBuilder 232 | ->setField('store_id') 233 | ->setConditionType('eq') 234 | ->setValue(0) 235 | ->create(); 236 | 237 | if ($option = $this->config->getCatalogSynchronizationOption()){ 238 | 239 | $categoriesIds = $this->config->getCatalogSynchronizationValues(); 240 | $categoryFilter = $this->filterBuilder->setField('category_id') 241 | ->setValue($categoriesIds) 242 | ->setConditionType($option != 1 ? 'nin' : 'in') 243 | ->create(); 244 | $filters[] = $categoryFilter; 245 | } 246 | 247 | $searchCriteria = $this->searchCriteriaBuilder->addFilters($filters)->create(); 248 | 249 | $this->addEntityToResponse(self::PRODUCTS, $this->productRepository->getList($searchCriteria)->getItems()); 250 | } 251 | 252 | /** 253 | * @param string $type 254 | * @param $collection 255 | */ 256 | private function addEntityToResponse(string $type, $collection) 257 | { 258 | foreach ($collection as $item) { 259 | $itemId = self::ENTITY_TYPES[$type] . '_' . $item->getId(); 260 | $this->entitiesList[] = [ 261 | 'name' => $item->getName(), 262 | 'id' => $itemId, 263 | 'parentId' => $type, 264 | 'type' => 'html', 265 | ]; 266 | } 267 | } 268 | 269 | private function addEmailsEntities() 270 | { 271 | $emailCollection = $this->emailCollectionFactory->create(); 272 | 273 | foreach ($emailCollection->getItems() as $templates) { 274 | $this->entitiesList[] = [ 275 | 'name' => $templates->getTemplateCode(), 276 | 'id' => self::EMAIL_ENTITY . '_' . $templates->getTemplateId(), 277 | 'parentId' => self::EMAILS, 278 | 'type' => 'html' 279 | ]; 280 | } 281 | } 282 | 283 | private function addCmsPagesEntities() 284 | { 285 | $this->addCmsEntity($this->pageRepository, self::CMS_PAGE_ENTITY, self::CMS_PAGES); 286 | } 287 | 288 | private function addCmsBlocksEntities() 289 | { 290 | $this->addCmsEntity($this->blockRepository, self::CMS_BLOCK_ENTITY, self::CMS_BLOCKS); 291 | } 292 | 293 | private function addCmsEntity($repository, $cmsEntityType, $parentId) 294 | { 295 | $cmsEntitiesForAllStores = $this->getCmsEntitiesByStore($repository, 0); 296 | $cmsEntitiesForCurrentStore = $this->getCmsEntitiesByStore($repository, $this->storeId); 297 | $cmsEntities = []; 298 | 299 | foreach (array_merge($cmsEntitiesForAllStores, $cmsEntitiesForCurrentStore) as $cmsEntity) { 300 | $cmsEntities[$cmsEntity->getIdentifier()] = [ 301 | 'name' => $cmsEntity->getTitle(), 302 | 'id' => $cmsEntityType . '_' . $cmsEntity->getIdentifier(), 303 | 'parentId' => $parentId, 304 | 'type' => 'html' 305 | ]; 306 | } 307 | 308 | foreach ($cmsEntities as $cmsEntity) { 309 | $this->entitiesList[] = $cmsEntity; 310 | } 311 | } 312 | 313 | /** 314 | * @param $repository 315 | * @param $storeId 316 | * @return mixed 317 | */ 318 | private function getCmsEntitiesByStore($repository, $storeId) 319 | { 320 | $this->searchCriteriaBuilder->addFilters( 321 | [ 322 | $this->filterBuilder 323 | ->setField('store_id') 324 | ->setConditionType('eq') 325 | ->setValue($storeId) 326 | ->create() 327 | ] 328 | ); 329 | 330 | return $repository->getList($this->searchCriteriaBuilder->create())->getItems(); 331 | } 332 | 333 | private function addProductAttributesEntities() 334 | { 335 | //$productAttributesList = $this->productAttributeRepository->getList($this->searchCriteriaBuilder->create())->getItems(); 336 | $this->getProductAttributes($this->productAttributeRepository); 337 | } 338 | 339 | /** 340 | * Return attributes that can be translated and not already in product entity 341 | * @param $repository 342 | */ 343 | public function getProductAttributes($repository) 344 | { 345 | $inputFilter = $this->filterBuilder 346 | ->setField('frontend_input') 347 | ->setConditionType('in') 348 | ->setValue(['multiselect', 'select']) 349 | ->create(); 350 | 351 | $isUserDefinedFilter = $this->filterBuilder 352 | ->setField('is_user_defined') 353 | ->setConditionType('eq') 354 | ->setValue(1) 355 | ->create(); 356 | 357 | $inputGroup = $this->filterGroupBuilder->addFilter($inputFilter)->create(); 358 | $isUserDefinedGroup = $this->filterGroupBuilder->addFilter($isUserDefinedFilter)->create(); 359 | 360 | $searchCriteria = $this->searchCriteriaBuilder->setFilterGroups([$inputGroup, $isUserDefinedGroup])->create(); 361 | $attributesList = $repository->getList($searchCriteria)->getItems(); 362 | 363 | foreach ($attributesList as $attribute) { 364 | $this->entitiesList[] = [ 365 | 'name' => $attribute->getDefaultFrontendLabel() ?: $attribute->getAttributeCode() . ' [label not set]', 366 | 'id' => self::PRODUCT_ATTRIBUTE_ENTITY . '_' . $attribute->getId(), 367 | 'parentId' => self::PRODUCT_ATTRIBUTES, 368 | 'type' => 'html' 369 | ]; 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /Model/GetTranslationEntity.php: -------------------------------------------------------------------------------- 1 | categoryCollectionFactory = $categoryCollectionFactory; 163 | $this->searchCriteriaBuilder = $searchCriteriaBuilder; 164 | $this->filterBuilder = $filterBuilder; 165 | $this->attributeFactory = $attributeFactory; 166 | $this->categoryAttributeRepository = $categoryAttributeRepository; 167 | $this->productAttributeRepository = $productAttributeRepository; 168 | $this->filterGroupBuilder = $filterGroupBuilder; 169 | $this->categoryRepository = $categoryRepository; 170 | $this->productRepository = $productRepository; 171 | $this->storeManager = $storeManager; 172 | $this->store = $store; 173 | $this->config = $config; 174 | $this->emailTemplate = $emailTemplate; 175 | $this->pageRepository = $pageRepository; 176 | $this->blockRepository = $blockRepository; 177 | $this->attributeOptionManagement = $attributeOptionManagement; 178 | $this->emulation = $emulation; 179 | } 180 | 181 | /** 182 | * @param $id 183 | * @return array|void 184 | */ 185 | public function execute($id) 186 | { 187 | $response = ['success' => true]; 188 | $entityData = explode('_', $id); 189 | $type = $entityData[0]; 190 | 191 | unset($entityData[0]); 192 | 193 | $id = implode('_', $entityData); 194 | 195 | if (trim((string)$id) === '') { 196 | return ['message' => 'Missed entity id!', 'success' => false]; 197 | } 198 | 199 | if (!in_array($type, $this->allowedStringId) && !is_numeric($id)) { 200 | return ['message' => 'Entity id (' . $id . ') for this entity should be numeric!', 'success' => false]; 201 | } 202 | 203 | switch ($type) { 204 | case GetTranslationEntitiesList::CATEGORY_ENTITY: 205 | $response = $this->getCategoryData((int)$id); 206 | break; 207 | case GetTranslationEntitiesList::PRODUCT_ENTITY: 208 | $response = $this->getProductData((int)$id); 209 | break; 210 | case GetTranslationEntitiesList::EMAIL_ENTITY: 211 | $response = $this->getEmailTemplateData((int)$id); 212 | break; 213 | case GetTranslationEntitiesList::CMS_PAGE_ENTITY: 214 | $response = $this->getCmsPageData($id); 215 | break; 216 | case GetTranslationEntitiesList::CMS_BLOCK_ENTITY: 217 | $response = $this->getCmsBlockData($id); 218 | break; 219 | case GetTranslationEntitiesList::PRODUCT_ATTRIBUTE_ENTITY: 220 | $response = $this->getProductAttributeData((int)$id); 221 | break; 222 | } 223 | 224 | return $response; 225 | } 226 | 227 | /** 228 | * @param int $id 229 | * @return array 230 | */ 231 | private function getCategoryData(int $id): array 232 | { 233 | $data = []; 234 | 235 | $attributes = $this->removeNotAllowedFields($this->getCatalogAttributes($this->categoryAttributeRepository), 'category'); 236 | 237 | foreach ($this->storeManager->getStores() as $store) { 238 | if (!$store->getIsActive()) { 239 | continue; 240 | } 241 | 242 | $storeId = $store->getId(); 243 | $category = $this->categoryRepository->get($id, $store->getId()); 244 | 245 | foreach ($attributes as $attribute) { 246 | $data[$storeId][$attribute['code']] = $category->getData($attribute['code']); 247 | } 248 | 249 | $data[$storeId]['store_id'] = $storeId; 250 | } 251 | 252 | return $data; 253 | } 254 | 255 | /** 256 | * @return array 257 | */ 258 | public function getTranslatableAttributes(): array 259 | { 260 | $attributes = []; 261 | $attributes['category'] = $this->getCatalogAttributes($this->categoryAttributeRepository); 262 | $attributes['product'] = $this->getCatalogAttributes($this->productAttributeRepository); 263 | $attributes['email'] = $this->getEmailAttributes(); 264 | $attributes['page'] = $this->getCmsAttributesByType('page'); 265 | $attributes['block'] = $this->getCmsAttributesByType('block'); 266 | 267 | return $attributes; 268 | } 269 | 270 | /** 271 | * @param int $id 272 | * @return array 273 | */ 274 | private function getProductData(int $id): array 275 | { 276 | $data = []; 277 | 278 | $attributes = $this->removeNotAllowedFields($this->getCatalogAttributes($this->productAttributeRepository), 'product'); 279 | 280 | foreach ($this->storeManager->getStores() as $store) { 281 | if (!$store->getIsActive()) { 282 | continue; 283 | } 284 | 285 | $storeId = $store->getId(); 286 | $product = $this->productRepository->getById($id, false, $storeId); 287 | 288 | foreach ($attributes as $attribute) { 289 | $data[$storeId][$attribute['code']] = $product->getData($attribute['code']); 290 | } 291 | 292 | $data[$storeId]['store_id'] = $storeId; 293 | } 294 | 295 | return $data; 296 | } 297 | 298 | /** 299 | * @param int $id 300 | * @return array 301 | */ 302 | private function getEmailTemplateData(int $id): array 303 | { 304 | $data = []; 305 | $attributes = $this->removeNotAllowedFields($this->getEmailAttributes(), 'email'); 306 | 307 | $emailTemplate = $this->emailTemplate->load($id); 308 | 309 | if (!$emailTemplate->getId()) { 310 | return ['message' => __('Entity with id %1 does not exist!', $id), 'success' => false]; 311 | } 312 | 313 | foreach ($this->storeManager->getStores() as $store) { 314 | if (!$store->getIsActive()) { 315 | continue; 316 | } 317 | 318 | $storeId = $store->getId(); 319 | 320 | foreach ($attributes as $attribute) { 321 | $data[$storeId][$attribute['code']] = $emailTemplate->getData($attribute['code']); 322 | } 323 | 324 | $data[$storeId]['store_id'] = $storeId; 325 | } 326 | 327 | return $data; 328 | } 329 | 330 | /** 331 | * @param string $identifier 332 | * @return array 333 | */ 334 | private function getCmsPageData(string $identifier): array 335 | { 336 | return $this->getCmsEntityData($this->pageRepository, 'page', $identifier); 337 | } 338 | 339 | /** 340 | * @param string $identifier 341 | * @return array 342 | */ 343 | private function getCmsBlockData(string $identifier): array 344 | { 345 | return $this->getCmsEntityData($this->blockRepository, 'block', $identifier); 346 | } 347 | 348 | /** 349 | * @param $repository 350 | * @param string $entityType 351 | * @param string $identifier 352 | * @return array 353 | */ 354 | private function getCmsEntityData($repository, string $entityType, string $identifier): array 355 | { 356 | 357 | $stores = [0]; 358 | $data = []; 359 | $attributes = $this->removeNotAllowedFields($this->getCmsAttributesByType($entityType), $entityType); 360 | 361 | foreach ($this->storeManager->getStores() as $store) { 362 | //if ($store->getIsActive()) 363 | $stores[$store->getId()] = $store->getId(); 364 | } 365 | 366 | foreach ($stores as $storeId) { 367 | $cmsEntity = $this->getCmsEntityByIdentifier($identifier, $repository, $storeId); 368 | 369 | if (!$cmsEntity) { 370 | if (isset($data[0])) { 371 | $data[$storeId] = $data[0]; 372 | } 373 | 374 | continue; 375 | } 376 | 377 | foreach ($attributes as $attribute) { 378 | $data[$storeId][$attribute['code']] = $cmsEntity->getData($attribute['code']); 379 | } 380 | 381 | $data[$storeId]['store_id'] = $storeId; 382 | } 383 | 384 | return $data; 385 | } 386 | 387 | /** 388 | * @param $identifier 389 | * @param $repository 390 | * @param $storeId 391 | * @return false|mixed 392 | */ 393 | private function getCmsEntityByIdentifier($identifier, $repository, $storeId) 394 | { 395 | $storeFilter = $this->filterBuilder 396 | ->setField('store_id') 397 | ->setConditionType('eq') 398 | ->setValue($storeId) 399 | ->create(); 400 | 401 | $identifierFilter = $this->filterBuilder 402 | ->setField('identifier') 403 | ->setConditionType('eq') 404 | ->setValue($identifier) 405 | ->create(); 406 | 407 | $storeGroup = $this->filterGroupBuilder->addFilter($storeFilter)->create(); 408 | $identifierGroup = $this->filterGroupBuilder->addFilter($identifierFilter)->create(); 409 | 410 | $searchCriteria = $this->searchCriteriaBuilder->setFilterGroups([$storeGroup, $identifierGroup])->create(); 411 | $cmsEntity = $repository->getList($searchCriteria)->getItems(); 412 | 413 | return reset($cmsEntity); 414 | } 415 | 416 | /** 417 | * @param int $id 418 | * @return array 419 | */ 420 | private function getProductAttributeData(int $id): array 421 | { 422 | $baseOptionLabel = $this->getBaseAttributesOptionLabels($id); 423 | 424 | $data = []; 425 | 426 | foreach ($this->storeManager->getStores() as $store) { 427 | if (!$store->getIsActive() || !$store->getId()) { 428 | continue; 429 | } 430 | 431 | $storeId = $store->getId(); 432 | 433 | $this->emulation->startEnvironmentEmulation($storeId); 434 | 435 | $attributeCode = $this->productAttributeRepository->get($id)->getAttributeCode(); 436 | $attributeOptions = $this->attributeOptionManagement->getItems($attributeCode); 437 | 438 | foreach ($attributeOptions as $option) { 439 | 440 | if (!$option->getValue()) { 441 | continue; 442 | } 443 | 444 | $labelInCrowdinForOriginalText = $baseOptionLabel[$option->getValue()]; 445 | $labelInCrowdinForOriginalText = str_replace('&', 'and', $labelInCrowdinForOriginalText); 446 | $labelInCrowdinForOriginalText = preg_replace("/[^A-Za-z0-9]/", '_', $labelInCrowdinForOriginalText); 447 | 448 | 449 | $data[$storeId][$labelInCrowdinForOriginalText] = (string)$option->getLabel(); 450 | } 451 | 452 | $data[$storeId]['store_id'] = $storeId; 453 | 454 | $this->emulation->stopEnvironmentEmulation(); 455 | } 456 | 457 | return $data; 458 | } 459 | 460 | /** 461 | * @param int $attributeId 462 | * @return array 463 | */ 464 | private function getBaseAttributesOptionLabels(int $attributeId): array 465 | { 466 | $this->emulation->startEnvironmentEmulation(0); 467 | $attributeCode = $this->productAttributeRepository->get($attributeId)->getAttributeCode(); 468 | 469 | 470 | $attributeOptions = $this->attributeOptionManagement->getItems($attributeCode); 471 | $baseOptionLabel = []; 472 | 473 | foreach ($attributeOptions as $option) { 474 | if (!$option->getValue()) { 475 | continue; 476 | } 477 | 478 | $baseOptionLabel[$option->getValue()] = (string)$option->getLabel(); 479 | } 480 | $this->emulation->stopEnvironmentEmulation(); 481 | 482 | return $baseOptionLabel; 483 | } 484 | 485 | /** 486 | * @param $entityType 487 | * @return array[] 488 | */ 489 | private function getCmsAttributesByType($entityType): array 490 | { 491 | $cmsAttributes = [ 492 | 'page' => [ 493 | ['label' => __('Title'), 'code' => 'title', 'type' => 'text'], 494 | ['label' => __('Content Heading'), 'code' => 'content_heading', 'type' => 'text'], 495 | ['label' => __('Content'), 'code' => 'content', 'type' => 'html'], 496 | ['label' => __('Meta Title'), 'code' => 'meta_title', 'type' => 'html'], 497 | ['label' => __('Meta Description'), 'code' => 'meta_description', 'type' => 'html'], 498 | ['label' => __('Meta Keywords'), 'code' => 'meta_keywords', 'type' => 'html'] 499 | ], 500 | 'block' => [ 501 | ['label' => __('Title'), 'code' => 'title', 'type' => 'text'], 502 | ['label' => __('Content'), 'code' => 'content', 'type' => 'html'] 503 | ] 504 | ]; 505 | 506 | return $cmsAttributes[$entityType]; 507 | } 508 | 509 | /** 510 | * @return array[] 511 | */ 512 | private function getEmailAttributes(): array 513 | { 514 | return [ 515 | ['label' => __('Template Name'), 'code' => 'template_code', 'type' => 'text'], 516 | ['label' => __('Template Subject'), 'code' => 'template_subject', 'type' => 'text'], 517 | ['label' => __('Template Text'), 'code' => 'template_text', 'type' => 'html'] 518 | ]; 519 | } 520 | 521 | /** 522 | * @param $repository 523 | * @return array 524 | */ 525 | private function getCatalogAttributes($repository): array 526 | { 527 | $inputFilter = $this->filterBuilder 528 | ->setField('frontend_input') 529 | ->setConditionType('in') 530 | ->setValue(['text', 'textarea']) 531 | ->create(); 532 | 533 | $typeFilter = $this->filterBuilder 534 | ->setField('backend_type') 535 | ->setConditionType('in') 536 | ->setValue(['varchar', 'text']) 537 | ->create(); 538 | 539 | $visibilityFilter = $this->filterBuilder 540 | ->setField('is_visible') 541 | ->setConditionType('eq') 542 | ->setValue(1) 543 | ->create(); 544 | 545 | $inputGroup = $this->filterGroupBuilder->addFilter($inputFilter)->create(); 546 | $typeGroup = $this->filterGroupBuilder->addFilter($typeFilter)->create(); 547 | $visibilityGroup = $this->filterGroupBuilder->addFilter($visibilityFilter)->create(); 548 | 549 | $searchCriteria = $this->searchCriteriaBuilder->setFilterGroups([$inputGroup, $typeGroup, $visibilityGroup])->create(); 550 | $attributesList = $repository->getList($searchCriteria)->getItems(); 551 | 552 | $attributes = []; 553 | 554 | foreach ($attributesList as $attribute) { 555 | $type = ($attribute->getFrontendInput() == 'textarea') ? 'html' : 'text'; 556 | 557 | $attributes[] = [ 558 | 'label' => $attribute->getDefaultFrontendLabel(), 559 | 'code' => $attribute->getAttributeCode() , 560 | 'type' => $type, 561 | ]; 562 | } 563 | 564 | return $attributes; 565 | } 566 | 567 | /** 568 | * @param array $fields 569 | * @param string $entityType 570 | * @return array 571 | */ 572 | private function removeNotAllowedFields(array $fields, string $entityType): array 573 | { 574 | $mappedFields = $this->config->getMappedFields(); 575 | 576 | foreach ($fields as $key => $attribute) { 577 | 578 | $attributeKey = $entityType . '/' . $attribute['code']; 579 | 580 | if (isset($mappedFields[$attributeKey]) && $mappedFields[$attributeKey] === '0') { 581 | unset($fields[$key]); 582 | } 583 | } 584 | 585 | return $fields; 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /Model/Mapped.php: -------------------------------------------------------------------------------- 1 | mathRandom = $mathRandom; 29 | } 30 | 31 | /** 32 | * Generate a storable representation of a value 33 | * 34 | * @param int|float|string|array $value 35 | * @return string 36 | */ 37 | public function serializeValue($value) 38 | { 39 | if (is_numeric($value)) { 40 | $data = (float) $value; 41 | return (string) $data; 42 | } elseif (is_array($value)) { 43 | $data = []; 44 | foreach ($value as $attributeCode => $isSyncEnabled) { 45 | if (!array_key_exists($attributeCode, $data)) { 46 | $data[$attributeCode] = $isSyncEnabled; 47 | } 48 | } 49 | return json_encode($data, JSON_UNESCAPED_UNICODE); 50 | } else { 51 | return ''; 52 | } 53 | } 54 | 55 | /** 56 | * Create a value from a storable representation 57 | * 58 | * @param int|float|string $value 59 | * @return array 60 | */ 61 | public function unserializeValue($value) 62 | { 63 | if (is_string($value) && !empty($value)) { 64 | return json_decode($value, true); 65 | } else { 66 | return []; 67 | } 68 | } 69 | 70 | /** 71 | * Check whether value is in form retrieved by _encodeArrayFieldValue() 72 | * 73 | * @param string|array $value 74 | * @return bool 75 | */ 76 | protected function isEncodedArrayFieldValue($value) 77 | { 78 | if (!is_array($value)) { 79 | return false; 80 | } 81 | 82 | unset($value['__empty']); 83 | 84 | foreach ($value as $row) { 85 | if (!is_array($row) 86 | || !array_key_exists('is_crowdin_sync_enabled', $row) 87 | || !array_key_exists('magento_translatable_fields', $row) 88 | ) { 89 | return false; 90 | } 91 | } 92 | return true; 93 | } 94 | 95 | /** 96 | * Encode value to be used in \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray 97 | * 98 | * @param array $value 99 | * @return array 100 | */ 101 | protected function encodeArrayFieldValue(array $value) 102 | { 103 | $result = []; 104 | 105 | foreach ($value as $attributeCode => $isSyncEnabled) { 106 | $resultId = $this->mathRandom->getUniqueHash('_'); 107 | $result[$resultId] = ['magento_translatable_fields' => $attributeCode, 'is_crowdin_sync_enabled' => $isSyncEnabled]; 108 | } 109 | 110 | return $result; 111 | } 112 | 113 | /** 114 | * Decode value from used in \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray 115 | * 116 | * @param array $value 117 | * @return array 118 | */ 119 | protected function decodeArrayFieldValue(array $value) 120 | { 121 | $result = []; 122 | 123 | unset($value['__empty']); 124 | 125 | foreach ($value as $row) { 126 | 127 | if (!is_array($row) 128 | || !array_key_exists('is_crowdin_sync_enabled', $row) 129 | || !array_key_exists('magento_translatable_fields', $row) 130 | ) { 131 | continue; 132 | } 133 | 134 | $result[$row['magento_translatable_fields']] = $row['is_crowdin_sync_enabled']; 135 | } 136 | 137 | return $result; 138 | } 139 | 140 | /** 141 | * Make value readable by \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray 142 | * 143 | * @param string|array $value 144 | * @return array 145 | */ 146 | public function makeArrayFieldValue($value) 147 | { 148 | $value = $this->unserializeValue($value); 149 | 150 | if (!$this->isEncodedArrayFieldValue($value)) { 151 | $value = $this->encodeArrayFieldValue($value); 152 | } 153 | return $value; 154 | } 155 | 156 | /** 157 | * Make value ready for store 158 | * 159 | * @param string|array $value 160 | * @return string 161 | */ 162 | public function makeStorableArrayFieldValue($value) 163 | { 164 | if ($this->isEncodedArrayFieldValue($value)) { 165 | $value = $this->decodeArrayFieldValue($value); 166 | } 167 | 168 | ksort($value); 169 | 170 | return $this->serializeValue($value); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Model/System/Config/Backend/Mapped.php: -------------------------------------------------------------------------------- 1 | translatableIntegrationMapped = $translatableIntegrationMapped; 47 | parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); 48 | } 49 | 50 | /** 51 | * Process data after load 52 | * 53 | * @return void 54 | */ 55 | protected function _afterLoad() 56 | { 57 | $value = $this->getValue(); 58 | $value = $this->translatableIntegrationMapped->makeArrayFieldValue($value); 59 | $this->setValue($value); 60 | } 61 | 62 | /** 63 | * Prepare data before save 64 | * 65 | * @return void 66 | */ 67 | public function beforeSave() 68 | { 69 | $value = $this->getValue(); 70 | $value = $this->translatableIntegrationMapped->makeStorableArrayFieldValue($value); 71 | $this->setValue($value); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Model/TranslationRepository.php: -------------------------------------------------------------------------------- 1 | getTranslationEntitiesList = $getTranslationEntitiesList; 60 | $this->getTranslationEntity = $getTranslationEntity; 61 | $this->updateTranslationEntity = $updateTranslationEntity; 62 | $this->_storeManager = $storeManager; 63 | $this->config = $config; 64 | } 65 | 66 | /** 67 | * @param string $storeId 68 | * @return mixed 69 | */ 70 | public function getEntitiesList($storeId) 71 | { 72 | if (!$this->config->isEnabled()) { 73 | return __('Extension doesn\'t enabled!'); 74 | } 75 | ini_set('max_execution_time', '0'); 76 | 77 | return $this->getTranslationEntitiesList->execute($storeId); 78 | 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function getStoresList() 85 | { 86 | if (!$this->config->isEnabled()) { 87 | return __('Extension doesn\'t enabled!'); 88 | } 89 | 90 | $options = []; 91 | 92 | foreach ($this->_storeManager->getStores() as $key => $value) { 93 | $options[] = [ 94 | 'id' => $value->getId(), 95 | 'name' => $value->getName(), 96 | 'locale' => $this->config->getLocaleByStoreId($value->getId()), 97 | ]; 98 | } 99 | 100 | return $options; 101 | } 102 | 103 | /** 104 | * @param string $id 105 | * @return mixed 106 | */ 107 | public function getEntity($id) 108 | { 109 | if (!$this->config->isEnabled()) { 110 | return __('Extension doesn\'t enabled!'); 111 | } 112 | 113 | return $this->getTranslationEntity->execute($id); 114 | } 115 | 116 | /** 117 | * @param string $id 118 | * @param string $data 119 | * @return mixed 120 | */ 121 | public function updateEntity(string $id, string $data) 122 | { 123 | if (!$this->config->isEnabled()) { 124 | return __('Extension doesn\'t enabled!'); 125 | } 126 | 127 | return $this->updateTranslationEntity->execute($id, $data); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Model/UpdateTranslationEntity.php: -------------------------------------------------------------------------------- 1 | categoryRepository = $categoryRepository; 163 | $this->productRepository = $productRepository; 164 | $this->emulation = $emulation; 165 | $this->emailTemplateResourceModel = $emailTemplateResourceModel; 166 | $this->emailTemplate = $emailTemplate; 167 | $this->pageRepository = $pageRepository; 168 | $this->blockRepository = $blockRepository; 169 | $this->storeManager = $storeManager; 170 | $this->resource = $resource; 171 | $this->getPageByIdentifier = $getPageByIdentifier; 172 | $this->getBlockByIdentifier = $getBlockByIdentifier; 173 | $this->pageFactory = $pageFactory; 174 | $this->blockFactory = $blockFactory; 175 | $this->searchCriteriaBuilder = $searchCriteriaBuilder; 176 | $this->filterBuilder = $filterBuilder; 177 | $this->filterGroupBuilder = $filterGroupBuilder; 178 | } 179 | 180 | 181 | /** 182 | * @param $id 183 | * @return array|void 184 | */ 185 | public function execute($id, $data) 186 | { 187 | $response = ['success' => true]; 188 | $entityData = explode('_', $id); 189 | $type = $entityData[0]; 190 | 191 | unset($entityData[0]); 192 | 193 | $id = implode('_', $entityData); 194 | $data = json_decode($data, true); 195 | 196 | if (trim((string)$id) === '') { 197 | return ['message' => 'Missed entity id!', 'success' => false]; 198 | } 199 | 200 | if (!in_array($type, $this->allowedStringId) && !is_numeric($id)) { 201 | return ['message' => 'Entity id (' . $id . ') for this entity should be numeric!', 'success' => false]; 202 | } 203 | 204 | if (!isset($data['store_id'])) { 205 | return ['message' => __('Missed store_id!'), 'success' => false]; 206 | } 207 | 208 | $this->storeId = (int)$data['store_id']; 209 | unset($data['store_id']); 210 | 211 | $this->emulation->startEnvironmentEmulation($this->storeId, 'adminhtml'); 212 | 213 | switch ($type) { 214 | case GetTranslationEntitiesList::CATEGORY_ENTITY: 215 | $this->updateCategoryData((int)$id, $data); 216 | break; 217 | case GetTranslationEntitiesList::PRODUCT_ENTITY: 218 | $this->updateProductData((int)$id, $data); 219 | break; 220 | case GetTranslationEntitiesList::EMAIL_ENTITY: 221 | $this->updateEmailTemplateData((int)$id, $data); 222 | break; 223 | case GetTranslationEntitiesList::CMS_PAGE_ENTITY: 224 | $response = $this->updateCmsPageData($id, $data); 225 | break; 226 | case GetTranslationEntitiesList::CMS_BLOCK_ENTITY: 227 | $response = $this->updateCmsBlockData($id, $data); 228 | break; 229 | case GetTranslationEntitiesList::PRODUCT_ATTRIBUTE_ENTITY: 230 | $response = $this->updateProductAttributeData((int)$id, $data); 231 | break; 232 | } 233 | 234 | $this->emulation->stopEnvironmentEmulation(); 235 | 236 | return $response; 237 | } 238 | 239 | /** 240 | * @param int $id 241 | * @param $data 242 | */ 243 | private function updateCategoryData(int $id, $data) 244 | { 245 | $category = $this->categoryRepository->get($id, $this->storeId); 246 | 247 | foreach ($data as $attributeCode => $attributeValue) { 248 | $category->setData($attributeCode, $attributeValue); 249 | 250 | /** 251 | * https://github.com/magento/magento2/issues/31083 252 | */ 253 | $category->getResource()->saveAttribute($category, $attributeCode); 254 | } 255 | 256 | //$this->categoryRepository->save($category); 257 | } 258 | 259 | /** 260 | * @param int $id 261 | * @param array $data 262 | */ 263 | private function updateProductData(int $id, array $data) 264 | { 265 | $product = $this->productRepository->getById($id, false, $this->storeId); 266 | 267 | foreach ($data as $attributeCode => $attributeValue) { 268 | $product->setData($attributeCode, $attributeValue); 269 | 270 | /** 271 | * https://github.com/magento/magento2/issues/31083 272 | */ 273 | $product->getResource()->saveAttribute($product, $attributeCode); 274 | } 275 | 276 | //$this->productRepository->save($product); 277 | } 278 | 279 | /** 280 | * @param string $identifier 281 | * @param array $data 282 | */ 283 | private function updateCmsPageData(string $identifier, array $data) 284 | { 285 | $page = $this->getCmsEntityByIdentifier($identifier, $this->pageRepository, $this->pageFactory); 286 | 287 | // if page saved on all store views 288 | if (in_array(0, $page->getStoreId())) { 289 | // create new page 290 | $page = $this->duplicateCmsPage($page, $this->storeId); 291 | } 292 | 293 | foreach ($data as $attributeCode => $attributeValue) { 294 | $page->setData($attributeCode, $attributeValue); 295 | } 296 | 297 | $this->pageRepository->save($page); 298 | } 299 | 300 | /** 301 | * @param $page 302 | * @param $storeId 303 | * @return mixed 304 | */ 305 | private function duplicateCmsPage($page, $storeId) 306 | { 307 | $storeIds = []; 308 | 309 | foreach ($this->storeManager->getStores() as $store) { 310 | $storeIds[$store->getId()] = $store->getId(); 311 | } 312 | 313 | unset($storeIds[$storeId]); 314 | 315 | $page->setStoreId($storeIds); 316 | $this->pageRepository->save($page); 317 | 318 | $newPage = $this->pageFactory 319 | ->create(['data' => $page->getData()]) 320 | ->setId(null) 321 | //->setIsActive(0) 322 | ->setCreationTime(null) 323 | ->setUpdateTime(null) 324 | ->setStoreId([$storeId]); 325 | 326 | return $this->pageRepository->save($newPage); 327 | } 328 | 329 | /** 330 | * @param string $identifier 331 | * @param array $data 332 | */ 333 | private function updateCmsBlockData(string $identifier, array $data) 334 | { 335 | $block = $this->getCmsEntityByIdentifier($identifier, $this->blockRepository, $this->blockFactory); 336 | 337 | // if block saved on all store views 338 | if (in_array(0, $block->getStoreId())) { 339 | // create new page 340 | $block = $this->duplicateCmsBlock($block, $this->storeId); 341 | } 342 | 343 | foreach ($data as $attributeCode => $attributeValue) { 344 | $block->setData($attributeCode, $attributeValue); 345 | } 346 | 347 | $this->blockRepository->save($block); 348 | } 349 | 350 | /** 351 | * @param $block 352 | * @param $storeId 353 | * @return mixed 354 | */ 355 | private function duplicateCmsBlock($block, $storeId) 356 | { 357 | $storeIds = []; 358 | 359 | foreach ($this->storeManager->getStores() as $store) { 360 | $storeIds[$store->getId()] = $store->getId(); 361 | } 362 | 363 | unset($storeIds[$storeId]); 364 | $block->setStores($storeIds); 365 | $this->blockRepository->save($block); 366 | 367 | $newBlock = $this->blockFactory 368 | ->create(['data' => $block->getData()]) 369 | ->setId(null) 370 | //->setIsActive(0) 371 | ->setCreationTime(null) 372 | ->setUpdateTime(null) 373 | ->setStores([$storeId]); 374 | 375 | return $this->blockRepository->save($newBlock); 376 | } 377 | 378 | /** 379 | * @param $identifier 380 | * @param $repository 381 | * @return false|mixed 382 | */ 383 | private function getCmsEntityByIdentifier($identifier, $repository, $factory) 384 | { 385 | $storeFilter = $this->filterBuilder 386 | ->setField('store_id') 387 | ->setConditionType('eq') 388 | ->setValue($this->storeId) 389 | ->create(); 390 | 391 | $identifierFilter = $this->filterBuilder 392 | ->setField('identifier') 393 | ->setConditionType('eq') 394 | ->setValue($identifier) 395 | ->create(); 396 | 397 | $storeGroup = $this->filterGroupBuilder->addFilter($storeFilter)->create(); 398 | $identifierGroup = $this->filterGroupBuilder->addFilter($identifierFilter)->create(); 399 | 400 | $searchCriteria = $this->searchCriteriaBuilder->setFilterGroups([$storeGroup, $identifierGroup])->create(); 401 | $cmsEntity = $repository->getList($searchCriteria)->getItems(); 402 | 403 | if (!$cmsEntity) { 404 | // in case if entity does not exist on specific store, need to check if it assigned to all stores 405 | $storeFilter = $this->filterBuilder 406 | ->setField('store_id') 407 | ->setConditionType('eq') 408 | ->setValue(0) 409 | ->create(); 410 | 411 | $storeGroup = $this->filterGroupBuilder->addFilter($storeFilter)->create(); 412 | $searchCriteria = $this->searchCriteriaBuilder->setFilterGroups([$storeGroup, $identifierGroup])->create(); 413 | $cmsEntity = $repository->getList($searchCriteria)->getItems(); 414 | } 415 | 416 | if (!$cmsEntity) { 417 | $cmsEntity = $factory->create( 418 | ['data' => 419 | [ 420 | 'title' => $identifier, 421 | 'identifier' => $identifier 422 | ] 423 | ] 424 | ) 425 | ->setId(null) 426 | ->setCreationTime(null) 427 | ->setUpdateTime(null) 428 | ->setStoreId([$this->storeId]); 429 | 430 | return $repository->save($cmsEntity); 431 | } 432 | 433 | return reset($cmsEntity); 434 | } 435 | 436 | /** 437 | * @param $id 438 | * @param array $data 439 | */ 440 | private function updateEmailTemplateData($id, array $data) 441 | { 442 | $emailTemplate = $this->emailTemplate->load($id); 443 | 444 | foreach ($data as $attributeCode => $attributeValue) { 445 | $emailTemplate->setData($attributeCode, $attributeValue); 446 | } 447 | 448 | $this->emailTemplateResourceModel->save($emailTemplate); 449 | } 450 | 451 | /** 452 | * @param $attributeId 453 | * @param array $data 454 | */ 455 | private function updateProductAttributeData($attributeId, array $data) 456 | { 457 | $connection = $this->resource->getConnection(); 458 | 459 | /** 460 | * [Black] => Array 461 | * ( 462 | * [value] => Black 463 | * [store_id] => 0 464 | * [option_id] => 49 465 | * [attribute_id] => 93 466 | * ) 467 | * [Blue] => Array 468 | * ( 469 | * [value] => Blue 470 | * [store_id] => 0 471 | * [option_id] => 50 472 | * [attribute_id] => 93 473 | * ) 474 | */ 475 | $selectAttributesData = $connection->select() 476 | ->from( 477 | ['option' => $this->resource->getTableName('eav_attribute_option')], 478 | [ 479 | 'value' => 'option_value.value', 480 | 'store_id' => 'option_value.store_id', 481 | 'option_id' => 'option.option_id', 482 | 'attribute_id' => 'option.attribute_id', 483 | ] 484 | ) 485 | ->joinLeft( 486 | ['option_value' => $this->resource->getTableName('eav_attribute_option_value')], 487 | 'option.option_id = option_value.option_id', 488 | [] 489 | )->where('option.attribute_id = ?', $attributeId); 490 | 491 | $attributeOriginalValues = $connection->fetchAssoc( 492 | $selectAttributesData->where('option_value.store_id = ?', Store::DEFAULT_STORE_ID) 493 | ); 494 | 495 | foreach ($data as $originalAttributeVal => $translatedAttributeVal) { 496 | if (isset($attributeOriginalValues[$originalAttributeVal])) { 497 | $optionId = $attributeOriginalValues[$originalAttributeVal]['option_id']; 498 | 499 | $valueId = $connection->fetchOne( 500 | $connection->select() 501 | ->from( 502 | [$connection->getTableName('eav_attribute_option_value')], 503 | ['value_id']) 504 | ->where('store_id = ?', $this->storeId) 505 | ->where('option_id = ?', $optionId) 506 | ); 507 | 508 | $attributeData = [ 509 | 'value_id' => (int)$valueId, 510 | 'option_id' => $optionId, 511 | 'store_id' => $this->storeId, 512 | 'value' => $translatedAttributeVal 513 | ]; 514 | 515 | if (!$valueId) { 516 | unset($attributeData['value_id']); 517 | } 518 | 519 | $connection->insertOnDuplicate($this->resource->getTableName('eav_attribute_option_value'), $attributeData); 520 | } 521 | } 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Crowdin Integration for Magento 2](https://magefan.com/magento-2-crowdin-integration) 2 | 3 | [![Total Downloads](https://poser.pugx.org/magefan/module-crowdin-integration/downloads)](https://packagist.org/packages/magefan/module-crowdin-integration) 4 | [![Latest Stable Version](https://poser.pugx.org/magefan/module-crowdin-integration/v/stable)](https://packagist.org/packages/magefan/module-crowdin-integration) 5 | 6 | This extension allows you to integrate Magento 2 Shop with Crowdin to easily manage and perform your store localization. 7 | 8 | 9 | 10 | ## Requirements 11 | * Magento Community 2.2.0-2.4.x (CE, EE, ECE, B2B) 12 | 13 | ## Online Documentation 14 | [Crowdin Documentation](https://store.crowdin.com/magento?utm_source=github.com&utm_medium=referral&utm_campaign=magefan) 15 | 16 | [Magefan Documentation](https://magefan.com/magento-2-crowdin-integration/documentation) 17 | 18 | ## Installation Instruction 19 | https://magefan.com/magento-2-crowdin-integration/installation 20 | 21 | ## Support 22 | If you have any issues, please [contact us](mailto:support@magefan.com) 23 | then if you still need help, open a bug report in GitHub's 24 | [issue tracker](https://github.com/magefan/module-blog/issues). 25 | 26 | ## License 27 | The code is licensed under [EULA](https://magefan.com/end-user-license-agreement). 28 | -------------------------------------------------------------------------------- /Setup/InstallData.php: -------------------------------------------------------------------------------- 1 | integrationManager = $integrationManager; 48 | $this->configWriter = $configWriter; 49 | $this->translationEntity = $translationEntity; 50 | } 51 | 52 | /** 53 | * @param ModuleDataSetupInterface $setup 54 | * @param ModuleContextInterface $context 55 | */ 56 | public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) 57 | { 58 | $this->createCrowdinIntegrationApiKeys(); 59 | $this->presetTranslatableAttributes(); 60 | } 61 | 62 | private function presetTranslatableAttributes() 63 | { 64 | $presetValue = []; 65 | 66 | foreach ($this->translationEntity->getTranslatableAttributes() as $key => $translatableAttributes) { 67 | foreach ($translatableAttributes as $translatableAttribute) { 68 | $presetValue[$key .'/'. $translatableAttribute['code']] = 1; 69 | } 70 | } 71 | 72 | $this->configWriter->save(Config::XML_PATH_FIELDS_MAPPING, json_encode($presetValue), $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeId = 0); 73 | } 74 | 75 | private function createCrowdinIntegrationApiKeys() 76 | { 77 | /** 78 | * Keep resource the same as in api.xml!!! 79 | */ 80 | $integrations = [ 81 | Config::INTEGRATION_NAME => [ 82 | Integration::NAME => 'Crowdin Integration', 83 | Integration::EMAIL => '', 84 | Integration::ENDPOINT => '', 85 | Integration::IDENTITY_LINK_URL => '', 86 | 'resource' => [ 87 | 'Magefan_Crowdin::config_integration', 88 | 'Magento_Cms::save_design' 89 | ] 90 | ], 91 | ]; 92 | 93 | $this->integrationManager->processConfigBasedIntegrations($integrations); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magefan/module-crowdin-integration", 3 | "description": "Crowdin Integration", 4 | "type": "magento2-module", 5 | "version": "2.0.7", 6 | "autoload": { 7 | "files": [ 8 | "registration.php" 9 | ], 10 | "psr-4": { 11 | "Magefan\\Crowdin\\": "" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | magefan 16 | Magefan_Crowdin::config_integration 17 | 18 | 19 | 1 20 | 21 | Magefan\Crowdin\Block\Adminhtml\System\Config\Form\Info 22 | 23 | 24 | 25 | 26 | Magento\Config\Model\Config\Source\Yesno 27 | 28 | 29 | 30 | 31 | Magefan\Crowdin\Block\Adminhtml\Form\IntegrationKey 32 | 33 | 1 34 | 35 | 36 | 37 | 38 | 39 | 1 40 | 41 | Magefan\Crowdin\Block\Adminhtml\Form\Field\Mapped 42 | Magefan\Crowdin\Model\System\Config\Backend\Mapped 43 | If some field won't be selected in mapping, it will be enabled by default. If you remove all mappings, all fields will be enable to sync to Crowdin 44 | 45 | 46 | 47 | 48 | 49 | 50 | Magefan\Crowdin\Model\Config\Source\SynchronizationType 51 | 52 | 53 | 54 | Magefan\Crowdin\Model\Config\Source\CategoryList 55 | Magefan\Crowdin\Block\Adminhtml\Form\Multiselect 56 | 57 | 1,2 58 | 59 | 60 | 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | { 14 | "block\/content":"1", 15 | "block\/title":"1", 16 | "category\/assign_product_with_attributes":"1", 17 | "category\/description":"1", 18 | "category\/meta_description":"1", 19 | "category\/meta_keywords":"1", 20 | "category\/meta_title":"1", 21 | "category\/name":"1", 22 | "category\/url_key":"1", 23 | "email\/template_code":"1", 24 | "email\/template_subject":"1", 25 | "email\/template_text":"1", 26 | "page\/content":"1", 27 | "page\/content_heading":"1", 28 | "page\/meta_description":"1", 29 | "page\/meta_keywords":"1", 30 | "page\/meta_title":"1", 31 | "page\/title":"1", 32 | "product\/accessory_custom_engraving_text":"1", 33 | "product\/accessory_description_detailed_extra":"1", 34 | "product\/accessory_description_extra":"1", 35 | "product\/accessory_description_pagebuilder_extra":"1", 36 | "product\/description":"1", 37 | "product\/description_extra":"1", 38 | "product\/meta_description":"1", 39 | "product\/meta_keyword":"1", 40 | "product\/meta_title":"1", 41 | "product\/mfdc_reviews_count":"1", 42 | "product\/name":"1", 43 | "product\/short_description":"1", 44 | "product\/url_key":"1", 45 | "product\/video_file":"1" 46 | } 47 | 48 | 49 | 50 | 0 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /etc/integration/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /etc/webapi.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | isIntegrationActivated()) { ?> 13 |
14 | 17 |
18 | 19 | 39 | 40 | 62 | 63 | 64 |
65 | 66 | Activate Integration named "crowdin" to synchronize Magento and Crowdin.', $block->getUrl('adminhtml/integration'))?> 67 | 68 |
69 | 70 | 76 | 77 | -------------------------------------------------------------------------------- /view/adminhtml/web/css/chosen.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Chosen, a Select Box Enhancer for jQuery and Prototype 3 | by Patrick Filler for Harvest, http://getharvest.com 4 | 5 | Version 1.8.7 6 | Full source at https://github.com/harvesthq/chosen 7 | Copyright (c) 2011-2018 Harvest http://getharvest.com 8 | 9 | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md 10 | This file is generated by `grunt build`, do not edit it by hand. 11 | */ 12 | .chosen-container { 13 | position: relative; 14 | display: inline-block; 15 | vertical-align: middle; 16 | font-size: 13px; 17 | -webkit-user-select: none; 18 | -moz-user-select: none; 19 | -ms-user-select: none; 20 | user-select: none 21 | } 22 | 23 | .chosen-container * { 24 | -webkit-box-sizing: border-box; 25 | box-sizing: border-box 26 | } 27 | 28 | .chosen-container .chosen-drop { 29 | position: absolute; 30 | top: 100%; 31 | z-index: 1010; 32 | width: 100%; 33 | border: 1px solid #aaa; 34 | border-top: 0; 35 | background: #fff; 36 | -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 37 | box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 38 | clip: rect(0, 0, 0, 0); 39 | -webkit-clip-path: inset(100% 100%); 40 | clip-path: inset(100% 100%) 41 | } 42 | 43 | .chosen-container.chosen-with-drop .chosen-drop { 44 | clip: auto; 45 | -webkit-clip-path: none; 46 | clip-path: none 47 | } 48 | 49 | .chosen-container a { 50 | cursor: pointer 51 | } 52 | 53 | .chosen-container .chosen-single .group-name, .chosen-container .search-choice .group-name { 54 | margin-right: 4px; 55 | overflow: hidden; 56 | white-space: nowrap; 57 | text-overflow: ellipsis; 58 | font-weight: 400; 59 | color: #999 60 | } 61 | 62 | .chosen-container .chosen-single .group-name:after, .chosen-container .search-choice .group-name:after { 63 | content: ":"; 64 | padding-left: 2px; 65 | vertical-align: top 66 | } 67 | 68 | .chosen-container-single .chosen-single { 69 | position: relative; 70 | display: block; 71 | overflow: hidden; 72 | padding: 0 0 0 8px; 73 | height: 25px; 74 | border: 1px solid #aaa; 75 | border-radius: 5px; 76 | background-color: #fff; 77 | background: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #fff), color-stop(50%, #f6f6f6), color-stop(52%, #eee), to(#f4f4f4)); 78 | background: linear-gradient(#fff 20%, #f6f6f6 50%, #eee 52%, #f4f4f4 100%); 79 | background-clip: padding-box; 80 | -webkit-box-shadow: 0 0 3px #fff inset, 0 1px 1px rgba(0, 0, 0, .1); 81 | box-shadow: 0 0 3px #fff inset, 0 1px 1px rgba(0, 0, 0, .1); 82 | color: #444; 83 | text-decoration: none; 84 | white-space: nowrap; 85 | line-height: 24px 86 | } 87 | 88 | .chosen-container-single .chosen-default { 89 | color: #999 90 | } 91 | 92 | .chosen-container-single .chosen-single span { 93 | display: block; 94 | overflow: hidden; 95 | margin-right: 26px; 96 | text-overflow: ellipsis; 97 | white-space: nowrap 98 | } 99 | 100 | .chosen-container-single .chosen-single-with-deselect span { 101 | margin-right: 38px 102 | } 103 | 104 | .chosen-container-single .chosen-single abbr { 105 | position: absolute; 106 | top: 6px; 107 | right: 26px; 108 | display: block; 109 | width: 12px; 110 | height: 12px; 111 | background: url(../images/chosen-sprite.png) -42px 1px no-repeat; 112 | font-size: 1px 113 | } 114 | 115 | .chosen-container-single .chosen-single abbr:hover { 116 | background-position: -42px -10px 117 | } 118 | 119 | .chosen-container-single.chosen-disabled .chosen-single abbr:hover { 120 | background-position: -42px -10px 121 | } 122 | 123 | .chosen-container-single .chosen-single div { 124 | position: absolute; 125 | top: 0; 126 | right: 0; 127 | display: block; 128 | width: 18px; 129 | height: 100% 130 | } 131 | 132 | .chosen-container-single .chosen-single div b { 133 | display: block; 134 | width: 100%; 135 | height: 100%; 136 | background: url(../images/chosen-sprite.png) no-repeat 0 2px 137 | } 138 | 139 | .chosen-container-single .chosen-search { 140 | position: relative; 141 | z-index: 1010; 142 | margin: 0; 143 | padding: 3px 4px; 144 | white-space: nowrap 145 | } 146 | 147 | .chosen-container-single .chosen-search input[type=text] { 148 | margin: 1px 0; 149 | padding: 4px 20px 4px 5px; 150 | width: 100%; 151 | height: auto; 152 | outline: 0; 153 | border: 1px solid #aaa; 154 | background: url(../images/chosen-sprite.png) no-repeat 100% -20px; 155 | font-size: 1em; 156 | font-family: sans-serif; 157 | line-height: normal; 158 | border-radius: 0 159 | } 160 | 161 | .chosen-container-single .chosen-drop { 162 | margin-top: -1px; 163 | border-radius: 0 0 4px 4px; 164 | background-clip: padding-box 165 | } 166 | 167 | .chosen-container-single.chosen-container-single-nosearch .chosen-search { 168 | position: absolute; 169 | clip: rect(0, 0, 0, 0); 170 | -webkit-clip-path: inset(100% 100%); 171 | clip-path: inset(100% 100%) 172 | } 173 | 174 | .chosen-container .chosen-results { 175 | color: #444; 176 | position: relative; 177 | overflow-x: hidden; 178 | overflow-y: auto; 179 | margin: 0 4px 4px 0; 180 | padding: 0 0 0 4px; 181 | max-height: 240px; 182 | -webkit-overflow-scrolling: touch 183 | } 184 | 185 | .chosen-container .chosen-results li { 186 | display: none; 187 | margin: 0; 188 | padding: 5px 6px; 189 | list-style: none; 190 | line-height: 15px; 191 | word-wrap: break-word; 192 | -webkit-touch-callout: none 193 | } 194 | 195 | .chosen-container .chosen-results li.active-result { 196 | display: list-item; 197 | cursor: pointer 198 | } 199 | 200 | .chosen-container .chosen-results li.disabled-result { 201 | display: list-item; 202 | color: #ccc; 203 | cursor: default 204 | } 205 | 206 | .chosen-container .chosen-results li.highlighted { 207 | background-color: #3875d7; 208 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #3875d7), color-stop(90%, #2a62bc)); 209 | background-image: linear-gradient(#3875d7 20%, #2a62bc 90%); 210 | color: #fff 211 | } 212 | 213 | .chosen-container .chosen-results li.no-results { 214 | color: #777; 215 | display: list-item; 216 | background: #f4f4f4 217 | } 218 | 219 | .chosen-container .chosen-results li.group-result { 220 | display: list-item; 221 | font-weight: 700; 222 | cursor: default 223 | } 224 | 225 | .chosen-container .chosen-results li.group-option { 226 | padding-left: 15px 227 | } 228 | 229 | .chosen-container .chosen-results li em { 230 | font-style: normal; 231 | text-decoration: underline 232 | } 233 | 234 | .chosen-container-multi .chosen-choices { 235 | position: relative; 236 | overflow: hidden; 237 | margin: 0; 238 | padding: 0 5px; 239 | width: 100%; 240 | height: auto; 241 | border: 1px solid #aaa; 242 | background-color: #fff; 243 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(1%, #eee), color-stop(15%, #fff)); 244 | background-image: linear-gradient(#eee 1%, #fff 15%); 245 | cursor: text 246 | } 247 | 248 | .chosen-container-multi .chosen-choices li { 249 | float: left; 250 | list-style: none 251 | } 252 | 253 | .chosen-container-multi .chosen-choices li.search-field { 254 | margin: 0; 255 | padding: 0; 256 | white-space: nowrap 257 | } 258 | 259 | .chosen-container-multi .chosen-choices li.search-field input[type=text] { 260 | margin: 1px 0; 261 | padding: 0; 262 | height: 25px; 263 | outline: 0; 264 | border: 0 !important; 265 | background: 0 0 !important; 266 | -webkit-box-shadow: none; 267 | box-shadow: none; 268 | color: #999; 269 | font-size: 100%; 270 | font-family: sans-serif; 271 | line-height: normal; 272 | border-radius: 0; 273 | width: 25px 274 | } 275 | 276 | .chosen-container-multi .chosen-choices li.search-choice { 277 | position: relative; 278 | margin: 3px 5px 3px 0; 279 | padding: 3px 20px 3px 5px; 280 | border: 1px solid #aaa; 281 | max-width: 100%; 282 | border-radius: 3px; 283 | background-color: #eee; 284 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), to(#eee)); 285 | background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); 286 | background-size: 100% 19px; 287 | background-repeat: repeat-x; 288 | background-clip: padding-box; 289 | -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, .05); 290 | box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, .05); 291 | color: #333; 292 | line-height: 13px; 293 | cursor: default 294 | } 295 | 296 | .chosen-container-multi .chosen-choices li.search-choice span { 297 | word-wrap: break-word 298 | } 299 | 300 | .chosen-container-multi .chosen-choices li.search-choice .search-choice-close { 301 | position: absolute; 302 | top: 4px; 303 | right: 3px; 304 | display: block; 305 | width: 12px; 306 | height: 12px; 307 | background: url(../images/chosen-sprite.png) -42px 1px no-repeat; 308 | font-size: 1px 309 | } 310 | 311 | .chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover { 312 | background-position: -42px -10px 313 | } 314 | 315 | .chosen-container-multi .chosen-choices li.search-choice-disabled { 316 | padding-right: 5px; 317 | border: 1px solid #ccc; 318 | background-color: #e4e4e4; 319 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), to(#eee)); 320 | background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); 321 | color: #666 322 | } 323 | 324 | .chosen-container-multi .chosen-choices li.search-choice-focus { 325 | background: #d4d4d4 326 | } 327 | 328 | .chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close { 329 | background-position: -42px -10px 330 | } 331 | 332 | .chosen-container-multi .chosen-results { 333 | margin: 0; 334 | padding: 0 335 | } 336 | 337 | .chosen-container-multi .chosen-drop .result-selected { 338 | display: list-item; 339 | color: #ccc; 340 | cursor: default 341 | } 342 | 343 | .chosen-container-active .chosen-single { 344 | border: 1px solid #5897fb; 345 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); 346 | box-shadow: 0 0 5px rgba(0, 0, 0, .3) 347 | } 348 | 349 | .chosen-container-active.chosen-with-drop .chosen-single { 350 | border: 1px solid #aaa; 351 | border-bottom-right-radius: 0; 352 | border-bottom-left-radius: 0; 353 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #eee), color-stop(80%, #fff)); 354 | background-image: linear-gradient(#eee 20%, #fff 80%); 355 | -webkit-box-shadow: 0 1px 0 #fff inset; 356 | box-shadow: 0 1px 0 #fff inset 357 | } 358 | 359 | .chosen-container-active.chosen-with-drop .chosen-single div { 360 | border-left: none; 361 | background: 0 0 362 | } 363 | 364 | .chosen-container-active.chosen-with-drop .chosen-single div b { 365 | background-position: -18px 2px 366 | } 367 | 368 | .chosen-container-active .chosen-choices { 369 | border: 1px solid #5897fb; 370 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); 371 | box-shadow: 0 0 5px rgba(0, 0, 0, .3) 372 | } 373 | 374 | .chosen-container-active .chosen-choices li.search-field input[type=text] { 375 | color: #222 !important 376 | } 377 | 378 | .chosen-disabled { 379 | opacity: .5 !important; 380 | cursor: default 381 | } 382 | 383 | .chosen-disabled .chosen-single { 384 | cursor: default 385 | } 386 | 387 | .chosen-disabled .chosen-choices .search-choice .search-choice-close { 388 | cursor: default 389 | } 390 | 391 | .chosen-rtl { 392 | text-align: right 393 | } 394 | 395 | .chosen-rtl .chosen-single { 396 | overflow: visible; 397 | padding: 0 8px 0 0 398 | } 399 | 400 | .chosen-rtl .chosen-single span { 401 | margin-right: 0; 402 | margin-left: 26px; 403 | direction: rtl 404 | } 405 | 406 | .chosen-rtl .chosen-single-with-deselect span { 407 | margin-left: 38px 408 | } 409 | 410 | .chosen-rtl .chosen-single div { 411 | right: auto; 412 | left: 3px 413 | } 414 | 415 | .chosen-rtl .chosen-single abbr { 416 | right: auto; 417 | left: 26px 418 | } 419 | 420 | .chosen-rtl .chosen-choices li { 421 | float: right 422 | } 423 | 424 | .chosen-rtl .chosen-choices li.search-field input[type=text] { 425 | direction: rtl 426 | } 427 | 428 | .chosen-rtl .chosen-choices li.search-choice { 429 | margin: 3px 5px 3px 0; 430 | padding: 3px 5px 3px 19px 431 | } 432 | 433 | .chosen-rtl .chosen-choices li.search-choice .search-choice-close { 434 | right: auto; 435 | left: 4px 436 | } 437 | 438 | .chosen-rtl.chosen-container-single .chosen-results { 439 | margin: 0 0 4px 4px; 440 | padding: 0 4px 0 0 441 | } 442 | 443 | .chosen-rtl .chosen-results li.group-option { 444 | padding-right: 15px; 445 | padding-left: 0 446 | } 447 | 448 | .chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div { 449 | border-right: none 450 | } 451 | 452 | .chosen-rtl .chosen-search input[type=text] { 453 | padding: 4px 5px 4px 20px; 454 | background: url(chosen-sprite.png) no-repeat -30px -20px; 455 | direction: rtl 456 | } 457 | 458 | .chosen-rtl.chosen-container-single .chosen-single div b { 459 | background-position: 6px 2px 460 | } 461 | 462 | .chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b { 463 | background-position: -12px 2px 464 | } 465 | 466 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi), only screen and (min-resolution: 1.5dppx) { 467 | .chosen-container .chosen-results-scroll-down span, .chosen-container .chosen-results-scroll-up span, .chosen-container-multi .chosen-choices .search-choice .search-choice-close, .chosen-container-single .chosen-search input[type=text], .chosen-container-single .chosen-single abbr, .chosen-container-single .chosen-single div b, .chosen-rtl .chosen-search input[type=text] { 468 | background-image: url(../images/chosen-sprite@2x.png) !important; 469 | background-size: 52px 37px !important; 470 | background-repeat: no-repeat !important 471 | } 472 | } 473 | 474 | .chosen-container.chosen-container-multi { 475 | width: 250px !important; 476 | } 477 | 478 | .mf-action-submenu.action-submenu { 479 | top: 100%; 480 | margin-top: 2px; 481 | } 482 | 483 | .mf-action-submenu.action-submenu._active { 484 | max-height: none; 485 | overflow-y: unset; 486 | } 487 | 488 | .mf-field-submenu.action-submenu { 489 | display: flex; 490 | align-items: center; 491 | padding: 8px; 492 | } 493 | 494 | .mf-action-submenu.action-submenu._active .mf-field-submenu.action-submenu { 495 | right: -20%; 496 | } 497 | 498 | .mf-field-submenu.action-submenu .mf-label{ 499 | margin: 9px; 500 | } 501 | 502 | .mf-field-submenu.action-submenu .mf-input{ 503 | margin-right: 10px; 504 | } 505 | 506 | .action-submenu._active { 507 | border: 0px; 508 | } 509 | 510 | .mf-action-submenu.action-submenu._active .mf-field-submenu.action-submenu.mf-select { 511 | right: -120%; 512 | } 513 | -------------------------------------------------------------------------------- /view/adminhtml/web/images/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magefan/module-crowdin-integration/900ddd907a9f08a1a3bdfde266a048ee9ccf629c/view/adminhtml/web/images/chosen-sprite.png -------------------------------------------------------------------------------- /view/adminhtml/web/images/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magefan/module-crowdin-integration/900ddd907a9f08a1a3bdfde266a048ee9ccf629c/view/adminhtml/web/images/chosen-sprite@2x.png -------------------------------------------------------------------------------- /view/adminhtml/web/js/chosen.jquery.min.js: -------------------------------------------------------------------------------- 1 | /* Chosen v1.8.7 | (c) 2011-2018 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */ 2 | 3 | (function(){var t,e,s,i,n=function(t,e){return function(){return t.apply(e,arguments)}},r=function(t,e){function s(){this.constructor=t}for(var i in e)o.call(e,i)&&(t[i]=e[i]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},o={}.hasOwnProperty;(i=function(){function t(){this.options_index=0,this.parsed=[]}return t.prototype.add_node=function(t){return"OPTGROUP"===t.nodeName.toUpperCase()?this.add_group(t):this.add_option(t)},t.prototype.add_group=function(t){var e,s,i,n,r,o;for(e=this.parsed.length,this.parsed.push({array_index:e,group:!0,label:t.label,title:t.title?t.title:void 0,children:0,disabled:t.disabled,classes:t.className}),o=[],s=0,i=(r=t.childNodes).length;s"+this.escape_html(t.group_label)+""+t.html:t.html},t.prototype.mouse_enter=function(){return this.mouse_on_container=!0},t.prototype.mouse_leave=function(){return this.mouse_on_container=!1},t.prototype.input_focus=function(t){if(this.is_multiple){if(!this.active_field)return setTimeout(function(t){return function(){return t.container_mousedown()}}(this),50)}else if(!this.active_field)return this.activate_field()},t.prototype.input_blur=function(t){if(!this.mouse_on_container)return this.active_field=!1,setTimeout(function(t){return function(){return t.blur_test()}}(this),100)},t.prototype.label_click_handler=function(t){return this.is_multiple?this.container_mousedown(t):this.activate_field()},t.prototype.results_option_build=function(t){var e,s,i,n,r,o,h;for(e="",h=0,n=0,r=(o=this.results_data).length;n=this.max_shown_results));n++);return e},t.prototype.result_add_option=function(t){var e,s;return t.search_match&&this.include_option_in_results(t)?(e=[],t.disabled||t.selected&&this.is_multiple||e.push("active-result"),!t.disabled||t.selected&&this.is_multiple||e.push("disabled-result"),t.selected&&e.push("result-selected"),null!=t.group_array_index&&e.push("group-option"),""!==t.classes&&e.push(t.classes),s=document.createElement("li"),s.className=e.join(" "),t.style&&(s.style.cssText=t.style),s.setAttribute("data-option-array-index",t.array_index),s.innerHTML=t.highlighted_html||t.html,t.title&&(s.title=t.title),this.outerHTML(s)):""},t.prototype.result_add_group=function(t){var e,s;return(t.search_match||t.group_match)&&t.active_options>0?((e=[]).push("group-result"),t.classes&&e.push(t.classes),s=document.createElement("li"),s.className=e.join(" "),s.innerHTML=t.highlighted_html||this.escape_html(t.label),t.title&&(s.title=t.title),this.outerHTML(s)):""},t.prototype.results_update_field=function(){if(this.set_default_text(),this.is_multiple||this.results_reset_cleanup(),this.result_clear_highlight(),this.results_build(),this.results_showing)return this.winnow_results()},t.prototype.reset_single_select_options=function(){var t,e,s,i,n;for(n=[],t=0,e=(s=this.results_data).length;t"+this.escape_html(s)+""+this.escape_html(p)),null!=a&&(a.group_match=!0)):null!=r.group_array_index&&this.results_data[r.group_array_index].search_match&&(r.search_match=!0)));return this.result_clear_highlight(),_<1&&h.length?(this.update_results_content(""),this.no_results(h)):(this.update_results_content(this.results_option_build()),(null!=t?t.skip_highlight:void 0)?void 0:this.winnow_results_set_highlight())},t.prototype.get_search_regex=function(t){var e,s;return s=this.search_contains?t:"(^|\\s|\\b)"+t+"[^\\s]*",this.enable_split_word_search||this.search_contains||(s="^"+s),e=this.case_sensitive_search?"":"i",new RegExp(s,e)},t.prototype.search_string_match=function(t,e){var s;return s=e.exec(t),!this.search_contains&&(null!=s?s[1]:void 0)&&(s.index+=1),s},t.prototype.choices_count=function(){var t,e,s;if(null!=this.selected_option_count)return this.selected_option_count;for(this.selected_option_count=0,t=0,e=(s=this.form_field.options).length;t0?this.keydown_backstroke():this.pending_backstroke||(this.result_clear_highlight(),this.results_search());break;case 13:t.preventDefault(),this.results_showing&&this.result_select(t);break;case 27:this.results_showing&&this.results_hide();break;case 9:case 16:case 17:case 18:case 38:case 40:case 91:break;default:this.results_search()}},t.prototype.clipboard_event_checker=function(t){if(!this.is_disabled)return setTimeout(function(t){return function(){return t.results_search()}}(this),50)},t.prototype.container_width=function(){return null!=this.options.width?this.options.width:this.form_field.offsetWidth+"px"},t.prototype.include_option_in_results=function(t){return!(this.is_multiple&&!this.display_selected_options&&t.selected)&&(!(!this.display_disabled_options&&t.disabled)&&!t.empty)},t.prototype.search_results_touchstart=function(t){return this.touch_started=!0,this.search_results_mouseover(t)},t.prototype.search_results_touchmove=function(t){return this.touch_started=!1,this.search_results_mouseout(t)},t.prototype.search_results_touchend=function(t){if(this.touch_started)return this.search_results_mouseup(t)},t.prototype.outerHTML=function(t){var e;return t.outerHTML?t.outerHTML:((e=document.createElement("div")).appendChild(t),e.innerHTML)},t.prototype.get_single_html=function(){return'\n '+this.default_text+'\n
\n
\n
\n \n
    \n
    '},t.prototype.get_multi_html=function(){return'
      \n
    • \n \n
    • \n
    \n
    \n
      \n
      '},t.prototype.get_no_results_html=function(t){return'
    • \n '+this.results_none_found+" "+this.escape_html(t)+"\n
    • "},t.browser_is_supported=function(){return"Microsoft Internet Explorer"===window.navigator.appName?document.documentMode>=8:!(/iP(od|hone)/i.test(window.navigator.userAgent)||/IEMobile/i.test(window.navigator.userAgent)||/Windows Phone/i.test(window.navigator.userAgent)||/BlackBerry/i.test(window.navigator.userAgent)||/BB10/i.test(window.navigator.userAgent)||/Android.*Mobile/i.test(window.navigator.userAgent))},t.default_multiple_text="Select Some Options",t.default_single_text="Select an Option",t.default_no_result_text="No results match",t}(),(t=jQuery).fn.extend({chosen:function(i){return e.browser_is_supported()?this.each(function(e){var n,r;r=(n=t(this)).data("chosen"),"destroy"!==i?r instanceof s||n.data("chosen",new s(this,i)):r instanceof s&&r.destroy()}):this}}),s=function(s){function n(){return n.__super__.constructor.apply(this,arguments)}return r(n,e),n.prototype.setup=function(){return this.form_field_jq=t(this.form_field),this.current_selectedIndex=this.form_field.selectedIndex},n.prototype.set_up_html=function(){var e,s;return(e=["chosen-container"]).push("chosen-container-"+(this.is_multiple?"multi":"single")),this.inherit_select_classes&&this.form_field.className&&e.push(this.form_field.className),this.is_rtl&&e.push("chosen-rtl"),s={"class":e.join(" "),title:this.form_field.title},this.form_field.id.length&&(s.id=this.form_field.id.replace(/[^\w]/g,"_")+"_chosen"),this.container=t("
      ",s),this.container.width(this.container_width()),this.is_multiple?this.container.html(this.get_multi_html()):this.container.html(this.get_single_html()),this.form_field_jq.hide().after(this.container),this.dropdown=this.container.find("div.chosen-drop").first(),this.search_field=this.container.find("input").first(),this.search_results=this.container.find("ul.chosen-results").first(),this.search_field_scale(),this.search_no_results=this.container.find("li.no-results").first(),this.is_multiple?(this.search_choices=this.container.find("ul.chosen-choices").first(),this.search_container=this.container.find("li.search-field").first()):(this.search_container=this.container.find("div.chosen-search").first(),this.selected_item=this.container.find(".chosen-single").first()),this.results_build(),this.set_tab_index(),this.set_label_behavior()},n.prototype.on_ready=function(){return this.form_field_jq.trigger("chosen:ready",{chosen:this})},n.prototype.register_observers=function(){return this.container.on("touchstart.chosen",function(t){return function(e){t.container_mousedown(e)}}(this)),this.container.on("touchend.chosen",function(t){return function(e){t.container_mouseup(e)}}(this)),this.container.on("mousedown.chosen",function(t){return function(e){t.container_mousedown(e)}}(this)),this.container.on("mouseup.chosen",function(t){return function(e){t.container_mouseup(e)}}(this)),this.container.on("mouseenter.chosen",function(t){return function(e){t.mouse_enter(e)}}(this)),this.container.on("mouseleave.chosen",function(t){return function(e){t.mouse_leave(e)}}(this)),this.search_results.on("mouseup.chosen",function(t){return function(e){t.search_results_mouseup(e)}}(this)),this.search_results.on("mouseover.chosen",function(t){return function(e){t.search_results_mouseover(e)}}(this)),this.search_results.on("mouseout.chosen",function(t){return function(e){t.search_results_mouseout(e)}}(this)),this.search_results.on("mousewheel.chosen DOMMouseScroll.chosen",function(t){return function(e){t.search_results_mousewheel(e)}}(this)),this.search_results.on("touchstart.chosen",function(t){return function(e){t.search_results_touchstart(e)}}(this)),this.search_results.on("touchmove.chosen",function(t){return function(e){t.search_results_touchmove(e)}}(this)),this.search_results.on("touchend.chosen",function(t){return function(e){t.search_results_touchend(e)}}(this)),this.form_field_jq.on("chosen:updated.chosen",function(t){return function(e){t.results_update_field(e)}}(this)),this.form_field_jq.on("chosen:activate.chosen",function(t){return function(e){t.activate_field(e)}}(this)),this.form_field_jq.on("chosen:open.chosen",function(t){return function(e){t.container_mousedown(e)}}(this)),this.form_field_jq.on("chosen:close.chosen",function(t){return function(e){t.close_field(e)}}(this)),this.search_field.on("blur.chosen",function(t){return function(e){t.input_blur(e)}}(this)),this.search_field.on("keyup.chosen",function(t){return function(e){t.keyup_checker(e)}}(this)),this.search_field.on("keydown.chosen",function(t){return function(e){t.keydown_checker(e)}}(this)),this.search_field.on("focus.chosen",function(t){return function(e){t.input_focus(e)}}(this)),this.search_field.on("cut.chosen",function(t){return function(e){t.clipboard_event_checker(e)}}(this)),this.search_field.on("paste.chosen",function(t){return function(e){t.clipboard_event_checker(e)}}(this)),this.is_multiple?this.search_choices.on("click.chosen",function(t){return function(e){t.choices_click(e)}}(this)):this.container.on("click.chosen",function(t){t.preventDefault()})},n.prototype.destroy=function(){return t(this.container[0].ownerDocument).off("click.chosen",this.click_test_action),this.form_field_label.length>0&&this.form_field_label.off("click.chosen"),this.search_field[0].tabIndex&&(this.form_field_jq[0].tabIndex=this.search_field[0].tabIndex),this.container.remove(),this.form_field_jq.removeData("chosen"),this.form_field_jq.show()},n.prototype.search_field_disabled=function(){return this.is_disabled=this.form_field.disabled||this.form_field_jq.parents("fieldset").is(":disabled"),this.container.toggleClass("chosen-disabled",this.is_disabled),this.search_field[0].disabled=this.is_disabled,this.is_multiple||this.selected_item.off("focus.chosen",this.activate_field),this.is_disabled?this.close_field():this.is_multiple?void 0:this.selected_item.on("focus.chosen",this.activate_field)},n.prototype.container_mousedown=function(e){var s;if(!this.is_disabled)return!e||"mousedown"!==(s=e.type)&&"touchstart"!==s||this.results_showing||e.preventDefault(),null!=e&&t(e.target).hasClass("search-choice-close")?void 0:(this.active_field?this.is_multiple||!e||t(e.target)[0]!==this.selected_item[0]&&!t(e.target).parents("a.chosen-single").length||(e.preventDefault(),this.results_toggle()):(this.is_multiple&&this.search_field.val(""),t(this.container[0].ownerDocument).on("click.chosen",this.click_test_action),this.results_show()),this.activate_field())},n.prototype.container_mouseup=function(t){if("ABBR"===t.target.nodeName&&!this.is_disabled)return this.results_reset(t)},n.prototype.search_results_mousewheel=function(t){var e;if(t.originalEvent&&(e=t.originalEvent.deltaY||-t.originalEvent.wheelDelta||t.originalEvent.detail),null!=e)return t.preventDefault(),"DOMMouseScroll"===t.type&&(e*=40),this.search_results.scrollTop(e+this.search_results.scrollTop())},n.prototype.blur_test=function(t){if(!this.active_field&&this.container.hasClass("chosen-container-active"))return this.close_field()},n.prototype.close_field=function(){return t(this.container[0].ownerDocument).off("click.chosen",this.click_test_action),this.active_field=!1,this.results_hide(),this.container.removeClass("chosen-container-active"),this.clear_backstroke(),this.show_search_field_default(),this.search_field_scale(),this.search_field.blur()},n.prototype.activate_field=function(){if(!this.is_disabled)return this.container.addClass("chosen-container-active"),this.active_field=!0,this.search_field.val(this.search_field.val()),this.search_field.focus()},n.prototype.test_active_click=function(e){var s;return(s=t(e.target).closest(".chosen-container")).length&&this.container[0]===s[0]?this.active_field=!0:this.close_field()},n.prototype.results_build=function(){return this.parsing=!0,this.selected_option_count=null,this.results_data=i.select_to_array(this.form_field),this.is_multiple?this.search_choices.find("li.search-choice").remove():(this.single_set_selected_text(),this.disable_search||this.form_field.options.length<=this.disable_search_threshold?(this.search_field[0].readOnly=!0,this.container.addClass("chosen-container-single-nosearch")):(this.search_field[0].readOnly=!1,this.container.removeClass("chosen-container-single-nosearch"))),this.update_results_content(this.results_option_build({first:!0})),this.search_field_disabled(),this.show_search_field_default(),this.search_field_scale(),this.parsing=!1},n.prototype.result_do_highlight=function(t){var e,s,i,n,r;if(t.length){if(this.result_clear_highlight(),this.result_highlight=t,this.result_highlight.addClass("highlighted"),i=parseInt(this.search_results.css("maxHeight"),10),r=this.search_results.scrollTop(),n=i+r,s=this.result_highlight.position().top+this.search_results.scrollTop(),(e=s+this.result_highlight.outerHeight())>=n)return this.search_results.scrollTop(e-i>0?e-i:0);if(s0)return this.form_field_label.on("click.chosen",this.label_click_handler)},n.prototype.show_search_field_default=function(){return this.is_multiple&&this.choices_count()<1&&!this.active_field?(this.search_field.val(this.default_text),this.search_field.addClass("default")):(this.search_field.val(""),this.search_field.removeClass("default"))},n.prototype.search_results_mouseup=function(e){var s;if((s=t(e.target).hasClass("active-result")?t(e.target):t(e.target).parents(".active-result").first()).length)return this.result_highlight=s,this.result_select(e),this.search_field.focus()},n.prototype.search_results_mouseover=function(e){var s;if(s=t(e.target).hasClass("active-result")?t(e.target):t(e.target).parents(".active-result").first())return this.result_do_highlight(s)},n.prototype.search_results_mouseout=function(e){if(t(e.target).hasClass("active-result")||t(e.target).parents(".active-result").first())return this.result_clear_highlight()},n.prototype.choice_build=function(e){var s,i;return s=t("
    • ",{"class":"search-choice"}).html(""+this.choice_label(e)+""),e.disabled?s.addClass("search-choice-disabled"):((i=t("",{"class":"search-choice-close","data-option-array-index":e.array_index})).on("click.chosen",function(t){return function(e){return t.choice_destroy_link_click(e)}}(this)),s.append(i)),this.search_container.before(s)},n.prototype.choice_destroy_link_click=function(e){if(e.preventDefault(),e.stopPropagation(),!this.is_disabled)return this.choice_destroy(t(e.target))},n.prototype.choice_destroy=function(t){if(this.result_deselect(t[0].getAttribute("data-option-array-index")))return this.active_field?this.search_field.focus():this.show_search_field_default(),this.is_multiple&&this.choices_count()>0&&this.get_search_field_value().length<1&&this.results_hide(),t.parents("li").first().remove(),this.search_field_scale()},n.prototype.results_reset=function(){if(this.reset_single_select_options(),this.form_field.options[0].selected=!0,this.single_set_selected_text(),this.show_search_field_default(),this.results_reset_cleanup(),this.trigger_form_field_change(),this.active_field)return this.results_hide()},n.prototype.results_reset_cleanup=function(){return this.current_selectedIndex=this.form_field.selectedIndex,this.selected_item.find("abbr").remove()},n.prototype.result_select=function(t){var e,s;if(this.result_highlight)return e=this.result_highlight,this.result_clear_highlight(),this.is_multiple&&this.max_selected_options<=this.choices_count()?(this.form_field_jq.trigger("chosen:maxselected",{chosen:this}),!1):(this.is_multiple?e.removeClass("active-result"):this.reset_single_select_options(),e.addClass("result-selected"),s=this.results_data[e[0].getAttribute("data-option-array-index")],s.selected=!0,this.form_field.options[s.options_index].selected=!0,this.selected_option_count=null,this.is_multiple?this.choice_build(s):this.single_set_selected_text(this.choice_label(s)),this.is_multiple&&(!this.hide_results_on_select||t.metaKey||t.ctrlKey)?t.metaKey||t.ctrlKey?this.winnow_results({skip_highlight:!0}):(this.search_field.val(""),this.winnow_results()):(this.results_hide(),this.show_search_field_default()),(this.is_multiple||this.form_field.selectedIndex!==this.current_selectedIndex)&&this.trigger_form_field_change({selected:this.form_field.options[s.options_index].value}),this.current_selectedIndex=this.form_field.selectedIndex,t.preventDefault(),this.search_field_scale())},n.prototype.single_set_selected_text=function(t){return null==t&&(t=this.default_text),t===this.default_text?this.selected_item.addClass("chosen-default"):(this.single_deselect_control_build(),this.selected_item.removeClass("chosen-default")),this.selected_item.find("span").html(t)},n.prototype.result_deselect=function(t){var e;return e=this.results_data[t],!this.form_field.options[e.options_index].disabled&&(e.selected=!1,this.form_field.options[e.options_index].selected=!1,this.selected_option_count=null,this.result_clear_highlight(),this.results_showing&&this.winnow_results(),this.trigger_form_field_change({deselected:this.form_field.options[e.options_index].value}),this.search_field_scale(),!0)},n.prototype.single_deselect_control_build=function(){if(this.allow_single_deselect)return this.selected_item.find("abbr").length||this.selected_item.find("span").first().after(''),this.selected_item.addClass("chosen-single-with-deselect")},n.prototype.get_search_field_value=function(){return this.search_field.val()},n.prototype.get_search_text=function(){return t.trim(this.get_search_field_value())},n.prototype.escape_html=function(e){return t("
      ").text(e).html()},n.prototype.winnow_results_set_highlight=function(){var t,e;if(e=this.is_multiple?[]:this.search_results.find(".result-selected.active-result"),null!=(t=e.length?e.first():this.search_results.find(".active-result").first()))return this.result_do_highlight(t)},n.prototype.no_results=function(t){var e;return e=this.get_no_results_html(t),this.search_results.append(e),this.form_field_jq.trigger("chosen:no_results",{chosen:this})},n.prototype.no_results_clear=function(){return this.search_results.find(".no-results").remove()},n.prototype.keydown_arrow=function(){var t;return this.results_showing&&this.result_highlight?(t=this.result_highlight.nextAll("li.active-result").first())?this.result_do_highlight(t):void 0:this.results_show()},n.prototype.keyup_arrow=function(){var t;return this.results_showing||this.is_multiple?this.result_highlight?(t=this.result_highlight.prevAll("li.active-result")).length?this.result_do_highlight(t.first()):(this.choices_count()>0&&this.results_hide(),this.result_clear_highlight()):void 0:this.results_show()},n.prototype.keydown_backstroke=function(){var t;return this.pending_backstroke?(this.choice_destroy(this.pending_backstroke.find("a").first()),this.clear_backstroke()):(t=this.search_container.siblings("li.search-choice").last()).length&&!t.hasClass("search-choice-disabled")?(this.pending_backstroke=t,this.single_backstroke_delete?this.keydown_backstroke():this.pending_backstroke.addClass("search-choice-focus")):void 0},n.prototype.clear_backstroke=function(){return this.pending_backstroke&&this.pending_backstroke.removeClass("search-choice-focus"),this.pending_backstroke=null},n.prototype.search_field_scale=function(){var e,s,i,n,r,o,h;if(this.is_multiple){for(r={position:"absolute",left:"-1000px",top:"-1000px",display:"none",whiteSpace:"pre"},s=0,i=(o=["fontSize","fontStyle","fontWeight","fontFamily","lineHeight","textTransform","letterSpacing"]).length;s").css(r)).text(this.get_search_field_value()),t("body").append(e),h=e.width()+25,e.remove(),this.container.is(":visible")&&(h=Math.min(this.container.outerWidth()-10,h)),this.search_field.width(h)}},n.prototype.trigger_form_field_change=function(t){return this.form_field_jq.trigger("input",t),this.form_field_jq.trigger("change",t)},n}()}).call(this); --------------------------------------------------------------------------------